MobX와 React Hooks를 조합하려고 하면 mobx-react-lite를 자연스럽게 접하게 됩니다.
그러나, 기존의 모든 기능이 구현되어 있지는 않아서 직접 구현을 하셔야 합니다.
예를 들면, 외부에 스토어를 생성하고 사용할때,
리액트에 안에서 사용할 수 있도록 해주는 inject나 Provider를 제공하지 않습니다.
Hooks로 코드를 작성하더라도 여전히 외부에 스토어를 작성하는 방법은 많이 쓰일것이기 때문에,
어떻게 외부에 스토어를 두고 상태를 관리하는지 알아보겠습니다.
npx create-react-app mobx-sample --typescript
또는
yarn create react-app mobx-sample --typescript
yarn add mobx mobx-react-lite
import React from 'react';
export const CityView: React.FC<{cities: string[]}> = ({ cities }) => {
return (
<ul>
{ cities.map(city => <li>{city}</li>)}
</ul>
);
}
export const CityList = () => {
return <CityView cities={[]} />
}
export default CityList;
import React from 'react';
import CityList from './city';
import './App.css';
const App: React.FC = () => {
return (
<div className="App">
<header className="App-header">
<CityList />
</header>
</div>
)
}
export default App;
const Cities = [
'Amsterdam',
'London',
'Madrid',
];
export const createStore = () => {
const store = {
get allCities() {
return Cities;
},
};
return store;
};
export type TStore = ReturnType<typeof createStore>
import React from 'react';
import { useLocalStore } from 'mobx-react-lite';
import { createStore, TStore } from './store';
export const storeContext = React.createContext<TStore | null>(null);
export const StoreProvider: React.FC = ({ children }) => {
const store = useLocalStore(createStore);
return (
<storeContext.Provider value={value}>
{children}
</storeContext.Provider>
);
};
export default StoreProvider;
import React from 'react;
import CityList from './city';
import StoreProvider from './context';
import './App.css';
const App: React.FC = () => (
<StoreProvider>
<div className="App">
<header className="App-header">
<CityList />
</header>
</div>
</StoreProvider>
);
export default App;
import React from 'react';
import { useObserver } from 'mobx-react-lite';
import { storeContext } from './context';
export const CityView: React.FC<{ cities: string[] }> = ({ cities }) => {
return (
<ul>
{cities.map(city => <li>{city}</li>)}
</ul>
);
}
export const CityList = () => {
const store = React.useContext(storeContext);
if(!store) throw Error("Store shouldn't be null");
return useObserver(() => {
return <CityView cities={store.allCities} />
});
}
export default CityList;
import { observable } from 'mobx';
export const createStore = () => {
const store = {
query: observable.box(''),
setQuery(query: string) {
store.query.set(query.toLowerCase());
},
get filteredCities() {
return Cities.filter(
city => city.toLowerCase().includes(store.query.get())
)
}
}
return store;
};
export type TStore = ReturnType<typeof createStore>;
import React from 'react';
import { useObserver } from 'mobx-react-lite';
import { storeContext } from './context';
export const CityView: React.FC<{ cities: string[] }> = ({ cities }) => {
return (
<ul>
{cities.map(city => <li>{city}</li>)}
</ul>
);
}
export const CityList = () => {
const store = React.useContext(storeContext);
if (!store) throw Error("Store shouldn't be null");
return useObserver(() => {
return <CityView cities={store.filteredCities} />
});
}
export default CityList;
import React from 'react';
import { useObserver } from 'mobx-react-lite';
import { storeContext } from './context';
const Search: React.FC = () => {
const store = React.useContext(storeContext);
if (!store) throw Error("Store shouldn't be null");
const { query, setQuery } = store;
return useObserver(() => {
return <input value={query.get()} onChange={e => setQuery(e.target.value)} />;
});
}
export default Search;
import React from 'react';
import Search from './search';
import CityList from './city';
import StoreProvider from './context';
import './App.css';
const App: React.FC = () => (
<StoreProvider>
<div className="App">
<header className="App-header">
<Search />
<CityList />
</header>
</div>
</StoreProvider>
);
export default App;
import React from 'react';
export const useStore = <ContextData>(
context: React.Context<ContextData>
) => {
const store = React.useContext(context);
if (!store) {
throw new Error();
}
return store;
};
재사용성 ❌
여전히 모든 사용처에서 useObserver로 감싸주어야한다
제네릭 ❌
context를 허용하지만 context내에 store말고 다른 값이 존재할수도 있습니다
타입안정성 👍
hooks로 가져온 값중 없는 값을 사용하려고 하면 typescript에서 에러가 발생합니다
import React from 'react';
import { useObserver } from 'mobx-react-lite';
export const useStore = <ContextData, Store>(
context: React.Context<ContextData>,
storeSelector: (contextData: ContextData) => Store,
) => {
const value = React.useContext(context);
if (!value) {
throw new Error();
}
const store = storeSelector(value);
return useObserver(() => store);
};
재사용성 ❌
useObserver로 전체 스토어를 감싸주었기 때문에, 스토어 내 어떤 값이라도 바뀌면 리렌더링이 발생합니다(호출한 곳에서 사용하지 않을때도)
제네릭 👍
스토어를 context중 일부로 선택가능하고, 일부 스토어 타입에 강제되지 않습니다.
타입안정성 👍
hooks로 가져온 값중 없는 값을 사용하려고 하면 typescript에서 에러가 발생합니다
import React from 'react';
import { useObserver } from 'mobx-react-lite';
export const useStoreData = <Selection, ContextData, Store>(
context: React.Context<ContextData>,
storeSelector: (contextData: ContextData) => Store,
dataSelector: (store: Store) => Selection
) => {
const value = React.useContext(context);
if (!value) {
throw new Error();
}
const store = storeSelector(value);
return useObserver(() => {
return dataSelector(store);
});
};
Usable 👍
dataSelector로 감싼 부분만 관찰하게 됬습니다.
Generic 👍
스토어를 context중 일부로 선택가능하고, 일부 스토어 타입에 강제되지 않습니다.
Typesafe 👍
hooks로 가져온 값중 없는 값을 사용하려고 하면 typescript에서 에러가 발생합니다
import { storeContext } from "./context";
import { TStore } from "./store";
export const useRootData = <Selection>(
dataSelector: (store: TStore) => Selection
) =>
useStoreData(storeContext, contextData => contextData!, dataSelector);
import React from 'react';
import { useRootData } from './hook';
export const CityView: React.FC<{ cities: string[] }> = ({ cities }) => {
return (
<ul>
{cities.map(city => <li>{city}</li>)}
</ul>
);
}
export const CityList = () => {
const cities = useRootData(store => store.filteredCities);
return <CityView cities={cities} />
}
export default CityList;
import React from 'react';
import { useRootData } from './hook';
const Search: React.FC = () => {
const { query, setQuery } = useRootData(store => ({
query: store.query.get(),
setQuery: store.setQuery
}));
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
export default Search;