😺

React コンテキストの真の使い方(useContext)

2021/09/23に公開約4,400字

※ ジョーク記事です
※ ジョークですが、プログラムはきちんと動きます

ソースコード https://github.com/SoraKumo001/next-context
動作確認 https://next-context-ten.vercel.app/

コンテキストについて

下記の公式ドキュメントでコンテキストはこのように説明されています。

コンテクストは各階層で手動でプロパティを下に渡すことなく、コンポーネントツリー内でデータを渡す方法を提供します。

つまり、ツリー内で有効となるデータ領域です。
それ以上を期待すると残念なことになるので、とにかくタダのデータ領域だと思いましょう。

https://ja.reactjs.org/docs/context.html

なぜコンテキストを使うのか

ツリー内でスコープを持つデータ領域が欲しいからです。
それ以上のことを期待してはいけません。

コンテキストの使い方

1.コンテキストを作成し、独自のイベント処理を放り込む
2.改変したコンテキストプロバイダーに値を渡す
3.勝手に作ったカスタムフックでデータのやりとりをする

今回は公式には載っていないコンテキストの使い方を説明します。
表示されるのは、ボタンをクリックすると値が増えるカウンターです。

1.コンテキストを魔改造する

src/libs/context.ts

import React, {
  Context,
  createContext,
  ReactNode,
  useContext,
  useEffect,
  useRef,
  useState
} from 'react'

type Manager<T> = {
  state: T
  dispatches: Set<Readonly<[React.Dispatch<React.SetStateAction<unknown>>, (state: T) => unknown]>>
}

type CustomContext<T> = {
  Provider: ({
    value,
    children
  }: {
    children: ReactNode
    value?: T
  }) => React.FunctionComponentElement<React.ProviderProps<Manager<T>>>
  _Provider: Context<Manager<T>>['Provider']
  Consumer: Context<T>['Consumer']
  displayName?: string | undefined
}

const createManager = <T>(state?: T) => ({
  state: state as T,
  dispatches: new Set<
    Readonly<[React.Dispatch<React.SetStateAction<unknown>>, (state: T) => unknown]>
  >()
})

const createCustomContext: {
  <T>(state: T): CustomContext<T>
  <T>(): CustomContext<T | undefined>
} = <T>(state?: T): CustomContext<T> => {
  const context = createContext<Manager<T>>(undefined as never)
  const customContext = context as unknown as CustomContext<T>
  customContext._Provider = context.Provider
  customContext.Provider = ({ value, children }: { children: ReactNode; value?: T }) => {
    const manager = useRef(createManager<T>(value || state)).current
    return React.createElement(customContext._Provider, { value: manager }, children)
  }
  return customContext
}

export const useSelector = <T, K>(context: CustomContext<T>, selector: (state: T) => K) => {
  const manager = useContext<Manager<T>>(context as unknown as Context<Manager<T>>)
  const [state, dispatch] = useState(() => selector(manager.state))
  useEffect(() => {
    const v = [dispatch as React.Dispatch<React.SetStateAction<unknown>>, selector] as const
    manager.dispatches.add(v)
    dispatch(selector(manager.state))
    return () => {
      manager.dispatches.delete(v)
    }
  }, [manager])
  return state
}
export const useDispatch = <T>(context: CustomContext<T>) => {
  const manager = useContext<Manager<T>>(context as unknown as Context<Manager<T>>)
  const { dispatches } = manager
  return (state: T | ((state: T) => T)) => {
    const newState = typeof state === 'function' ? (state as (state: T) => T)(manager.state) : state
    if (newState !== state) {
      manager.state = newState
      dispatches.forEach(([dispatch, selector]) => dispatch(selector(manager.state)))
    }
  }
}

export { createCustomContext as createContext }

createContextを改変して、useSelectoruseDispatchを生み出します。
データ構造が違うのでProviderにも介入しています。

2.コンテキストプロバイダーに値を渡して、ついでにコンポーネントも作る

ボタンのクリックイベントで、各ステートを更新しています

  • src/pages/index.tsx
import React from 'react'
import { createContext, useDispatch, useSelector } from '../libs/context'

const context = createContext({ a: 20, b: 100 })

const Component01 = () => {
  console.log('Component01')
  const a = useSelector(context, (v) => v.a)
  return <div>a:{a}</div>
}
const Component02 = () => {
  console.log('Component02')
  const b = useSelector(context, (v) => v.b)
  return <div>b:{b}</div>
}
const Component03 = () => {
  console.log('Component03')
  const dispatch = useDispatch(context)
  return (
    <div>
      <div>
        <button onClick={() => dispatch((v) => ({ ...v, a: v.a + 1 }))}>a</button>
        <button onClick={() => dispatch((v) => ({ ...v, b: v.b + 1 }))}>b</button>
      </div>
    </div>
  )
}

const Page = () => {
  return (
    <context.Provider>
      <Component01 />
      <Component02 />
      <Component03 />
    </context.Provider>
  )
}
export default Page

3.Component03のボタンを押す

a,bのボタンでそれぞれのカウンターが増加し、console.logには対象のコンポーネント名のみ表示されます。つまり無駄な再レンダリングは起こりません。
違うボタンを押した際に直前の変更コンポーネントも一回だけ再評価されますが、仕様です。

https://next-context-ten.vercel.app/

気分はまるでRedux!

あれ?

参考記事

こちらの記事を参考に書かせていただきました
こちらの方が真っ当な方法です

https://zenn.dev/hitoshiasano/articles/3aea56a6a8c0f7
GitHubで編集を提案

Discussion

ログインするとコメントできます