Jotai は、原子モデルに基づいた非常に優れた React の状態管理ライブラリです。グローバルな React の状態管理を構築するために、ボトムアップのアプローチを採用し、原子を組み合わせて状態を構築し、原子の依存関係に基づいてレンダリングを最適化します。これにより、React コンテキストの余分な再レンダリングの問題が解決され、メモ化の必要性もなくなります。
Jotai を使用すると、トップレベルのコンポーネントで結合された状態(State)を簡単に複数の Atom に分割し、状態と UI を分離して効率的に状態を管理できます。また、コンポーネント内部でも必要に応じて状態の Getter / Setter を取得することができ、大量の再レンダリングを減らすことができます。
useState
の代わりにuseAtom
を使用する#
Jotai の atom の使用方法は非常にシンプルで、通常はuseState
の initialValue をatom()
に書き込んで、直接useState
を置き換えるだけです。
const isShowAtom = atom(true)
const Foo = () => {
const [isShow, setIsShow] = useState(true)
// change to
const [isShow, setIsShow] = useAtom(isShowAtom)
}
さらに、Jotai はuseAtomValue
とuseSetAtomValue
も提供しており、必要に応じて使用できます。atomValue を消費しない場合は、useAtom
を使用する必要はありません。代わりに、useSetAtomValue
を直接使用することができます。この場合、atomValue が変化してもこのコンポーネントは再レンダリングされません。以下の例では、親コンポーネントがボタンをクリックしても再レンダリングは発生せず、Bar のみが更新されます。
const isShowAtom = atom(true)
const Foo = () => {
const setIsShow = useSetAtom(isShowAtom)
return <>
<Bar />
<button onClick={() => setIsShow(true)}>Show</button>
</>
}
const Bar = () => {
const isShow = useAtomValue(isShowAtom)
// ...
}
::: info
余談:Jotai を使用しない場合、useState による過剰な再レンダリングをどのように回避すればよいですか?
上記の例では、useState + useContext を使用して代替することができます。
const IsShowContext = createContext(false)
const SetIsShowContext = createContext<SetStateAction<boolean>>(() => {})
const Foo = () => {
const [isShow, setIsShow] = useState(false)
return (
<IsShowContext.Provider value={isShow}>
<SetIsShowContext.Provider value={setIsShow}>
<FooImpl />
</SetIsShowContext.Provider>
</IsShowContext.Provider>
)
}
const FooImpl = () => {
const setIsShow = useContext(SetIsShowContext)
return (
<>
<Bar />
<button onClick={() => setIsShow(true)}>Show</button>
</>
)
}
const Bar = () => {
const isShow = useContext(IsShowContext)
// ...
}
ただし、このようなパフォーマンスの最適化のために Context を細分化する方法は、状態が多い場合には非常にメンテナンスが難しいです。
Jotai のような状態管理ライブラリを使用したくない場合は、foxactのuseContextState
の実装を試してみることもできます。
:::
useContext
とatom
を使用してグローバルな状態をコンポーネント内に収束させる#
上記のuseAtom
をuseState
に置き換えた後、コンポーネントの状態が外部に出てしまい、コンポーネントを再利用できなくなります。コンポーネントを再利用すると、その状態は共有されます。useContext
とatom
を組み合わせて、コンポーネントの状態を再びコンポーネント内に収束させることができます。
まず、上記の単純な例を見てみましょう。
const FooContext = createContext(null)
const Foo = () => {
const contextValue = useRef({
isShowAtom: atom(false),
}).current
return (
<FooContext.Provider value={contextValue}>
<FooImpl />
</FooContext.Provider>
)
}
const FooImpl = () => {
const { isShowAtom } = useContext(FooContext)
const setIsShow = useSetAtom(isShowAtom)
return (
<>
<Bar />
<button onClick={() => setIsShow(true)}>Show</button>
</>
)
}
const Bar = () => {
const { isShowAtom } = useContext(FooContext)
const isShow = useAtomValue(isShowAtom)
// ...
}
このように、コンテキストを使用して状態を再びコンポーネント内に取り込むことができます。トップレベルでcontextValue
を作成し、その中にatom
をマウントします。子コンポーネントでは、必要に応じてatom
を消費します。このコンテキストには依存関係がないため、コンテキストの値が変化しても再レンダリングの問題はありません。ここでは、コンテキストの値は常に変化しないため、Jotai のおかげで、どこからでもcontextValue
を取得し、useAtomValue
を使用してatom
の変化を購読し、コンポーネントに反映することができます。
Atom を選択的に購読するためのツール - selectAtom
#
Jotai は、selectAtom
関数を提供しており、この関数を使用すると、既存の atom を基に読み取り専用の新しい atom を作成することができます。主な使用方法は、セレクターの実装です。
通常、atomValue がプリミティブ型である場合は、この関数を使用する必要はありません。ただし、オブジェクトなどの参照型の値の場合、Immutable の特性に基づいて、内部の大きなオブジェクトが変更されると新しいオブジェクトが作成されます。オブジェクト内部の任意の変更は、atomValue 全体を購読しているコンポーネントの更新を引き起こします。
const objectAtom = atom({
type: 'object',
value: {
foo: 1
}
})
const Foo = () => {
const setter = useSetAtom(objectAtom)
return <>
<Bar />
<button onClick={() => setter(prev => ({...prev, value: { foo:1 }}))}>Show</button>
</>
}
const Bar = () => {
const { type } = useAtomValue(objectAtom)
// ...
}
上記の例では、Bar はtype
のみを消費していますが、Foo のボタンをクリックすると、objectAtom の value が変わるため、Bar も変更されます。
const objectAtom = atom({
type: 'object',
value: {
foo: 1,
},
})
const Foo = () => {
const setter = useSetAtom(objectAtom)
return (
<>
<Bar />
<button
onClick={() => setter((prev) => ({ ...prev, value: { foo: 1 } }))}
>
Show
</button>
</>
)
}
const Bar = () => {
const type = useAtomValue(
selectAtom(
objectAtom,
useCallback((atomValue) => atomValue.type, []), // 注意这里
),
)
// ...
}
しかし、selectAtom を使用すると、内部の値を抽出することでこの問題を回避することができます。ただし、渡されるセレクターをuseCallback
でラップするか、この関数をコンポーネントの外部に抽出し、次の再レンダリングで関数が変わらないようにする必要があります。そうしないと、再レンダリングの問題が発生します。
上記の 3 つのポイントは、ほとんどのシナリオをカバーできます。早速試してみてください。
この記事は Mix Space からの同期更新であり、元のリンクは https://innei.in/posts/programming/jotai-experience-with-component-state-abstraction です。