mobx-react와 React Hooks API 함께 사용하기
mobx-react v4에서 v6로 마이그레이션
React hooks와 mobx-react v4
React 16.8에서 도입된 React Hooks는 함수형 컴포넌트에서 상태를 관리할 수 있는 새로운 방법을 제공한다. Hooks API를 사용하면 함수형 컴포넌트에서도 자체 상태값을 가질 수 있으며, 클래스 컴포넌트의 라이프사이클 메소드도 대체할 수 있다. 물론 클래스 컴포넌트가 더 좋은지 함수형이 더 좋은지는 논쟁의 여지가 있다. 그리고 컴포넌트가 커지면 복잡도가 높아지는 것은 클래스나 함수형이나 마찬가지다. 하지만 개인적으로는 적절한 크기의 컴포넌트라면 확실히 Hooks를 사용했을 때 코드가 더 간결하고 직관적이라는 인상을 받았다.
그런데 mobx-react가 제공하는 observer
로 래핑된 함수형 컴포넌트에서 useState
같은 훅 API를 사용하려고 하면 React는 “훅은 함수형 컴포넌트에서만 사용할 수 있다”는 메시지와 함께 오류를 발생시킨다. mobx-react v4의 observer
API는 클래스 컴포넌트를 리턴하는 hoc(higher order component)이기 때문이다. 훅을 사용하면서 mobx-react의 store에서 데이터를 가져오려면 mobx-react v6 또는 mobx-react-lite를 사용해야 한다.
mobx-react v6
mobx-react-lite는 React 16.8과 훅을 지원하기 위해 함수형 컴포넌트에서만 사용할 수 있는 API만 제공하고 있다. 특히 mobx-react-lite는 React legacy Context API를 사용하는 Provider
, inject
API를 제공하지 않는다. 대신 React.createContext API를 사용해서 store를 가져오는 방법을 제안한다.
mobx-react v4, v5를 사용해서 앱을 개발하고 있었다면 mobx-react-lite의 React Hooks를 위한 API를 포함하고 있는 mobx-react v6로 마이그레이션 하면 된다. 마이그레이션이라 해도 이미 구현되어 있는 클래스 컴포넌트는 수정할 필요가 없어서 크게 부담이 없다. 물론 공식 문서에서는 store를 새로운 API(=useLocalStore)로 구현하는 방법을 권하고 있다. 하지만 Hook API가 제공된다고 해서 클래스 컴포넌트를 버려야 할 이유는 없으므로 어떻게 사용할지는 이 도구를 사용하는 사람의 선택에 달렸다고 할 수 있다.
함수형 컴포넌트에서 inject
를 대체할 useStore
함수
앞서 언급했듯이 마이그레이션을 위해서 기존의 코드를 수정해야 할 필요는 없다. 다만 훅을 사용하는 함수형 컴포넌트를 위한 inject
hoc는 별도로 제공하지 않으므로 store를 가져오기 위한 헬퍼 함수를 만들어야 한다. 간단하게 구현 가능하다.
import React from 'react';
import { MobXProviderContext } from 'mobx-react';
/**
* React hooks를 사용하는 컴포넌트에서 store를 가져올 때 사용한다.
* 참조) https://mobx-react.js.org/recipes-migration#hooks-for-the-rescue
*/
function useStores() {
return React.useContext(MobXProviderContext);
}
export default useStores;
React.useContext
API는 파라미터로 전달된 Context의 현재 값을 반환한다. 거기에 MobXProviderContext
를 사용하면 mobx-react의 Provider
가 제공하는 store 객체를 가져올 수 있다.
import { observer } from 'mobx-react'
import { useStores } from '../useStores'
const UserInfo = observer(() => {
const { user } = useStores()
return (
<div>
name: {user.name}
</div>
)
})
컴포넌트를 observer
hoc로 래핑하는 과정은 동일하다. 대신 클래스 컴포넌트의 inject
hoc를 사용하는 대신 useStores
헬퍼 함수를 사용해서 store 객체를 가져올 수 있다.
개발을 진행하면서 특정 데이터를 사용하는 패턴이 보인다면 커스텀 훅을 만드는 것처럼 커스텀 useStores 함수를 만들 수도 있을 것이다.
import { useObserver } from 'mobx-react'
function useUserData() {
const { user, login } = useStores()
return useObserver(() => ({ // useObserver를 사용해서 리턴하는 값의 업데이트를 계속 반영한다
userName: user.name,
isLoggedIn: login.isLoggedIn,
}))
}
const UserInfo = () => {
const { username, isLoggedIn } = useUserData()
return (
<div>
{username} is {isLoggedIn ? 'on' : 'off'}
</div>
)
}
useUserData
가 리턴하는 객체가 useObserver
로 래핑되어 있기에 컴포넌트를 observer
로 래핑하지 않아도 동작한다. 하지만 컴포넌트에 다른 observable을 사용한다면 래핑이 필요하다.
함수형 컴포넌트를 위한 injector hoc
함수형 컴포넌트에서는 useStores
를 사용하면 된다. 하지만 클래스 컴포넌트에서 사용하던 스타일로 inject
hoc를 사용하고 싶다면 직접 구현할 수 있다. 공식 문서에서도 간단한 구현을 제공하고 있다. 하지만 개인적으로는 useStores
를 사용하는 편이 더 간단하고 훅 API에도 어울려 보인다.
import { observer } from 'mobx-react'
import { useStores } from '../useStores'
function inject(selector, baseComponent) {
const component = ownProps => {
const store = useStores()
return useObserver(() => baseComponent(selector(store, ownProps)))
}
component.displayName = baseComponent.name
return component
}