はじめに
以前は React コンポーネントはクラスで実装するのが常識だったが、シンプルな実装として SFC (Stateless Functional Component) という関数での実装の手法も存在した。
そして、 React Hooks が利用できるようになって、関数コンポーネントでも state を持つことができるようになり、 FC (Functinal Component) による実装が可能になった。
ここでは FC と React Hooks の組み合わせについて自分なりの解釈と説明をします。
これまでの SFC について
SFC は上位のコンポーネントから渡した props の値が変更された時に実行され、レンダリングされます。
props の値が変化しない場合は再レンダリングはされません。
そのため、状態は常に SFC の利用側でコントロールする必要がありました。
const ExampleSFC = (props: {
count: number,
// onIncrementClick のハンドラを外側で実装して、インクリメントした値を count props で渡す感じ
onIncrementClick: () => void
}) => {
return (
<div>
<div>count: {count}</div>
<button onClick={onIncrementClick}>+1</button>
</div>
);
}
FC について
React Hooks を使うことで、関数コンポーネントで State (状態) を持つことができるようになります。
ここでは Hooks のうち、 useState と useCallback を使います。
const ExampleFC = (props: {
initialCount: number = 0
}) => {
// コンポーネント内で count を持つ
const [count, setCount] = useState(initialCount);
// +1 がクリックされたら count をインクリメントするコールバックを定義する
const onIncrementClick = useCallback(() => {
setCount(count => count + 1);
}, []);
return (
<div>
<div>count: {count}</div>
<button onClick={onIncrementClick}>+1</button>
</div>
);
}
FC の実行とレンダリングのタイミング
SFC は props が変化したら実行 (=レンダリング) されるということを SFC の項目で説明したが、 FC は props に加えて state と、 consume している context の値が変化した際も実行される。
ここでいう state は useState によるもの、 context は useContext によるものを指す。
useState について
React Hooks で提供されている useState は、それを実行したコンポーネントでのみ持つ "状態" である。
const [value, setValue] = useState();
この場合、 setValue()
を実行して value
を更新した際に、その FC が実行される。
useContext について
React Hooks で提供されている useContext は、その親 (先祖) コンポーネントで作成されたコンテキスト (createContext()
) で提供されている値を監視&参照する事ができる。
ざっくり const value = useContext(context)
の様な形で context の値を consume することができる。
ここで consume している値が更新された際に FC が実行される。
useContext を扱うことは少ないかもしれないが、 react-redux で提供されている useSelector も同様の仕組みとなっている。 (と認識している)
useCallback について
上記のように、 props, state, context の値が更新されると FC が実行されるが、イベントハンドラを以下のように実装するとパフォーマンス的な問題が発生する。
const ExampleFC = (props: {
initialCount: number = 0
}) => {
// コンポーネント内で count を持つ
const [count, setCount] = useState(initialCount);
return (
<div>
<div>count: {count}</div>
<button onClick={() => setState(count => count + 1)}>+1</button>
</div>
);
}
ここでの問題は、 button の onClick に設定している () => setState(count => count + 1)
この関数の定義が FC が実行される度に再生成されるという問題である。
この ExampleFC では initialCount props と count state のみ参照しているため、実行される頻度は高くないため問題が顕在化しにくいが、多数の props, state, context を参照し、多数の callback を持つ FC になると問題が顕著になると思う。
前述した FC について
の項目で説明した Example は以下のように useCallback を使ってコールバックを生成している。
const onIncrementClick = useCallback(() => {
setCount(count => count + 1);
}, []);
useCallback を使うことによって、何度 FC が実行されてもハンドラ関数の定義は一度のみに限定することが可能となる。
これはメモ化という仕組みによるものだが、メモ化については割愛する。
useCallback の第2引数は依存する値 (dependencies) となっており、指定した値が更新された際にコールバックが再生成される。
コールバック内で使用可能な変数は、コールバックが生成された際の値となっているため、基本的にはコールバック内で使う変数を第2引数で指定する。
(ちょっと複雑だが、例で記載している setCount は第2引数で指定していない。 useState で生成した set 関数は常に一意となるため、第2引数で指定する必要はない。)
useEffect について
useEffect はコンポーネントのライフサイクルを管理する上で非常に重要になる Hooks 関数である。
クラスコンポーネントでいう componentDidMount, componentDidUpdate, componentDidUnmount などの副作用を実現するために useEffect を利用する。
useEffect は以下のように利用する。
const ExampleFC = (props: {
initialCount: number = 0
}) => {
// コンポーネント内で count を持つ
const [count, setCount] = useState(initialCount);
// +1 がクリックされたら count をインクリメントするコールバックを定義する
const onIncrementClick = useCallback(() => {
setCount(count => count + 1);
}, []);
useEffect(() => {
console.log(`count が更新されました。新しい値は ${count} です。`);
}, [count]);
return (
<div>
<div>count: {count}</div>
<button onClick={onIncrementClick}>+1</button>
</div>
);
}
前述したとおり、 FC は参照する props, state, context が変化した度に実行される。
例は count しか変化する変数がないため若干わかりにくい例かもしれないが、 count が更新される度に ExampleFC が実行され、上から下まで一連の実装が処理される。
useEffect も例外ではなく毎回通って実行されるが、メモ化という仕組みにより、第2引数で指定した依存変数が変化した場合にのみ第1引数で指定したコールバックが実行される。
なので、更新を感知して実行したい処理を第1引数に書き、どの変数が変化したら第1引数で指定した処理を実行したいのかを第2引数で指定する形となる。
最後に
拙い文章でアレだが、こんな主張である。