😺
React コンテキストの真の使い方(useContext)
※ ジョーク記事です
※ ジョークですが、プログラムはきちんと動きます
ソースコード https://github.com/SoraKumo001/next-context
動作確認 https://next-context-ten.vercel.app/
コンテキストについて
下記の公式ドキュメントでコンテキストはこのように説明されています。
コンテクストは各階層で手動でプロパティを下に渡すことなく、コンポーネントツリー内でデータを渡す方法を提供します。
つまり、ツリー内で有効となるデータ領域です。
それ以上を期待すると残念なことになるので、とにかくタダのデータ領域だと思いましょう。
なぜコンテキストを使うのか
ツリー内でスコープを持つデータ領域が欲しいからです。
それ以上のことを期待してはいけません。
コンテキストの使い方
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
を改変して、useSelector
とuseDispatch
を生み出します。
データ構造が違うので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には対象のコンポーネント名のみ表示されます。つまり無駄な再レンダリングは起こりません。
違うボタンを押した際に直前の変更コンポーネントも一回だけ再評価されますが、仕様です。
気分はまるでRedux!
あれ?
参考記事
こちらの記事を参考に書かせていただきました
こちらの方が真っ当な方法です
Discussion