🌊

React Docs BETAを読む 【Synchronizing with Effects】

2022/07/23に公開

今回はRefのEffectsを使った同期についての章です。
外部システムとの連携やログなどレンダリングに合わせてなにかの処理を使用したい場合にEffectを使ってみようという話ですね。

What are Effects and how are they different from events?

EffectとEventの違いについてです。
Effectについて学ぶ前にいかの2つが前提知識となるようです。

  • レンダリングコード
  • イベントハンドラ

それぞれこれまでの章で紹介されている内容なので、ここの章を読む前に他の記事を一読しておいたほうがよいです。
今回の同期というのは、チャットサーバーへの接続のような、純粋ではなく副作用を含んでいる処理で、特定のトリガーが存在しない場合に有効です。

Effects let you specify side effects that are caused by rendering itself, rather than by a particular event.

重要なのはEffectには特定のイベントではなくレンダリングそのものが原因の副作用を指定するという部分ですかね。

You might not need an Effect

Effectの追加で気をつけるべきは、Refと同様にあくまでReactから一歩外に出て使われるという部分ですね。
何かしらの状態に紐づいていたり特定のイベントがある場合はEffectである必要性はないわけですね。

How to write an Effect

Effectの作成は以下の3つのStepから行われます。

  1. Effectの宣言
  2. Effectの依存関係の指定
  3. 必要に応じてクリーンアップを追加する

Step 1: Declare an Effect

Effectの宣言は以下の通りです。

function MyComponent() {
    useEffect(() => {
    // ここに記述した処理はレンダリング毎に実行されます。
    });
    return <div />;
}

宣言した関数を遅延させレンダリング後に実行します。
Docsではビデオの再生をコントロールする例でEffectの使いかたが説明されています。
Refなどを用いた判定をトップレベルに書くとエラーとなります。これは実行時に参照するDOMが未存在であったり、レンダリング中にRefへの参照を行っていたりと問題があるからですね。
この判定をEffectでラップすることでレンダリング後に実施されるため問題が解消されるということです。
他にもJqueryなどを使用した非Reactコードも同様の方法でラップできます。

Step 2: Specify the Effect dependencies

デフォルトはレンダリングごとの実行ですが、大体の場合は頻度が多すぎますね。
そのためEffectには依存関係を配列で渡すことが一般的です。

 useEffect(() => {
    // ...
  }, []);

この依存関係を渡すことで、依存関係が変更された場合のみEffectが実行されるようになります。
依存関係に複数の値を渡した場合は、すべてが同一の場合スキップされます。
refや設定関数など安定した値はEffect内で使用していても依存関係へ追加する必要はありません。

Step 3: Add cleanup if needed

依存関係に空の指定を行うと、初回レンダリングのみ実行されます。
上記を利用して初回レンダリング時にコネクションを作成などすることがある場合があります。
その場合、アンマウント時にコネクションを破棄しないとセッションが複数存在してしまいます。
Effectにクリーンアップを追加することで、アンマウント時の挙動を指定できます。

 useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, []);

How to handle the Effect firing twice in development?

開発中はStrict Modeをオフにしない限りEffectは2回発火します。
2回発火して困る場合は、修正方針としては発火回数の抑制ではなく「アンマウント後にも正しく機能するEffectを実装する」という方向で考えます。
通常はクリーンアップ関数の実装で解決するはずです。
作成するEffectはおおよそ以下のパターンに区別されます。

Controlling non-React widgets

非React要素の制御ですね。
プラグインなどを使用する場合に必要なやつですね。
非Reactコードに対してReact内の状態を同期したい場合など使用します。

Subscribing to events

EventListenerなどでイベントを監視している場合も同様ですね。
イベントリスナーの解除をクリーンナップ関数で実行する必要があります。

Triggering animations

アニメーションのトリガーですね。
クリーンアップ関数でアニメーションを初期化するなどします。

Fetching data

Fetchする場合はクリーンアップ関数でFetchの中止を行うか、結果の無視を行います。

Sending analytics

ページ訪問時などに分析した結果を送信する場合にも利用します。
これは2回マウントされた場合に問題が発生する場合でも解消しないほうがベターなようです。
開発中の余分なレンダリングは問題にならないはずですし、テストする場合はStrictモードの解除などを行います。

Not an Effect: Initializing the application

アプリケーション起動時に必要なコードなどはコンポーネント外に記述します。

if (typeof window !== 'undefined') { // Check if we're running in the browser.
    checkAuthToken();
    loadDataFromLocalStorage();
}

function App() {
    // ...
}

Not an Effect: Buying a product

Effectに商品購入のロジックを入れ込んだ場合、2回実行されるのを避けることができません。
この場合はそもそもEffectに渡さず、適切なハンドラで実行するというのがよいですね。

これらの例のように再マウントでの動作不良はバグの発見に役立ちます。
ユーザーが戻るボタンを押したときやリンククリックした場合など、再マウントされた時に正常な動作を行うためにチェックするべきですね。

Putting it all together

この章は改めてクリーンナップ関数についての説明でした。
クリーンアップを実行しなかった場合、Effectはキャプチャされ分離した状態で実行されるようです。
クリーンアップを適切に実装することで、Effectを適切な回数で実行することができます。

まとめ

Effectの使い方についてでした。
Effectはなんとなく使いがちで肥大化したり、無駄な処理が入りがちなので注意したいですね。
意外と一番身近なバグの温床なんじゃないでしょうか。
refと同じく非常手段だというのは今回の例で非常に納得がいきまいした。
リファクタが捗りますね。

GitHubで編集を提案

Discussion