Open7
Reactの公式ドキュメントを読む
2023/3に正式リニューアルされたreactのドキュメントを改めて読む。
https://react.dev/learn のLEARN REACTの部分で気になったところをまとめていく。
Describing the UI
Your First Component
- 特になし
Importing and Exporting Components
- componentをdefault exportとnamed exportどっちにするかは、チームで統一する場合もあるが、特に公式でどちらかを推してるわけではない。
Writing Markup with JSX
- JSXの利点: ロジックにマークアップをsyncできる。マークアップ上のコンポーネントごとにロジックを分離できる
- HTMLをJSXに変換してくれる便利ツールがある。class -> classNameとかの属性名変換もやってくれる
JavaScript in JSX with Curly Braces
- 特になし
Passing Props to a Component
- propsをバケツリレーする時は、子コンポーネントに
<Child {...props} />
などとすると良い。ただし、これが頻発する場合は、子コンポーネントをchildrenとして引数にとるラッパー的なコンポーネントを作った方が筋が良い場合がある。
Conditional Rendering
- 何もrenderしたくない時はコンポーネントからnullを返せるが、そのコンポーネントを使う側から見た驚き最小化の観点から望ましくない場合もある。そういう時は、親コンポーネントから、そのコンポーネント自体をrenderするかどうか判定した方がいい
- conditionalな記述(if, 三項演算子, ...)がjsxのマークアップでめっちゃネストしてるような状況はよくない。markupもコードの一部なので、可読性を重視して別コンポーネントに切り出すなどすべき。
-
&&
でコンポーネントの出しわけをする場合、&&
の左側には数字を入れたら絶対ダメ。0
がrenderされてしまう。
Rendering Lists
- mapでリストをrenderする時は、keyの設計に気を付ける(リスト内の個々の項目のデータに紐づいたidなどにすることで、reactが項目の削除・移動・挿入などを明示的に知ることができ、正しくDOMを更新できる)。
- 特に、単にリストの中の項目の順序のインデックス(0, 1, 2, ...)を使うと、項目の削除・移動・挿入時に微妙なバグを作る可能性がある。
- keyを完全乱数にしてしまう場合もよくない。renderごとにすべてのコンポーネントが再生成されてしまい遅い。
- 通常の
<></>
表記のフラグメントにはkeyを渡せない。やりたければ<Fragment></Fragment>
を使う
Keeping Components Pure
- Reactは、すべてのコンポーネントが、純粋関数であると想定する。つまり、副作用がなく(=関数の実行以前に存在していた変数やオブジェクトを変えない)、参照透過性がある(=同じ入力なら同じ出力)
- 例えば、コンポーネントの中で何か外部の変数をインクリメントし(=副作用)、その変数を返り値のmarkupに使っている(=参照透過性の破綻)とする。このとき、「コンポーネントが何回呼ばれたか」によって、その外部変数の値は変わってしまうので、renderされたDOM出力が制御できない。
- Reactにおける入力とは、props, state, contextの3種類。
- Strict Mode(=root componentを
<StrictMode></StrictMode>
で囲っている状態)では、dev環境ですべてのコンポーネントを2重呼び出しする。これでバグるようなコンポーネントは純粋関数じゃない可能性が高い。 - Reactでは、通常副作用はイベントハンドラの中で行われる。コンポーネントは純粋関数だが、イベントハンドラは純粋関数じゃなくてよい(コンポーネントの実行時にはイベントハンドラは呼ばれないので)。
- 副作用起こしたい、でも適切なイベントハンドラがない、という場合にのみ
useEffect
を使う。でもこれは最終手段にすべき。 - なぜコンポーネントの純粋関数性をそこまで重視するのか?
- ブラウザだけでなくサーバでもrenderingが起こる可能性がある
- コンポーネントのメモ化が安全に行える。(純粋じゃなかったら、同じinputに対して違うoutputになる可能性があるのでメモ化できない)
- renderしてる途中でcomponent treeの途中でデータが変わったときに、renderを中断して新しいrenderを始められる。(純粋じゃなかったら、中断するとおかしなことになる可能性がある)
Adding Interactivity
Responding to Events
- イベントハンドラの命名のconvention: 関数自体は
handleXXX
という名前にする。それをpropとして受け渡す時はonXXX
という名前にする。 - Reactでは、
onScroll
を除いて、すべてのイベントが親要素にpropagate (bubbling)する。e.stopPropagation()
でpropagationを止める。 - capturingフェーズのイベントを使いたい時が稀に存在する。例えば、あらゆる子孫コンポーネントで発生したすべてのクリックイベントを取得したいが、クリックをstopPropagationしてる子孫コンポーネントが存在するとき。こういうときは、親コンポーネントで
onClickCapture
に処理を書いておくと、capturingフェーズですべてのクリックイベントに対して処理を実行できる。 -
preventDefault
はデフォルトの挙動(e.g., formタグのonSubmit)を抑制する。stopPropagation
とは無関係 - イベントハンドラは副作用OK!
State: A Component's Memory
-
useState
の役割- render間でデータを保持する
- setter関数によりstateを更新したときに、再renderをtriggerする
- stateの粒度: 関連するstateは、まとめてオブジェクトにして一つのstateにしても良い。例えば、フォームのフィールドをすべて1つのオブジェクトにまとめてstateにするなど。
-
useState
の内部の仕組み- Reactは、すべてのコンポーネントについて、そのコンポーネントの中のstate, setStateのペアをarrayで持っている。
- render時に、useStateが呼ばれると、そのarrayの中から一つずつペアを取り出して返す。arrayの中にペアがなかったら新しく作る。
- こういう仕組みになってるので、コンポーネントの中で動的にuseStateの数が変わったら困る!
- また、動的にuseStateの初期値を変えようとしても無理!
Render and Commit
- UIをserveするまでの3ステップ
- trigger: 初回描画時、もしくは自分や祖先のstateが変わったときにrenderをトリガ
- render: コンポーネントを呼び出す。初回renderでは、ReactはDOMノードをどう作るかを計算する。re-render時は、DOMノードがどう変わったかを計算する。ただ、実際のDOM操作はcommitフェーズで行う。
- commit: 初回renderでは、Reactは
appendChild
を使ってDOMノードを作る。re-render時は、必要最低限のDOM変更操作を行う(=DOM操作が要らなかったら何もしない)。 - (paining): DOM操作が終わったら、ブラウザがrepaintを行う。(ブラウザレンダリングとも呼ばれる)
State as a Snapshot
- renderingとは、Reactがcomponentを呼ぶこと。その時返されるJSXはその時々のsnapshot。
- メンタルモデル:
-
setXXX
は、あくまで次のrenderに向けたstate変更。 - 例えば、コンポーネントの中で
setNumber(number + 1)
を何回呼んでも、次のrenderではnumberは1増えるだけ。(numberの値自体は1回のrenderの中では変わらない) - また、
alert(number)
とかやったときの出力は、あくまで「そのalertが呼ばれたときのrender時のnumber」になる
-
Queueing a Series of State Updates
- batching: いくら
setXXX
をイベントハンドラ内で呼んでも、そのハンドラが実行し終わるまではre-renderをトリガしない。 - あまりないケースだが、次のrenderまでに同一のstateを複数回更新したい時は、
setNumber(n => n+1)
とすると良い。こうすると、reactはsetNumberに渡されたupdater functionをqueueに積んでいき、イベントハンドラの処理が終了したらそのqueueから順次更新を適用していく。 - updater functionじゃなくて普通に値を
setNumber
に渡した場合でも、queueには積まれていく。その場合は、「この値にnumberをセットしてください」という命令として扱われる。 - updater functionは、使わなくていい場合は使わない(当然)。
- updater functionの引数の名前は、
e
とかln
とか、1-2文字を使うのが一般的だが、もっと説明的でも別に良い
Updating Objects in State
- オブジェクトをuseStateで管理している場合は、絶対にuseStateの返り値のobjectのプロパティを直接mutateしてはいけない。それでうまくいくケースもあるが、多くの場合バグる。
- オブジェクトを変更したい場合は、ちゃんと
setXXX
を使い、かつそこで新しいオブジェクトを作って渡す。 - このとき、spread構文を使って、
setPerson({...person, name: 'hoge'})
みたいな感じにすると便利。ただし、spread構文は、浅いコピーしか行わないので注意 - オブジェクトがネストしていた場合は、下記のように書けば大丈夫。ただしちょっと長い
setPerson({
...person, // Copy other fields
artwork: { // but replace the artwork
...person.artwork, // with the same one
city: 'New Delhi' // but in New Delhi!
}
});
- ネストすると上記のように面倒なので、なるべくフラットにするのも一つの手。また、Immerを使うと、下記のようにプロパティmutateっぽい書き方でちゃんとオブジェクトのコピーをしてくれるので楽。
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
Updating Arrays in State
- object同様、
useState
で管理しているarrayもmutateしてはいけない - useStateの返り値のarrayに対して、下記のようなメソッドの利用を避けるべき。もしくはImmerを使う。
- adding: push, unshift (concatやspread構文を使う)
- removing: pop, shift, splice (filterやsliceを使う。spliceとsliceの違いに注意。spliceはarrayそのものを改変してしまう)
- replacing: splice, arr[i] = xxx (mapを使う)
- sorting: reverse, sort (arrayを先にコピーしてから、ソートする!)
- sortに関しては、下記のような書き方をすると良い
const nextList = [...list];
nextList.reverse();
setList(nextList);
- arrayの中のobjectを更新するとき
- この場合も、objectをmutateしてはいけない。新しいobjectを新しいarrayの中に作って返さなければならない
Managing State
Reacting to Input with State
- 不要なstateを除く
- 例えば、フォームで、
isTyping
とisSubmitting
が両方true、みたいな状態は起こり得ない。「そういう状態を表現できてしまう = 不要なstateが存在する」と思った方がいい。 - 他のstateから計算できる時はそれを使う
-
'typing' | 'submitting' | 'success'
のような型のstatus
という状態を作るのも有効。必要に応じて、そこからconst isTyping = status === 'typing'
とかやれば良い。 -
useReducer
を使うのもあり
- 例えば、フォームで、
Choosing the State Structure
- state設計の指針
- 関連するstateをまとめる
- もし常に2つのstateを同時に更新するなら、1つにまとめた方が良い。それにより、どちらかを更新し忘れることがなくなる。また、ユーザーが自由にfieldを追加できるフォームとかも、一つのstateにしておくと管理しやすい。
- 矛盾した状態が実現できないようにする
- 上述のisTypingとisSubmittingとか。
- 冗長な状態をなくす
- 他のstateから計算できるものはstateにしない。下記のようにpropsをstateに入れたりするのは、stateの初期化以外の目的で使ってはダメ。(初期化の目的で使うなら、prop名を
initialMessageColor
,defaultMessageColor
などとする)
- 他のstateから計算できるものはstateにしない。下記のようにpropsをstateに入れたりするのは、stateの初期化以外の目的で使ってはダメ。(初期化の目的で使うなら、prop名を
- 関連するstateをまとめる
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
- stateの重複をなくす
- 複数のstateが同じ情報を保持しないようにする。例えば、
items
という商品一覧と、selectedItem
という、ユーザーが選択した商品の2つのstateを保持する場合、item名などが変わった時に、2つのstateを両方更新しないといけない。この場合は、selectedId
をstateにすべき。
- 複数のstateが同じ情報を保持しないようにする。例えば、
- 深くネストしたstateをなくす
- 単純に更新処理がしづらく、扱いづらい。なるべくflatにすると良い。例えば、tree構造のstateは、parent/childの関係だけを各項目に持たせたflatなarrayにすると良い。
Sharing State Between Components
- lifting state up: コンポーネント間で同じstateをつかいたいときに、そのstateを共通の親に持ってきてバケツリレーすること
- controlledとuncontrolled: 子コンポーネントに、親からstate相当のものをpropとして渡すのがcontrolled。その逆がuncontrolled。controlledなコンポーネントは親から好きに制御できるため柔軟。uncontrolledなコンポーネントは、親側に制御ロジックを色々入れなくていいため楽。どちらを採用すべきかは場合による。
Preserving and Resetting State
- UI tree: DOMツリー、仮想DOMツリー、CSSOMツリーなどの総称。
- stateは、component(=関数コンポーネント)の中に生きているわけではなく、Reactの中に保持されており、UI tree内のcomponentと紐づけられている(=Reactは、componentの種類 + そのcomponentのUI tree上での位置 にstateを紐づけている)。
- componentがUI treeから一旦消える or 別のコンポーネントに置き換わると、紐づいたstateも消えてしまう。
- 逆に、UI tree上で同じ位置にcomponentがあれば、propがいくら変わっても、state情報は引き継がれる。
- 関数componentの中に関数componentを作れないのもこの仕組みに起因。(=内側の関数は、re-renderのたびに違うcomponentになってしまうので、stateが維持できない)
- 同じpositionに同じcomponentをrenderしたい、でもstateは分けたいときどうするか?
- 無理矢理違うpositionにする:
false
という値も一応仮想DOM内にpositionがある、とみなされるので、それを利用する -
key
をpropsに渡す: 違うkeyにすると、stateも別になる。なお、keyを変えると、元のコンポーネントのstateがリセットされてしまう。これを保持したい場合は、親コンポーネント側でstateを持つと良い。localStorageや、CSSのdisplay: none
などを利用することもできる。
- 無理矢理違うpositionにする:
Extracting State Logic into a Reducer
- state関連のロジックが増えてきた時に、そのロジックをreducerという単一の関数に移すことができる。
-
useState
->useReducer
への移行手順例- dispatchを書く
- 既存のstate更新ロジックから、
dispatch
関数を作る。 - たとえば、
dispatch({type: 'deleted', id: taskId});
とか。 - ここで、dispatchに渡す引数はなんでもいいが、慣習として、typeに「何が起こったか」を過去形で入れ、その他のフィールドに必要な情報をつめる。
- 既存のstate更新ロジックから、
- reducer関数を書く
- reducerは、現在のstateとaction(=dispatchの引数)を引数に取り、次のstateを返す関数。
action.type
で分岐したswitch-caseで書くことが多い。
- useReducerを使う
-
useReducer
の引数に、reduer関数と、stateの初期値を入れると、stateの値とdispatch関数が返ってくるのでそれを使う。
-
- dispatchを書く
-
useState
とuseReducer
の比較- コードサイズ: 大抵useStateの方が小さいが、ボイラープレート的なものが多いとuseReducerの方が良い場合も
- 可読性: シンプルな場合はuseStateの方がよく、複雑な場合はuseReducerの方が良い
- デバッグ: useReducerだと、stateの更新がreducer関数に集約されているので、そこでconsole.logするとデバッグしやすい
- テスト: reducer関数単体でテストできるのがとても良い
- 個人の好み: どっちもある
- reducerを書くコツ
- 純粋関数にする
- actionのtypeは、単一のユーザーインタラクションにする。
set_xxx
が何個もあるactionはよくない
- Immerを使うとobjectやarrayの更新をするreducerが書きやすい
Passing Data Deeply with Context
-
createContext
(コンポーネント外)、useContext
(子孫コンポーネント)、Provider
(親コンポーネント)の3つで成り立つ。 - contextを入れ子にすることもできる。例えば下記のようにすると、Sectionを入れ子にした時に、levelの値が一段ずつ上がっていく。
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
- 異なるcontextは、互いに完全に独立。
- contextを使いすぎてはいけない
- 数階層バケツリレーする = 即context とはならない。普通にpropsを渡すか、もしくは
children
propをうまく使えないかをまず考える。それでもダメならcontext使う。
- 数階層バケツリレーする = 即context とはならない。普通にpropsを渡すか、もしくは
- contextのユースケース: ダークモードなどのテーマ、現在のログインユーザー、ルーティング、その他複雑なstate
Scaling Up with Reducer and Context
- reducer単体では、dispatch関数が一つのコンポーネントでしか呼べないという欠点があった。
- そこで、stateとdispatchを保持するcontextを下記のように作ると、子孫コンポーネント全てでdispatchが呼べるし、stateの値も読めるようになる。
import { createContext, useReducer } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
Escape Hatches
Referencing Values with Refs
-
useRef
で作ったref.current
は、いくらmutateしてもre-renderをトリガしない。 - refはなんでも参照できる。number, string, object, 関数..
- ユースケース例
- イベントハンドラの中で作った
setInterval
を、別のイベントハンドラでclearしたい。 - このとき、interval IDをrefで持っておくことができる。interval IDはレンダリングとは無関係なので、refに格納しておくのが良い。
- その他ユースケースとして、
setTimeout
でも同様のことができるし、DOM操作にも使える。
- イベントハンドラの中で作った
- ただし、ほとんどのケースではstateで良い。refはあくまでエスケープハッチ
- ref.currentの値をrenderの途中(イベントハンドラなどの外)で読み書きすべきではない。refはrenderと同期してないため。
- refは、Reactの中では下記のようなイメージで実装されていると思えば良い。stateのオブジェクトのフィールドを書き換えてもre-renderされない、という性質を使っている。
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
Manipulating the DOM with Refs
-
<div ref={myRef}>
みたいな感じで、useRef
の返り値をDOM要素に渡すとその要素のrefが取得できる - その後、
ref.current
に対して、scrollIntoView()
など、色々なブラウザAPIを呼んだりできる - 動的な長さのarrayになっているDOM要素全てのrefを取得するにはどうするか
- ref callbackが使える: DOM要素のref propに関数を渡すと、その要素のnodeを引数とした処理が書ける。この中で、nodeを
ref.current
の中にMapやarrayなどで詰め込む作業をすれば良い
- ref callbackが使える: DOM要素のref propに関数を渡すと、その要素のnodeを引数とした処理が書ける。この中で、nodeを
- Reactコンポーネントにはref prop渡せない
- これはReactの思想による。refはescape hatchなので、他のコンポーネントのDOM要素はデフォルトでrefでいじれないようにして、codeがfragileになることを防ぐ意図がある。
-
forwardRef
を使うことで、子コンポーネントにrefを渡せるようになる - デザインシステムのlow-levelコンポーネント(e.g., ボタン)では、forwardRefを使ってrefを渡せるようにするのは一般的。一方、high-levelコンポーネントはforwardRef使わない方が事故を防げる
-
useImperativeHandle
を使うと、refを渡したDOM要素に対する操作の種類を制限できる - DOMの
ref.current
は、commitフェーズで確定する。renderフェーズではnullになっている可能性があるため、読まない方がいい。基本的に、refはイベントハンドラかuseEffect
の中で読んだりいじったりする - set stateした直後に
ref.current
をいじりたい、ただしstateが変更された後の状態でいじりたい、という場合には、set stateの部分をflushSync
で囲むとよい - refによるDOMの変更(e.g., remove)はリスキー。focus, scrollなどであればまだ安全。
Synchronizing with Effects
- component内の2つの主要ロジック
- レンダリング
- イベントハンドラ: 何らかのイベントに反応して副作用を伴う処理を行う
- effectは、「レンダリングそのものによって起こる副作用」を処理できる。
- effectは、commitフェーズの最後に処理される。
-
useEffect
の中で何かstateを参照し、さらにそのstateを更新するような処理を書くと、当然だが無限ループする。 - effectは、第二引数を指定しない場合、全てのrenderの後のcommitフェーズで実行される。
- refオブジェクトは第二引数に含める必要がない(含めてもいいが意味ない)。変化するのは
ref.current
であって、refオブジェクト自体は常に同じだから。 - 第二引数のdependencyの比較は、それぞれ
Object.is
で行われる - また、
setXXX
も含めなくてよい。なぜなら、一度setXXX
がuseState
から返されると、その後のrenderではつねに同じものが返されるから。 - cleanup
- effectが再実行されるたび・componentがunmountされるときに呼ばれる。
- developmentでeffectが2回呼ばれると困る場合: 大抵はcleanupを正しく実装すれば大丈夫。
- cleanupの実例
- 何かをsubscribe (e.g.,
window.addEventListener
)したら必ずcleanupでunsubscribeすること - effectでアニメーションをトリガする場合、cleanupでそれをリセットすること
- 何かをfetchする場合、cleanupでfetchした結果をignoreすること。仮にfetch結果を何かのstateに入れる処理をしていた場合、ignoreしないと、古いパラメータでfetchした結果がstateに(一時的にかもしれないが)入ってしまう。
- 何かをsubscribe (e.g.,
-
useEffect
でfetchするな。下記理由- SSRできない
- "network waterfall"を生みやすい: 親コンポーネントが子コンポーネントをrenderして、子の中のeffectでfetchして...とやってると、fetchが並列になりづらい
- キャッシュ・preload効かない
- ボイラープレート多い。cleanupとか含め
- effectの使用が適切でないもの
- アプリの起動時に一回だけ呼ばれるもの: コンポーネントの外に出すのが良い
- post系の処理
- effectが実際何をやっているか
- renderingのoutputとして、useEffectの中に書いた関数と、dependency arrayが返され、実行される
- renderするたびに、関数とdependencyがoutputされる。都度dependencyがチェックされ、同じなら何もしない
- dependencyが違ったら、前回実行されたeffectの返り値のcleanupが実行され、その後effectも実行される
- unmount時にcleanupが実行される
You Might Not Need an Effect
- effectいらない代表ケース2つ
- renderingのためのデータtransform: component内のtop levelでやれ
- ユーザー操作起点のイベントの処理: 普通にイベントハンドラ使え
- effectいらない具体例(気になったものだけ)
- propが変化した時にコンポーネント全体をリセットしたいとき: コンポーネントに渡すkeyを変化させるとリセットできる
- propが変化した時に、一部のstateだけリセットしたいとき: リセットしたときに
prevItems
をstateにのこしておいて、それが変化したらcomponentのトップレベルでリセット処理を行う。下記のような感じ。一見理解しづらいが、effectでselectionを更新するより、この方が一回レンダリングの回数が減る(effectでやると、一瞬、古いselection
が残ったちらつきが出てしまう)。ただし、一番良いのは、selectedIdをstateにしておいて、selectedId
とitems
からselection
を計算すること。
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// Better: Adjust the state while rendering
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}
- React外部のもの(third-party libraryやブラウザAPI)にイベントリスナを仕掛けてsubscribeし、何かイベントがあったらコンポーネントのstateを更新する、という系の処理:
useEffect
の中でwindow.addEventListener
などを使っても良いが、useSyncExternalStore
というReact提供のhookを使うとすっきり書ける - fetch: 前述の通り
- コンポーネントの中から
useEffect
が少なくなるほど、アプリのmaintainabilityが上がる。effectを使う際は、カスタムフックでラップして使うと、可読性も上がるし、後でeffectをなくそうと思ったときにやりやすくなることもある。
Lifecycle of Reactive Effects
- componentのライフサイクル: mount -> update -> unmount
- effectをcomponentの観点(mountされたら..., unmountされたら...)で見るな。そういう観点でみると、effectのdependencyのstateがcomponentのupdateのときに変化し、effectが実行される時によくわからなくなる。あくまで、「データの同期」(effect内に書く処理)と「それをとめる処理」(effectの返り値)に着目する。
- 一つのeffectに関係ない処理を混ぜるな(e.g., roomIdごとにchatのconnectionを繋ぎ直す処理と、ページ閲覧時にログを送る処理)。実行タイミングが混ざることによってバグることがある。ただし当然chains of computationみたいなのはNG
- 何がeffectのdependencyになり得るか?
- state, props: 当然なる
-
location.pathname
: なり得ない。pathnameは変わり得るが、reactがその変更を検知してre-renderできないから。effectの中に、location.pathname
を監視する処理を書くか、もしくはuseSyncExternalStore
を使うと良い -
ref.current
: 同様の理由でなり得ない。 - componentの外で定義された定数、
setXXX
: なり得ない。re-renderごとに変化しないことがわかっているから。
- effectで無限ループなどのトラブルがあった時に疑う場所
- 一つのeffectに処理を詰めすぎてないか
- 不要なeffectはないか (e.g., イベントハンドラにできるもの)
- dependencyにfunctionやobjectはないか。もしfunction/objectがrenderごとに再生成されてたら、当然無限ループする。(これらをdependencyにすることは極力避ける!)
- 一つのeffectに処理を詰めすぎてないか
- eslintがdependencyで怒ってきたとき、絶対にignoreするな!何かしらほかの解決策はある。
Separating Events from Effects
- イベントハンドラは「手動」、effectは「自動」でトリガされる
- props, state, その他component内で宣言される変数はreactive valueと呼ばれる。(逆にcomponent外で宣言されたらreactive valueじゃない)
- その観点で行くと、
- イベントハンドラ内ロジック: reactiveじゃない = reactive valueの変化によってトリガされない。
- effect内ロジック: reactive = dependencyに指定したreactive valueの変化によってトリガされる。
- 厄介なのは、effectで、「effect内で使ってる特定のreactive valueには反応してトリガされて欲しいけど、他のreactive valueには反応してほしくない」というパターン。
- 例えば、「ページURLの変化ごとにログを送りたい。ただし、ログには送信時点での商品数も含めたい」という場合。
- 下記のように書きたいが、lint errorが出る。だからと言ってlintをignoreするのは悪手。
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React Hook useEffect has a missing dependency: 'numberOfItems'
// ...
}
- そこで、
useEffectEvent
が使える。下記のようにすると、lintエラーなく所望の挙動が実現できる。
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
Removing Effect Dependencies
- effectのdependencyに不要なreactive valueが混じっていると、effectが過剰に走ってしまう。
- しかし、原則としてeffect内で使う全てのreactive valueはdependencyにしなければならない。linterをignoreするのは、高確率でバグを生む。
- dependencyを除きたいときは、下記をチェックすると良い
- イベントハンドラにすべき処理が混ざってないか?
- 一つのeffectの中に、異なるdependencyを持つ複数の処理が混ざってないか?
- effect内で、
setXXX
でstateを更新するとき、不要にstateの値そのものを読んでないか?例えば、setMessages([...msgs, newMsg])
をsetMessages(msgs => [...msgs, newMsg])
に置き換えられないか? -
useEffectEvent
が使えないか?特に、effect内の処理がpropsとしてcomponentに渡ってくるときとかにもuseEffectEvent
は有効 - dependencyにobjectやfunctionが入っていると、それらの予期せぬ再生成により、effectが走りすぎてしまうことがある。これを防ぐため、なるべくdependencyにこれらを使わない方が良い。できるなら、effectの中でobject/functionを作る、もしくはobjectをバラして、primitiveな値をdependencyに指定する方がバグが減る。
Reusing Logic with Custom Hooks
- カスタムhookで、ロジックの再利用を容易に
- 命名のconventionとして、必ず
use
から始めるように。(linterが強制してくれる) - 中で
useState
やuseEffect
などのReact hooksを使わない関数は、hookである必要はない。普通の関数にする。(ただし、将来的にその関数の中でReact hookを使うかも、みたいな時はhookにしていい。その方が、ifやforでその関数を使えないなどのルールを強制することができ、後々役にたつ。) - カスタムhookでは、ロジックは共有できるが、当然stateの値そのものは共有されないので注意
- また、当然カスタムhookは純粋関数である必要がある
- カスタムhookの引数にobject/functionを渡し、かつそれをeffectで使いたいときは、前セクションの注意事項がそのまま当てはまる。必要に応じて
useEffectEvent
使う。 - いつカスタムhookを使うべきか?
- 必ずしも全てのロジック重複をカスタムhookに切り出さなくても良い
- ただし、effectを使う時は、カスタムhookを積極的に使う。effectは「Reactの外に踏み出す」ものなので、処理の意図をちゃんとコード上で伝えられるようにしておいた方が良い。
- カスタムhookの命名は、ユースケースが明確にわかるように
- カスタムhookに切り出すメリットとして、後からhook内のロジックを変えたときに、いちいち全部のcomponentなおさなくていい、というのがある。
- 特に、effect系の処理はできれば撲滅した方がいいので、hookに切り出す意味が大きい。
- Reactのバージョンが上がって、effect撲滅系機能が出てきた時にもすぐに対応できる。(e.g.,
useEffect
をuseExternalSyncStore
に置き換える) - 将来的には、データfetchもReactが提供する
use
でできるようになるかも。
ついでに、パフォーマンス系のhookも読む
useMemo
- 注意点
- Strict Modeでは純粋関数かどうかの確認のため2回計算される。
- 基本的に、
useMemo
で作られたキャッシュをReactが破棄することはない - dependencyの比較は、他のこれ系hookど同様
Object.is
で行われる -
useMemo
は、パフォーマンス最適化の文脈によってのみ使われるべき。もしuseMemo
しないことでアプリが動かなくなるなら問題があると考えた方が良い。
- 基本的に、重い計算のキャッシュに使う。
- 全てを
useMemo
にすべきか?- ほとんどの場合いらないが、例えばお絵描きアプリなど、インタラクションの粒度が細かいアプリだと役にたつかも。
- 使い所としては下記にほぼ限定される。
- 対象の計算が非常に重く、かつdependencyの変化頻度が低い場合
-
memo
でメモ化しているコンポーネントのpropとして、useMemo
の計算結果を渡す場合(コンポーネントのre-renderを避ける意味で重要) -
useMemo
の計算結果が、他のhook (useEffect
やuseMemo
)のdependencyとして使われる場合
-
useMemo
を使うのは害ではないので、思考停止で全部useMemo
にしてもいいが、コードの可読性が落ちるのと、dependencyの管理をちゃんと意識して書かないと、結局memo化が壊れたりする。
- 多くのパフォーマンス問題は、
useMemo
のようなメモ化を行わなくても、下記に気をつければ治ることも多い- ラッパー的なコンポーネントに関しては、
children
propを使って、不要な子コンポーネントのre-renderを抑える - できるだけstateをcomponent treeの末端に寄せて、re-renderの範囲を狭める
- component内のロジックはpureにする
- 不要なeffectを避ける。effectまみれだと、stateの更新が連鎖上に重なり、不要なrenderが生じることがある
- effectから不要なdependencyを除く
- ラッパー的なコンポーネントに関しては、
useCallback
-
useMemo
の関数版。 -
useMemo
の「使い所」のうち、2, 3だけがuseCallback
の使い所となる。 - カスタムhookの返り値に含める関数は、すべて
useCallback
でラップすることが推奨される。これにより、hookの利用側が、必要に応じてコンポーネントのmemo化などのパフォーマンス最適化を行うことができる。 - 注意点として、下記のように
useCallback
に不要なdependecyが加わってしまうことがよくある。
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
こういうのはupdater functionを使って書き直すことで、dependencyを減らせる。
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []);