🦔

Micro State Managiment with React Hooks を読んでReact.Contextの使い方を改善していく

2023/01/28に公開

読んだ本

Micro State Managiment with React Hooks
https://amzn.to/403Ylgh

以前のコード

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