React Hooksまとめ

react-hooksはなぜ生まれ、どのようなメリ・デメがあるのかについて書きます。

Reactとは

Reactとは Facebookが開発をしているUIの作成に特化したオープンソースライブラリーである。 仮想DOMが特徴的で、さまざまなプラットフォームで最適化されたUIの描画を実現する。

Reactの歴史

Reactでコンポーネントを作成するとき、Reactリリース当初は React.createClass 関数を使用しコンポーネントを作成していた。(現在は非推奨)

var HelloComponent = React.createClass({ render: function() { return <p>Hello</p>; } });

しかし ES2015(ES6) が class構文 に対応したことにより、 class構文を使用したクラスコンポーネント時代が到来した。

class Component extends React.Component { render() { return ( <h1>Hello, {this.props.name}</h1> ); } }

その後、

  • コード量の削減
  • 可読性の向上
  • テストしやすさ
  • this を使用しなくても良い

という理由からクラスコンポーネントよりも関数コンポーネントを使用する流れが生まれた。 しかし関数コンポーネントではクラスコンポーネント同等の機能が再現できないため、 高階コンポーネントという手法が使われるようになった。

import React from 'react'; export default function enhance(WrappedComponent) { return class extends React.Component { constructor(props) { super(props); this.state = { open: false, }; } onClickOpen() { this.setState({ open: true }); } onClickClose() { this.setState({ open: false }); } render() { // 機能を追加した新しいコンポーネントを返す return ( <WrappedComponent {...this.props} onClickOpen={this.onClickOpen} onClickClose={this.onClickClose} /> ); } }; }
// 関数コンポーネント function TestComponent(props) { return <div>{props.title}</div> }; // 関数を使って機能を合成 const EnhancedComponent = enhance(BaseComponent); // クリックするとコンソールが出力されるdiv要素が表示される <EnhancedComponent />

上記の例では enhanse という高階コンポーネントでクラスコンポーネント固有のthis.setState を内包している onClickOpen, onClickClose 関数を作成し、引数で渡ってきたコンポーネントにpropsとして渡している。

しかし高階コンポーネントでも結局クラスコンポーネントを使用しており、また

  • props名が高階コンポーネントとラップされるコンポーネントで重複すると使えない

  • refを使った高階コンポーネントをさらにラップすると意図しないrefを取ってしまう

  • ラップコンポーネントからは、親から渡すpropsなのか高階コンポーネントからくるpropsなのか判断できない

  • ネストが増えるためデバッグツールの表示が見ずらい

  • 高階コンポーネントと良く一緒に使用される recompose というオープンソースの開発が止まっている(開発者がReactチームに参加した)

という問題点が介在していた。

そして、この流れのあとに開発されたのが今回対象となる Hooksである。

Hooks について

Hooks が React公式で開発され、関数コンポーネントにもクラスコンポーネント同等の機能を高階コンポーネントを使用せずに導入することができるようになった。

Hooks を使用することで今までクラスでしか再現できなかった機能(stateやライフサイクルなど)を実現できるようになり、またこれらの機能を外部に切り出せるため、ロジックがコンポーネントに張り付いてしまう問題を解決することができるようになった。

さらに state やコンポーネントのライフサイクル のみに特化した機能を外部にHooksとして切り出せる(カスタムフック)ため、 高階コンポーネントよりもコード量を少なく且つ細分化し機能を切り出すことができ、単なるクラスコンポーネント相当の機能を提供するだけではなく、Hooksはそれ以上の恩恵が得られるようになったと考えられる。

公式で取り上げられていた mediumの記事に良い例えが載っていた。

They’re not a way to share state — but a way to share stateful logic. We don’t want to break the top-down data flow!

意訳すると Hookは状態を共有する方法ではなく、状態管理を伴うロジックを共有するものだ。上位コンポーネントから下位コンポーネントへデータを渡す流れを壊したいわけではない。

まとめとして Hooks を使用することによって

  • クラスコンポーネントを使用せずにクラスコンポーネント同等機能の実現(高階コンポーネントなど複雑な処理はいらない)
  • コンポーネントのコード削減
  • テストしやすい
  • ネストが増えないためデバッグしやすい
  • ロジック再利用性のさらなる向上

というメリットがあり、これらから コード量の削減・可読性の向上・テストのしやすさという恩恵が得られる。

使用例

代表的なHooks(useState, useEffect, useContext)を使い、クラスコンポーネントを関数コンポーネントへ変換する使用例を示す。

useState, useEffectの使用例

初回は読み込み中という文言が表示され、1秒後にメールアドレスとユーザー名を入力するフォームが表示されるコンポーネントを考える。

スクリーンショット 2020-08-10 21.09.09

スクリーンショット 2020-08-10 20.25.05

これをクラスコンポーネントで書くと下記のようになる。

import React from "react"; export default class Form extends React.Component { constructor(props) { super(props); this.state = { email: null, name: null, loading: true, }; } componentDidMount() { setInterval(() => this.setState({ loading: false }), 1000); } onChangeEmail = event => { this.setState({ email: event.target.value }); }; onChangeName = event => { this.setState({ name: event.target.value }); }; render() { const { email, name, loading } = this.state; if (!loading) return <p>読込中</p>; return ( <div> <form> <div> <label>メールアドレス</label> <input onChange={this.onChangeEmail} type="email" /> </div> <div> <label>名前</label> <input onChange={this.onChangeName} type="text" /> </div> </form> <p> {email} : {name} </p> </div> ); } }

これを react-hook を使って関数コンポーネントで書き換えると下記のようになる

import React, { useState, useEffect } from "react"; export default function Form() { const [email, setEmail] = useState(null); const [name, setName] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { setInterval(() => setLoading(false), 1000); }, []); function onChangeEmail(event) { setEmail(event.target.value); } function onChangeName(event) { setName(event.target.value); } if (loading) return <p>読込中</p>; return ( <div> <form> <div> <label>メールアドレス</label> <input onChange={onChangeEmail} type="email" /> </div> <div> <label>名前</label> <input onChange={onChangeName} type="text" /> </div> </form> <p> {email} : {name} </p> </div> ); }

さらに input から event.target.value を受け取りセットしている箇所をcustom hook として下記のように切り出すことができる。

function useInput() { const [value, setValue] = useState(null); function onChange(event) { setValue(event.target.value); } return [value, onChange]; }

また1秒後に読み込みが完了するロジックも custom hookとして切り出す。(他コンポーネントでも使っていると仮定する)

function useLoading() { const [loading, setLoading] = useState(true); useEffect(() => { setInterval(() => setLoading(false), 1000); }, []); return loading; }

最終的には下記のようになる。

import React, { useState } from "react"; import useInput from "useInput"; import useLoading from "useLoading"; export default function Form() { const [email, onChangeEmail] = useInput(); const [name, onChangeName] = useInput(); const loading = useLoading(); if (loading) return <p>読込中</p>; return ( <div> <form> <div> <label>メールアドレス</label> <input onChange={onChangeEmail} type="email" /> </div> <div> <label>名前</label> <input onChange={onChangeName} type="text" /> </div> </form> <p> {email} : {name} </p> </div> ); }

コード量が減り、再利用性が向上したことがわかる。

useContextの使用例

Reactには Context という機能があり、これを使用することであるコンポーネントから、その下に位置するコンポーネントへ props で値をバケツリレーしなくても、値を参照することができる。

まず値を渡すためにまず独自スコープ(この場合はテーマの値を渡す)となるContextを作成する。

const ThemeContext = React.createContext('light');

その後、親に位置するコンポーネントに

<ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider>

と書くことで Provider 以下にあるコンポーネントでは ThemeContext の値を参照することができる。

次に子に位置するコンポーネントを作成する。

function Toolbar() { return ( <div> <ThemedButton /> </div> ); }

見ててわかるようにこのコンポーネントでは props を親から受け取っておらず、また子にも渡していない。

そして孫に位置するコンポーネントを下記のように書く。

class ThemedButton extends React.Component { static contextType = ThemeContext; render() { return <Button theme={this.context} />; } }

従来のContextで参照するためには2つの方法があり、一つはクラスコンポーネントの静的変数 contextType に指定する方法である。 これにより this.context から Contextの値を取得することができる。m

全体のコードを書くと下記のようになる。

const ThemeContext = React.createContext('light'); class App extends React.Component { render() { // Use a Provider to pass the current theme to the tree below. // Any component can read it, no matter how deep it is. // In this example, we're passing "dark" as the current value. return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } // A component in the middle doesn't have to // pass the theme down explicitly anymore. function Toolbar() { return ( <div> <ThemedButton /> </div> ); } class ThemedButton extends React.Component { // Assign a contextType to read the current theme context. // React will find the closest theme Provider above and use its value. // In this example, the current theme is "dark". static contextType = ThemeContext; render() { return <Button theme={this.context} />; } }

また、もう一つ Contextの値を取得する方法 は Consumer を使用することである。

function ThemedButton() { return <ThemeContext.Consumer>{(theme) => <Button theme={theme} />}</ThemeContext.Consumer>; }

そして今回紹介する useContext により、新たに Context から値を取得する方法が加わった。 useContext を使用すると下記にように書くことができる。

export default function ThemedButton() { const theme= useContext(ThemeContext); return <Button theme={theme} />; }

このように Hookを使用することで、クラスコンポーネントまたはコンポーネントでラップしなくても Context から値が取得することができる。 また他の Hook と同様にuseContext もカスタムフックとして外部へ切り出すことができる。

ただし useContext で使用するコンポーネントの上位には Provider が書かれている必要がある。

FAQ

公式サイトの FAQを元に疑問に感じられる点をピックアップしまとめる。

どのようなタイミングでカスタムフックを作るべきか?

React公式サイトには下記のように書かれている。

Now that function components can do more, it’s likely that the average function component in your codebase will become longer. This is normal — don’t feel like you have to immediately split it into Hooks. But we also encourage you to start spotting cases where a custom Hook could hide complex logic behind a simple interface, or help untangle a messy component.

意訳すると コンポーネントが肥大化することは普通であり、この事によりすぐに hookとして切り出す必要は無い。 しかし、custom Hookを使うことで複雑なロジックを簡潔なインターフェースで隠すことができ、複雑で絡み合ったコンポーネントを紐解くのに役立つ となる。

以上のことから、まずはcustom hookを使用せずに書いてみてロジックが複雑で分かりづらいと感じたときに、custom hookで簡素化できないかを考える流れで進めるのが良いと考える。それに加え各コンポーネントにて共通化できそうなロジックがあった場合に custom hookとして切り出すのが良いのではないかと考えた。

hookとクラスどちらを使えばよいか?

公式ホームページでは下記のようなことが書かれている。

クラスコンポーネントを全部書き換える必要があるのですか?

いいえ。React からクラスを削除する予定はありません — 我々はみなプロダクトを世に出し続ける必要があり、クラスを書き換えている余裕はありません。新しいコードでフックを試すことをお勧めします。

参照

React本体から 取り除かれた createClass とは異なり、クラスコンポーネントから削除される予定は無いらしい。

フック、クラスのいずれを使うべきですか、あるいはその両方でしょうか?

準備ができしだい、新しいコンポーネントでフックを試すことをお勧めします。チームの全員の準備が完了し、このドキュメントに馴染んでいることを確かめましょう。(例えばバグを直すなどの理由で)何にせよ書き換える予定の場合を除いては、既存のクラスをフックに書き換えることはお勧めしません。

長期的には、フックが React のコンポーネントを書く際の第一選択となることを期待しています。

参照

また公式サイトで取り上げられていた mediumの記事 には下記のようなことが書かれている。

I think that while there is definitely going to be a short-term cognitive cost to learning them, the end result will be the opposite.

意訳すると

Hookを学ぶことは短期的にはコストになるが、長期で見るとその逆の結果が得られる

つまり新しいコンポーネントでは学習コストがかかったとしても、Hookを使用することが長期的に良いのではないかと考える。

フックはクラスのユースケースのすべてをカバーしていますか?

我々の目標はできるだけ早急にフックがすべてのクラスのユースケースをカバーできるようにすることです。まだ使用頻度の低い getSnapshotBeforeUpdate、getDerivedStateFromError および componentDidCatch についてはフックでの同等物が存在していませんが、すぐに追加する予定です。

参照

フックはレンダープロップや高階コンポーネントを置き換えるものですか?

レンダープロップや高階コンポーネントは、ひとつの子だけをレンダーすることがよくあります。フックはこのようなユースケースを実現するより簡単な手段だと考えています。これらのパターンには引き続き利用すべき場面があります(例えば、バーチャルスクローラーコンポーネントは renderItem プロパティを持つでしょうし、コンテナコンポーネントは自分自身の DOM 構造を有しているでしょう)。とはいえ大抵の場合ではフックで十分であり、フックがツリーのネストを減らすのに役立つでしょう。

参照

以上のことから、

  • 新規で始める場合は関数コンポーネントでHookを使うことがおすすめ
  • 既存のクラスコンポーネントを書き換える必要は無い
  • Hookで再現できないライフサイクルを使いたい場合はクラスコンポーネントを使用(ただし今後 Hookで対応する可能性あり)
  • レンダープロップや高階コンポーネントは必要であれば使うが大抵 Hookで対応できる

と考えられる。

react-hooks API一覧

useState

const [state, setState] = useState(initialState);

state はコンポーネントが更新される対象となる値、 setStatestate を更新するための関数。

state の初期値は useState に渡される第一引数になる。

setStatestate の値を更新し、その際にコンポーネントも再レンダリングされる。

また、もし setState に渡される値が既存の state と同じなのであれば、コンポーネントの再レンダリングはスキップされる。

使用例

下記例は - ボタンを押すと count が1小さくなり、 + ボタンを押すと count が一つ大きくなる例である。

function Counter({initialCount}) { const [count, setCount] = useState(initialCount); return ( <> Count: {count} <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> </> ); }

注意

Component クラスで使用される setState は、既存のstateに新しいstateをマージするが、useState ではこの値が上書きされるため、もし Component クラス同様の処理を行いたい場合は下記のような処理を加える必要がある。

setState(prevState => { // Object.assign would also work return {...prevState, ...updatedValues}; });

useEffect

混乱を招くバグが発生する or UIに対して一貫性が持てなくなるため、Mutations 、購読、タイマー、ロギングや他副作用の発生する処理は関数コンポーネント内に書くべきではない。

その代わりに useEffect を使うべきだ。この関数は 画面が renderされた後に実行される。

純粋なReactの宣言的プログラミングから命令的プログラミングによってしまうことによってどのような影響があるかについて考えよう。

デフォルトではすべてレンダーが完了したときに useEffect が実行されるが、このタイミングを任意に変えることができる。

副作用のクリア

コンポーネントがページから離れる前に 購読やタイマーなどの処理をクリアする必要が度々ある。 これを実現するため useEffect は返り値として、クリーンアップ処理を受け取れるようになっている。

useEffect(() => { const subscription = props.source.subscribe(); return () => { // Clean up the subscription subscription.unsubscribe(); }; });

このような処理を書くことによってメモリーリークを防ぐことができる。

また上記に加えて、もしコンポーネントが複数回レンダーを行う時、 *次の副作用が実行される前に前回の副作用のクリーンアップ処理が実行される。 *

上記の例でいうと、もしレンダーが複数回行われるのであればその都度購読が新規に行われる。

もし何度も副作用を実行させたくないのであれば、次に説明する方法を参照して欲しい。

副作用のタイミング

componentDidMountcomponentDidUpdate とは違い、 useEffect に渡される関数はレイアウトやレンダー処理の後に実行される。

殆どの処理はブラウザーへのレンダリングをブロックするべきではないため、この挙動は副作用の共通的な動作として適している。

しかしすべての副作用を遅らせることはできない。

例えばユーザーに不自然なUIの動きを見せないためにDOM操作などの視覚的な処理は同期的に行わなくてはならない。

これらの種類の副作用のためにReactは useLayoutEffect という Hook を提供している。

この Hook はほとんど useEffect と同じであるが、渡される関数の実行されるタイミングが異なる。

useEffect に渡した関数は ブラウザーに描画されるまで実行が延期されるが、他の新規レンダーよりも前に実行されることが保証されている。 Reactは常に新しいレンダーが実行される前に、その前のレンダー処理後に実行される副作用がすべて実行される。

副作用の条件

useEffect(didUpdate);

デフォルトで副作用はレンダーが完了した後に呼ばれる。 しかしこの場合まえのセクションで説明した購読のケースのように一度呼び出されるものを想定していた場合、副作用が過多に呼び出されてしまう可能性がある。 更新ごとに購読処理を呼び出すのではなく、ある 値が変わったときのみに処理を呼び出すようにしたい。 これを実現するために useEffect の第2引数に配列を渡すことができ、この配列の値が変わったときに副作用の処理が呼び出される。

useEffect( () => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source], );

上記の例の場合 props.source が変わったときにのみ購読処理が走る。

useContext

const value = useContext(MyContext);

React.createContext で作成した context オブジェクトを引数として受け取り、その context オブジェクトの値を返す。

この時受け取る value は木構造で最も近くにある <MyContext.Provider> によって定義されたものになる。

最も近くにある <MyContext.Provider>value が更新されたときに、このフックが この新しい value と共に rerenderのイベントを発火させる。

たとえ、このHookを呼び出している先祖が React.memoshouldComponentUpdate を使っていたとしても、 useContext を使っているコンポーネントでは rerenderが発生する。

useContext の引数は context オブジェクト自身にしなくてはならない。

  • OK: useContext(MyContext)
  • NG: useContext(MyContext.Consumer)
  • NG: useContext(MyContext.Provider)

useContext を使用しているコンポーネントは、その Contextvalue が変わったときに必ず rerender される。

もしこの rerender処理の頻度が多いと思うのであればメモ化を使用して最適化することができる。

Tip もし Hook が登場する前に Context に馴染み深いのであれば、 useContext は classにおける static contextType = MyContext または <MyContext.Consumer> と同義である。

useContext(MyContext) はただ Context の中身を読み、変更を購読するだけである。 なので Contextvalue を供給するため、依然として <MyContext.Provider> は親コンポーネントに書く必要がある。

onst themes = { light: { foreground: "#000000", background: "#eeeeee" }, dark: { foreground: "#ffffff", background: "#222222" } }; const ThemeContext = React.createContext(themes.light); function App() { return ( <ThemeContext.Provider value={themes.dark}> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } function ThemedButton() { const theme = useContext(ThemeContext); return ( <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> ); }

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState の代替となるフックである。

reducerのタイプ (state, action) => newState を受け取り、 dispatch関数の対となる現在のstateを返す。もし Reduxを知っているのであれば、この処理についてすでにわかるだろう。

次の state に依存していたり、計算をするのに複雑な処理が絡んでくる場合 useState よりも useReducer を使用するのが好ましい。

useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.

コールバックの代わりに dispatch を渡すことができるので、子コンポーネントを更新するときにパフォーマンスを最適化するのに useReducer は役立つ。

下記は useState の章で説明した例を reducer で書き直したものである。

const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }

初期値の設定

useReducer では初期値を設定するのに2つの方法があり、開発状況によってどちらかを選ぶのが良い。

もっとも単純なやり方は、初期値を引数として渡す方法である。

const [state, dispatch] = useReducer( reducer, {count: initialCount} );

初期化の遅延

初期値は遅延して作成することもできる。

これを行うために、第3引数に初期化用の関数を渡す必要がある。

初期値は `init(initialArg) のように設定することができる。

これにより初期化にロジックが絡んでいる場合、初期化処理を外部へ切り離すことができる。

また状態をリセットするときもこの関数を使用できるので便利である。

function init(initialCount) { return {count: initialCount}; } function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; case 'reset': return init(action.payload); default: throw new Error(); } } function Counter({initialCount}) { const [state, dispatch] = useReducer(reducer, initialCount, init); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> );

dispatchから取り出す

If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)

現在の値と同様の値を reducerが返すときは子コンポーネントを再レンダリングや副作用が発生しない。(この時stateの比較にReactは Object.is を使用している。)

しかし Reactは依然として値を取り出す前に特定のコンポーネントを再レンダリングする必要があるかもしれない。

Reactは不必要に木構造を深く見ていかないため、このように不安になる必要はない。

もし計算量の多い処理がレンダリング時に実行されるのであれば、 useMemo を使用し最適化することができる。

useCallback

const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );

メモ化されたコールバックを返す。

インラインコールバックと変更を監視する配列を引数として渡す。

useCallback は変更を監視する配列のうち一つが変わったときに再計算されるメモ化されたコールバックを返す。

これは shouldComponentUpdate のように不必要なレンダリングを防ぐために参照値が等しいかを見ている子コンポーネントを最適化するときに有用である。

また useCallback(fn, deps)useMemo(() => fn, deps) と等しい。

コールバックに渡す引数と変更を監視する配列の値は等しくなると思うが、これを2度書くのは冗長だと考え将来的にはコンパイラがコールバックの引数を元に配列を作成するようになるらしい。

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

メモ化 された値を返す。

作成用の関数と変更をウォッチする配列を引数として渡す。

useMemo は変更を監視しているしている値の一つが変わったときにのみメモ化された値を再計算する。

この最適化はレンダーごとに計算量の多い処理をすることを防ぐのに役に立つ。

useMemo に渡した関数はレンダリング時に実行されることを思い出して欲しい。

つまりレンダリング時に実行されてほしくない処理は渡すべきではない。

例えば useEffect に渡すような副作用は useMemo にわたすべきではない。 もし配列が渡されていないのであれば、新しい値はレンダーごとに計算される。

意味的な保証(??) ではなく、パフォーマンスの最適化として useMemo を使用することができる。

将来的には React は上記で説明した幾つかのメモ化された値は敢えて処理を無くし、次のレンダー時に再計算できるようになるかもしれない (たとえばコンポーネントがスクリーンから外れたときにメモリーを回復するなど)

最初は useMemo を使わずにコードを書き、その後パフォーマンスの最適化が必要であれば useMemo を使用すべきである。

useRef

const refContainer = useRef(initialValue);

useRef は引数として渡される initialValue で初期化された .current をプロパティとして持つ ref オブジェクトを返します。

返されるオブジェクトはコンポーネントが破棄されない間生存します。

よくある用途は子コンポーネントに命令的にアクセスすることです。

function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // `current` points to the mounted text input element inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }

根本的に useRef は 変更可能な値をプロパティの .current に持つはこのようなものです。

従来の使い方でも、Reactは <div ref={myRef} /> のようにして ref オブジェクトを渡すと 、DOMノードが反抗されるたびに.current プロパティへ対応するノードを格納していました。

しかし useRef()ref 属性よりも有用だ。

useRef() はプレーンな JavaScriptオブジェクトを作成するので、クラス内のインスタンスフィールドを使うように、useRef() はミュータブルな値を管理しやすい。

useRef() と引数として {current: ...} と refオブジェクトを渡す唯一の違いは useRef はレンダーごとに常に同じrefオブジェクトを使う点である。

useRef は中身が変更されたとしても通知しないことを覚えておいて欲しい。

つまり .current プロパティを変更したとしても再レンダリングは発生しない。

もし Reactが ref にDOMを格納したり破棄したときにレンダリングさせたいのであれば、useRef の代わりにコールバック ref を使ったほうが良い。

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandlerefs を使用するときに親コンポーネントに向けられるインスタンスの値をカスタマイズすることができる。

殆どの場合で refs を使用した命令的なコードは避けるべきである。

また useImperativeHandleforwardRef と一緒に使用すべきである。

function FancyInput(props, ref) { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input ref={inputRef} ... />; } FancyInput = forwardRef(FancyInput);

この例の場合、 <FancyInput ref={inputRef} /> をレンダーする親コンポネントは inputRef.current.focus() を呼び出すことができる。

useLayoutEffect

ほとんど useEffect と同じであるが、 useLayoutEffect はすべての DOM が操作された後に同期的に発火されるという点で異なる。

useLayoutEffect は DOM のレイアウトを参照したり、同期的に再レンダーをするために使用すべきである。

useLayoutEffect の内部に設定された更新処理は、ブラウザーが描画をする前に同期的に実行される。

また基本的に 視覚的な更新をブロックしてしまうのを防ぎたいのであれば useEffect を使用することが好ましい。

TODO useLayoutEffect を入れるケースが有ったほうが良いかも

Tip

もしクラスコンポーネントからマイグレーションする場合、 useLayoutEffectcomponentDidMountcomponentDidUpdate と同じフェーズで発火することに気をつけるべきである。

しかし、初めは useEffect を使用し、もし問題があるのであれば useLayoutEffect を使うアプローチをおすすめする。

もしサーバーサイドレンダリングを使用しているのであれば、JavaScriptがダウンロードされるまで useLayoutEffectuseEffect の両方が発火されないということを覚えておくと良い。

なぜかというと、Reactは useLayoutEffect を含んでいるサーバーサイドレンダリングされるコンポーネントがある時警告をするからである。

これを修正したいのであれば、ロジックを useEffect へ移すか (もし初回レンダーで必要でないのであれば) 、クライアントがレンダーするまでコンポネントを表示させるのを遅らせるかである(もし useLayoutEffect が走るまでに HTMLが壊れているように見えてしまうのであれば)。

サーバーサイドレンダリングからの影響をレイアウトを必要とするコンポネントへ与えないために、showChild && <Child /> と書きレンダーをし、 useEffect(() => { setShowChild(true); }, []) で表示を遅らせる方法が考えられる。

このようにすればクライアント上で JavaScriptが実行される前に UIが崩れるのを防ぐことができる。

useDebugValue

useDebugValue(value)

useDebugValueReact DevTools にてカスタムフックを表示するために使用することができる。

例えば Building Your Own Hooks にて紹介されている useFriendStatus というカスタムフックで考えてみると下記のようになる。

function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); // ... // DevToolsにてこのフック名の隣にラベルを表示する // e.g. "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline'); return isOnline; }

Defer formatting debug values

値のフォーマットを揃えることはしばしば処理が増える場合があり、 またその処理はフックの検証を行わないのであれば不要である。

この理由により useDebugValue はフォーマット用関数を第2引数として任意に受け取れる。 この関数はフックが検証されるときのみに呼び出される。

このフックはデバッグ用の値をパラメーターとして受け取り、フォーマットされたディスプレイ用の値を返すはずだ。

例えば日付データを返すフックは、下記のように引数にフォーマット用の関数を渡すことで toDateString を不必要に呼び出すことを防ぐことができる。

useDebugValue(date, date => date.toDateString());

©Tsurutan. All Rights Reserved.