はじめに
この記事は React 級位者向けの記事です。React 有段者の方であればこの記事に書いてあるようなことは形は違えどすでに当たり前に行っていることだと思います。
ただし、有段者の方でも、それを無意識で自然に行っていたものであれば、それを有意識化するという意味でこの記事が役立つかもしれませんのでご興味があれば是非。
なお、React 初心者向けの記事ではないかもしれません。ある程度用語を知らないと読み進めることも困難な気がしますが、読んで損はないと思いますのでやる気のある方は是非。
レンダリングとロジックを分離することの重要性
React を使うにあたっては「レンダリングとロジックの分離」という概念が重要です。
理由は、React が重きを置いているのがレンダリング機能だからです。関数型コンポーネントが主流になってからは特にそれが顕著で、「レンダリングとロジックの分離」は、「描画処理に副作用(Side Effect)を混ぜるな」とか、「宣言的なものに命令的な処理を混ぜるな」とか、「純粋(pure)性に違反するな」とか、その応用といえそうないろいろな言い方はありますが、口酸っぱく言われます。
口酸っぱく言う割に、初心者・級位者向けのサンプルコードでは全然それを意識していないコードが多く、というか、上記で述べたように React はレンダリングに重きを置いている、つまりロジックの部分は各ユーザーが好きにやってくださいね、というスタンスなので、結局初心者・級位者は「レンダリングとロジックの分離」というものがよくわからず、そのやり方もよくわからない、ということになっている気がします。
なお、React の、ロジック部分はユーザー任せ、というスタンス自体は非難されるべきものではなく、むしろ称賛されるべきものです。あとで説明しますが、その部分については各ユーザーの美意識による要素が多いので、各ユーザー任せにしとくのが合理的だからです。
モジュールレベルの変数を許容する流れ
React は、特に関数型コンポーネントが主となってからですが、モジュールレベルの変数を使うことを避けようとしてきました。プログラミング界隈での、古からの、グローバル変数を避けよう、関数型プログラミングを目指そう、1時間に一度は椅子から立ち上がろう(?)、などの考えと一致するものですので、悪いものではありません。
しかし、Pros & Cons により、「モジュールレベルの変数も、利用ルールやそれを守るための設計がきちんとしている限りは別に使ってもよくね?」という流れとなりました。
特に問題視されたのは、いわゆる「props バケツリレー問題」です。
const YourComponentWrapper = props => {
const [state, setState] = useState(0)
return (<YourComponent1 state={state} setState={setState}>)
}
const YourComponent1 = props => {
return (<Fragment>
<button onClick={e => props.setState(prevState => prevState + 1)}>{props.state}</button>
<YourComponent2 state={props.state} setState={props.setState} />
</Fragment>)
}
const YourComponent2 = props => {
return (<Fragment>
<button onClick={e => props.setState(prevState => prevState + 2)}>{props.state}</button>
<YourComponent3 state={props.state} setState={props.setState} />
</Fragment>)
}
const YourComponent3 = props => {
return (<button onClick={e => props.setState(prevState => prevState + 3)}>{props.state}</button>)
}
この程度であればまだ許容範囲かもしれませんが、コンポーネントが多機能になってくると地獄と化します。「宣言的くたばれ」と思わず叫びたくなるくらいです。
その解決策として公式に導入されたのが、createContext
や useContext
です。当時は「なるほどね~、なんか賢そうなやり方ね~」と思ったわけですが、階層増加問題(Wrapper Hell) など別の問題がみえてくるにつれ人気は低下します(個人的な感想です)。
Context 型から Store 型への流れ
「公式は context というモジュール変数の使用を許容しているよね?モジュール変数を使っていいなら、もっと良い方法がありそうじゃね」
賢い人たちはピントきてしまいました。この流れでできたのが、useSyncExternalStore
や Jotai、さらには React の時期バージョンで導入されそうな use(store)
です(React19 で実装された use(resource)
や use(context)
とは別物です、念のため)。
特に Jotai は現時点ではおそらくベストプラクティスです。Jotai 使用禁止のプロジェクトにはちょっと関わりたくないくらいです。
なぜ、Jotai か。他の記事にも書きましたが、作者が Daishi Kato 氏だからです。日本の宝である優秀なエンジニアが「Jotai 使っとけば Pure 原則に絶対違反しないよ!」と保障(保証?)してくれるのです。それ以上の何を求めるというのですか。
なお、Redux や Recoil などのもはや伝説ともいえる有名どころについてはあえて無視しましたが、それらのようなグローバル状態管理ライブラリの人気にも関わらず、公式は「context さえあれば十分ではないか、朕は民主主義の悪い部分であるとおもふ」という感じで長らく放置していました(個人的な感想です。既出のように、React はロジックに深く関わらない、という強い意思の表れともいえるので、絶対的非難の対象とも言い切れません)。
setState の容れ物どうする?
「Jotai 便利だな~」と Jotai を利用して粗大ごみ制作物を作りまくっていると、ある日ふと気づきます。「state
を Jotai で管理するのはよいとして、 setState
はどうすんねん。それとも、setState
も Jotai で管理すればよい?」
state などのリアクティブ変数は内容が変更されたら再描画が発生する、すなわち、リアクティブ変数はレンダリングとロジック両方の架け橋ともいえる存在なので、Jotai などの優秀なライブラリにその管理を任せるのが合理的です。しかし setState はどうでしょう。ただの Side Effect なのでそこまで厳密な管理は不要なはずです。とはいっても、ただ裸のモジュール変数を使ってそれらを管理するのも「う~ん」という感じです。特に、チームの場合は管理方法を統一したくなるのが親心(?)というものです。
そこで setState などの Side Effect 用の関数や変数を管理する仕組みを作ってみました。
なお、setState のように単純なものは通常 Jotai の機能に吸収されてしまいますので、Jotai さえあれば足りるため、この問題に気付きにくいのだろうとは思います。もっと実践的で分かりやすい例があればよかったのですが思いつきませんでしたスミマセン。
PLUTO
PLUTO は、鉄腕アトムの敵の巨大ロボットの名前です。Jotai の API で使うのが Atom
なのでそれに対抗(?)すべく名付けました。
ソースです。
/*
「PLUTO、レンダリングとロジックを分離するために造られた巨大ロボ。」
PLUTO: Decouple Rendering and Logic.
Usage: https://qiita.com/uniho/items/75f4949bf33dc921b9c1
Note:
Unlike standard React hooks, `usePluto()` can be called anywhere
(not just at the top level of a component), similar to `use()` **API** in React 19.
This means you can call it inside event handlers, async callbacks (or even outside of components).
If you also need to share state values (not just side-effect functions or values),
consider using a state management library like Jotai.
License: MIT
*/
export function pluto() {
const ref = {};
// const key = Symbol();
// plutoStore.set(key, ref);
return {
ref,
// key,
// use: () => ref,
// get: () => plutoStore.get(key),
// delete: () => plutoStore.delete(key),
// has: () => plutoStore.has(key),
};
}
export function usePluto(pluto) {
if (pluto && pluto.ref) {
return pluto.ref;
}
throw new Error('pluto does not exist.');
}
// export const plutoStore = new Map();
使用例です。
const yourPluto = pluto()
const YourComponentWrapper = props => {
const [state, setState] = useState(0)
const plt = usePluto(yourPluto)
useEffect(() => {
plt.setState = setState
return () => {
// Cleanup
}
}, [])
return (<YourComponent1 state={state} />)
}
const YourComponent1 = props => {
const plt = usePluto(yourPluto)
return (<Fragment>
<button onClick={e => plt.setState(prevState => prevState + 1)}>{props.state}</button>
<YourComponent2 state={props.state} />
</Fragment>)
}
const YourComponent2 = props => {
const plt = usePluto(yourPluto)
return (<Fragment>
<button onClick={e => plt.setState(prevState => prevState + 2)}>{props.state}</button>
<YourComponent3 state={props.state} />
</Fragment>)
}
const YourComponent3 = props => {
const plt = usePluto(yourPluto)
return (<button onClick={e => plt.setState(prevState => prevState + 3)}>{props.state}</button>)
}
解説
API の雰囲気は Jotai の API に似せてみました。当初は store にあたる配列を準備して pluto.use()
や pluto.get()
などで引き出して使うような形式で設計していましたが、現在の姿に落ち着きました。この辺はソースの機能美をどこに求めるか(というほど大袈裟なものではない)、によるものと思われますので、各々が自由に変更してください。
ソースを見ていただければわかる通り usePluto
は純粋な Hooks ではありませんので、コンポーネント内の最上位レベルでしか使えないなどのルールはありません。それどころかコンポーネント外ですら使えます。
もっといえば、 usePluto
Hook はなくても困らないわけですが、「この Hook はあったほうがよい」、と Gemini(ジェミナイと読むのが正しいらしい)上級SE と ChatGPT 上級 SE が強くおっしゃるので残しました。
使用例では、state はあえてバケツリレーのままにしてあります。state はリアクティブ変数ですので、そのバケツリレーの回避には Jotai を使ってください。PLUTO の出番となるのは、特殊な Side Effect をリレーすれば十分なときだけです。
最後に
いかがでしたでしょうか。
PLUTO が「レンダリングとロジックの分離違反について警告してくれる!」などの夢を抱いていた方には申し訳ありません。でも、この記事を読んで PLUTO の本質を理解いただいた級位者の方は、もはや有段者です!
最後まで拙文にお付き合いいただきありがとうございました。みなさんの React Life に少しでもお役に立てれば幸いです。それではまた。
※コメント欄にも、識者からの見解など有用な情報がありますのでご覧ください。