大きく、末端コンポーネントと全体アーキテクチャの視点がある。
末端コンポーネントでの Hooks
ここはあまり議論の余地なく、setState で local state を持っているものや、 componentDidMount していたものを置き換えることが出来ると思う。
FC を class にせずにちょっとリッチにするのが簡単になる。
class の setState 相当
function Counter() { const [count, setCount] = useState(0); const onClick = useCallback(() => setCount(s => s + 1), []); return <button onClick={onClick}>{count}</button> }
componentDidMount / componentWillUnmount 相当
function KeyListenerer { useEffect(() => { const onClick = event => console.log(event.keyCode); window.addEventListener('keydown', onClick); return () => window.removeEventListener('keydown', onClick); }, []); return <span>...</span> }
[]
は返り値を memoize するためのキーで [] を与えると常に同じ参照を返す。
これは慣れてないとちょっと難しい。useEffect で []
を与えないと effect の実行は componentDidUpdate 相当になる。
一旦は memoize keys なしで書いて、再描画を抑制する必要があったら、ちょっと頭を捻ってkeysを与える感じになりそう。
(翻訳) React Hooks は魔法ではなく、ただの配列だ
React.memo と useCallback
useCallback の便利なところは、今まで class で this.onClickBound = this.onClick.bind(this)
のような書き方をしていた箇所が、pure(or memo) の shallow equal 比較で memoize された関数参照なので true になる。React.memo と組み合わせることで、子に関数を参照を渡す時に render を抑制することが簡単になる。
こういうケースで有効。
const Button = React.memo(props => { return <button onClick={props.onClick}>{props.value}</button> }); function App() { const onClick = useCallback(() => console.log('xxx'), []); return <Button onClick={onClick} value={"foo"}/> } function Root() { const [data, setData] = useState({}) useEffect(() => { const id = setInterval(() => { setData(v => v + 1) }); return () => clearInterval(id); }); return <App {...data}/> }
App の親から App の render が掛かっても、Button の shallow equal は true なので、 Button は value が変わらない限り更新されない。
アーキテクチャ上の React Hooks and Suspense
ここは無限に議論の余地がある。誰もベストプラクティスを持ってない。
一応、トレンドとしてはマイクロフロントエンドがある。 [翻訳記事]マイクロフロントエンド - マイクロサービスのフロントエンドへの応用
React コアチームはマイクロフロントエンド的なものを志向しているように見える。Component がそれ自体でどんどん賢くなる方向性。Component に処理を書かず、一箇所に集約する Redux の Single Source 的な方向性は、素朴に使うとマイクロフロントエンドと対立する。
すごく大雑把に言うと、 organisms の接続先が redux store になるか、何らかの API を経由した先になるか。という違いになるのではないか。あるいは Component 自体が自身の接続ロジックを密に知ることになる。
Organisms 相当は、こんな風になるだろうか。
const Child = React.lazy(() => import('./Child')) function MyOrg() { const [state, setState] = useState(null) useEffect(() => { const data = readFooData(); setState(data) }); return <Suspense fallback="loading..."><Child /></Suspense> }
Suspense のことを考えると、一緒に非同期を解決してしまうのは確かに便利ではある。しかし、アプリケーション全体を協調させるための State が一つ欲しい。rcombineReducers は不要かもしれない。そうなると、 redux は使わず、 useReducer
だけで済ませられる、かも。
[Fizz] New Server Rendering Infra by sebmarkbage · Pull Request #14144 · facebook/react
実験
手を動かして考えるためにこんなものを実装した。(npm に publish はしてない)
https://github.com/mizchi/redux-worker-context
react-redux の connect を useSelector(state => state.foo)
と書きたかっただけだが、それだけでは面白くないので、MainTheard との関係を希薄にするために WebWorker に store の実体を移してみた。MainThread は worker の store から 自分に関係ある snapshot を受け取り、その利用者は更に snapshot を select して自分自身にマッピングする。
シングルトンだとしても、connect の mapStateToState で参照を絞るのはいいアイデアだと思うので、とりあえずそのまま redux を使った。
RootState => ComponentSnapshot => LocalSnapshot
みたいなイメージ。これは Worker をサーバーに見立てて、クライアントだけでクライアント/サーバーモデルを擬似的に表現している。
これは、 middleware も redux のものになるので、既存資産はそのまま使える。WebWorker に処理が移ってるので CPU ヘヴィな操作がある程度許容される。
WorkerDOM みたいなものが実現可能になったら面白いことができるかもしれない、という期待もある。
Redux and Hooks
Redux は、まず消極的に、まず内部 API を変えるのに用いる、とのこと。useStore
みたいな API が生えるとしても、だいぶ後。
https://github.com/reduxjs/react-redux/issues/1063#issuecomment-436479804
自分でhooksで遊びたかったら、現状自分で書くしかない。