🦔
Micro State Managiment with React Hooks を読んでReact.Contextの使い方を改善していく
読んだ本
Micro State Managiment with React Hooks
以前のコード
type Data = {/* データの構造 */}
type MutateData = (newData: Data) => void
type DataContextValue = {
data: Data | null;
mutateDate: MutateData;
}
const initialState:DataContextValue = {
data: null
mutateDate: () => {/* Nothing to do */}
}
export const DataContext = React.createContext<DataContextValue>(initialState)
const useData = (id: string) => {
const { data } = fetchSomeData(id)
const mutateData = React.useCallback(await (newData) => {/* Do some async process. */}, [])
return React.useMemo(() => ({data, mutateData}), [data, mutateData])
}
type ProviderProps = { children?: ReactNode }
export cosnt DataContextProvider: React.FC<ProviderProps> = ({ children }) => {
const value = useData()
return <DataContext.Provider value={value}>
{children}
</DataContext.Provider>
}
export const DataPage = () => {
return <DataContextProvider>
<DataPageLayout />
</DataContextProvider>
}
export const DataPageLayout = () => {
const { data, mutateDate } = React.useContext(DataContext)
const onClickSubmitButton = React.useCallback((editingData: Data) => {
mutateDate(editingData)
}, [editingData, mutateDate])
return <VStack>
<DataView data={data} />
<Divider />
<DataForm data={data} onSubmit ={onClickSubmitButton} />
</VStack>
}
何がよくないか?
Contextでデータとデータの変更メソッドの両方が流れてくる。
dataを変更するたびにすべてのコンポーネントを再描画しようとする。
表示側のコンポーネントでmemo化すれば部分的な再描画は防ぐことはできる。
しかしプロパティすべての同一性チェックをしないとObjectの参照が新しければ防げないこともある。
// chakra-uiのようなものを使っている想定
const DataView: React.FC<{ data: Data }> = ({ data }) => {
return <VStack>
<Heading>{ data.title }</Heading>
<Text>{ data.body }</Text>
</VStack>
}
export default React.memo(DataView)
改善例
Contextを分ける(できるだけ複数プロパティのオブジェクトよりフィールドごとにContextがある状態)
type Data = {/* データの構造 */}
type MutateData = (newData: Data) => void
type DataContextValue = Data | null
type MutateDataContextValue = MutateData
export const DataContext = React.createContext<ContextValue>(null)
export const MutateDataContext = React.createContext<MutateDataContextValue>(() => {})
const useData = (id: string) => {
const { data } = fetchSomeData(id)
const mutateData = React.useCallback(await (newData) => {/* Do some async process. */}, [])
return React.useMemo(() => ({data, mutateData}), [data, mutateData])
}
何らかのロジックのHooksで得た値をそれぞれのProviderに流す。
type ProviderProps = { children?: ReactNode }
export cosnt DataContextProvider: React.FC<ProviderProps> = ({ children }) => {
const { data, mutateData } = useData()
return <DataContext.Provider value={data}>
<MutateDataContext.Provider value={mutateData}>
{children}
</MutateDataContext.Provider>
</DataContext.Provider>
}
export const DataPage = () => {
return <DataContextProvider>
<DataPageLayout />
</DataContextProvider>
}
コンポーネント側からはPropsが消えてContextから取得するようにする。
export const DataPageLayout = () => {
return <VStack>
<DataView />
<Divider />
<DataForm />
</VStack>
}
export const DataView = () => {
const data = React.useContext(DataContext)
if (!data) return null
return <VStack>
<Heading>{ data.title }</Heading>
<Text>{ data.body }</Text>
</VStack>
}
export const DataForm = () => {
const data = React.useContext(DataContext)
const mutateData = React.useContext(MutateDataContext)
const [editingData, setEditingData] = useState(data)
const onClickSubmitButton = React.useCallback((editingData: Data) => {
mutateDate(editingData)
}, [editingData, mutateDate])
return <VStack as="form" onSubmit={onClickSubmitButton}>
<!-- いろいろフォーム要素 -->
<Button type="submit">編集</Button>
</VStack>
}
Storybookは?
const story = {
component: DataView,
}
// デフォルトのContextの値が入る
export const Default = () => <DataView />
// 好きなデータを流せば良い
const data = {
title: 'SomeTitle',
body: 'SomeContents',
}
export const ViewSomeData = () => <DataContext.Provider value={data}>
<DataView />
</DataContext.Provider>
前提となるContextが必要な場合はStoryのdecoratorsに入れることもできる。
このとき通信が発生したりするコードがあると(ちょっと不確か)Story実行中にエラーになることがあるので、
Context, (ロジック的なHooksや通信を伴う)Provider、Contextを使うコンポーネントをファイル的に分離すると良さそう。
const story = {
component: DataView,
decorators: [
(Story: FC) => {
return <DataContext.Provider value={data}>
<Stroy />
</DataContext.Provider>
}
]
}
Discussion