はじめに
過去の話になりますが、React Redux v6というものがありました。v5ではlegacy contextを使っていたのですが、React v16.3でContext APIが登場したため、それに追従したものです。その頃にはConcurrent Modeの計画もあり(当初はAsync Reactと呼ばれていました)、Context APIをうまく使うことで、Concurrent Modeにおけるある潜在的な問題(Tearingと呼ばれます)を解決できるはずとなりました。それは、Contextにstateを入れて、Reactに更新の伝搬を任せるという方法でした。
しかし、この方法はReact Hooksが登場して困ったことになりました。 Hooksで更新の伝搬を止める方法がなかったのです。React Reduxは止むなくcontextにstoreを入れる方法に戻してv7をリリースしました。Tearingの問題の解決はあきらめた形になります。
RFC 119
React Contextの更新の伝搬の問題を解決する方法としてuseContextSelectorが提案されています。
https://github.com/reactjs/rfcs/pull/119
React Redux界隈では期待されていますが、議論は進んでいません。この提案方式では、Tearingの問題だけでなく、stale propsの問題も解決できます。
use-context-selector
しかし、オープンソースの世界です。代替品を実装しました。
https://github.com/dai-shi/use-context-selector
userlandの実装では、stale propsは解決できないのですが、実際はそれほど問題にならないか、workaroundがあることが多いと思います。
使い方
import { createContext, useContextSelector } from 'use-context-selector';
const context = createContext(null);
React.createContextの代わりにライブラリからimportしたcreateContextを使います。
const StateProvider = ({ children }) => {
const [state, setState] = useState({ count1: 0, count2: 0 });
return (
<context.Provider value={[state, setState]}>
{children}
</context.Provider>
);
};
Providerの使い方は通常と同じです。
const Counter1 = () => {
const count1 = useContextSelector(context, v => v[0].count1);
const setState = useContextSelector(context, v => v[1]);
const increment = () => setState(s => ({
...s,
count1: s.count1 + 1,
}));
return (
<div>
<span>Count1: {count1}</span>
<button type="button" onClick={increment}>+1</button>
</div>
);
};
contextを利用するコンポーネントでは、useContextの代わりにuseContextSelectorを使います。こうすることで、selectした結果が変わらない限り、context valueの更新の伝搬は止まります。
おわりに
実は、use-context-selectorの実装アイデアは、RFC119が提案されてから考えたわけではありません。react-trackedを実装する段階ですでに課題はあり、試行錯誤の上、実装していました。そこから、Proxyによるstate usage trackingを取り除いてシンプルにしたものが本ライブラリです。
Tearingの問題については、こちらのリポジトリにリンクやチェックツールがあるので、興味がある方はご覧ください。