React Hooks は Stateless Functional Component でも setState 的な状態操作や componentDidMount のような操作を可能にするための仕様提案です。
既に開発ブランチに入っていますが、 現時点で公式に採用されたものではないです。リリース時にはAPIが変わる可能性があります。
React のメイン開発者の一人である sebmarkbage の出してる RFC https://github.com/reactjs/rfcs/pull/68
試してみる
react@16.7.0-alpha.0 に既に実装されており、公式のブログでも解説が出ています。
自分は以下のように動作確認をしました。
yarn add react@16.7.0-alpha.0 react-dom@16.7.0-alpha.0 -D
import React from "react"; import ReactDOM from "react-dom"; const useState: <T>( t: T ) => [T, (prev: T | ((t: T) => T)) => void] = (React as any).useState; const useEffect: (f: () => void) => void = (React as any).useEffect; function Example() { const [count, setCount] = useState(0); useEffect(() => { const tid = setInterval(() => { setCount(s => s + 1); }, 1000); return () => { clearInterval(tid); }; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ); } ReactDOM.render(<Example />, document.querySelector("#root"));
TypeScript で書いていたので、無理矢理、勉強がてら型をつけていて、その定義がこう。
const useState: <T>( t: T ) => [T, (prev: T | ((t: T) => T)) => void] = (React as any).useState; const useEffect: (f: () => void) => void = (React as any).useEffect;
setState
単純な更新部分から見ていきましょう。
function Example() { const [count, setCount] = useState(0); // ... return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ); }
ボタンをクリックするとカウントが一つ増えます。
挙動を見る限り、setCount
が実行されると、この SFC が再実行されています。
内部的な話ですが、今までだと HoC で関数をラップして状態管理を外出しし、その setState としてラップされた SFC を再実行していたのが、HoC の助けを足りずに状態を持てるようになっています。これはライブラリ本体に手を入れないと実現できない挙動です。おそらく、実行コンテキストで登録されたリスナーとインスタンスの関係を結びつけているんでしょう。
uesEffect
useEffect は render とは関係ない副作用を記述するための機能です。
宣言的に開始処理、終了処理が書けます。
例1
setInterval 用のタイマーを登録する例
function Example() { const [count, setCount] = useState(0); useEffect(() => { console.log("start timer"); const tid = setInterval(() => { setCount(s => s + 1); }, 5000); return () => { console.log("stop timer"); clearInterval(tid); }; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ); }
このコードでは、5秒に1回、値をインクリメントします。
これで気をつけるべきは setCount を呼ぶことで、useEffect の終了処理が呼ばれ、再実行されることでタイマー登録処理が再登録されます。
なので、この Click Me をクリックすると、setCount が呼ばれ、タイマーがリセットされます。なんでもいいから最後に更新されてから5秒後にインクリメント、が再定義されているわけです。
ここまで書いて気付いたんですが、一回実行されるたびに setiInterval は必ず、 clearInterval され、かつ再登録されるので、setTimeout でも全く同じ挙動になりますね。
useEffect(() => { const tid = setTimeout(() => { setCount(s => s + 1); }, 2000); return () => { clearTimeout(tid); }; });
この、「最後に実行されてからの effect だけ意識する」というのは、ちゃんと使う限りは副作用を起こすコードで意識すべきスコープを狭く出来て、とても良さそう。
例2
カウンタが奇数のときだけ useEffect を持つ Foo を表示してみます。
function Foo() { useEffect(() => { console.log("foo start"); return () => { console.log("foo end"); }; }); return <div>foo</div>; } function Example() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> {count % 2 === 1 && <Foo />} </div> ); }
ボタンを何度かクリックしてみたときの Console
foo start foo end foo start foo end foo start foo end
再更新だけではなく、外からアンマウントされる際のデストラクタでもある、という感じですね。 componentWillUnmount のように使うことができるという感じ。
useReducer
setState の reducer 版
型はこんな感じ
const useReducer: <T, A>( reducer: (s: T, a: A) => T, t: T ) => [T, (action: A) => void] = (React as any).useReducer;
コード
type State = { count: number; }; type Action = | { type: "reset"; } | { type: "increment"; } | { type: "decrement"; }; const initialState: State = { count: 0 }; function reducer(state: State, action: Action): State { switch (action.type) { case "reset": return initialState; case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: "reset" })}>Reset</button> <button onClick={() => dispatch({ type: "increment" })}>+</button> <button onClick={() => dispatch({ type: "decrement" })}>-</button> </> ); } ReactDOM.render(<Counter />, document.querySelector("#root"));
第三引数を渡すと、それを元に初期化する。
const [state, dispatch] = useReducer( reducer, initialState, {type: 'reset', payload: initialCount}, );
これは reducer の function reducer(state = initialState, action) {...}
の initialState の部分を外出しできるって感じっぽいですね。
全体的に、initialState を受け付けるが、 state 自体は外部にメモ化されていて状態を持ってる、って感じですかね。個人的には常に initialState を受け付けてるように見えて、あまり直感的ではないような気も…。
他のヘルパ
- useMemo
- useCallback
- useRef
- useImperativeMethods
- useMutationEffect
あとでちゃんと勉強する
感想
ライブラリでは表現できない、React 本体だから実装できる感じの API だと感じます。 class extensds React.Component の API あんまりつかってほしくなくて、SFC で全部が表現できるようにしようってのを感じますね。React コアチームは HOC と redux 嫌いそう。
記述の自由度、奔放さが上がる代償に、行儀が悪いコードも沢山かけてしまうよなーという印象もあります。 メモリリークなく useEffect を正しく使うには RAII ちゃんとやるみたいなのを徹底しないといけないので。
useEffect は宣言的な React から抜け道を用意する感があって、ちょっと怖いですね。
Issue 見る限りは、 hot reload どうすんの?みたいな質問とかがあって、確かに既存のは動かなさそう。