Reactの状態をスロットリングさせる

## はじめに
スロットリングとは、一定時間内に発生する特定の処理を制限するテクニックの1つです。

例としてユーザーが検索のためにテキストボックスへ入力するシーンを考えます。
テキストボックスへ入力した内容が変更されるたびに検索処理が走るような仕様の場合、大量の処理が押し寄せてくるのでパフォーマンスの低下につながります。
そこで、スロットリングを導入することで検索処理が一定の入力ごとに行われ、パフォーマンスの低下を防げます。

Reactでは、スロットリングは主にユーザーの入力やウィンドウのリサイズなど、高頻度で発生するイベントを制御するために使用されます。

## スロットリング無しの処理を見る
まずは、比較対象となるスロットリング無しのコンポーネントを作ります。
`useState`を用いて入力したテキストを準備し、テキストが更新されると下部に入力したテキストの文字数が出るようにしました。

https://codesandbox.io/embed/vs2r92?view=Editor+%2B+Preview&module=%2Fsrc%2FApp.tsx

今回は長さを求めるだけなのでパフォーマンスに影響はありませんが、外部との通信を同じタイミングで行った場合にトラフィックの増加が想像できます。
また、入力は短期間で行われることが予想されるので実装方法によっては結果の出力順が前後してしまいます。


## 状態の更新がスロットリングされるhooksを作る
テキストエリアの入力はそのまま行わせたい一方、特定の処理は一定の入力置きに行いたいので、テキストエリアの値を管理する状態と特定の処理を行うように監視する状態を分けることを考えます。
```ts
const [text, setText] = useState('');
const displayText = text;
```
`text`がテキストエリアを管理する状態で、`displayText`は`text`を渡しただけの値です。
これでは分けた意味がないので`alertText`の更新を1秒に1回のタイミングだけ行うようにします。
このようにする場合`displayText`は単純に`text`から生成できないので`useState`で状態を作るようにします。さらに、タイミングの調整に`setTimeout`を利用するので`useEffect`を用いて記述します。
```ts
const [text, setText] = useState('');
const [displayText, setDisplayText] = useState(text);

useEffect(() => {
  const timeoutID = setTimeout(() => {
    setDisplayText(value);
  }, 1000);

  return () => {
    clearTimeout(timeoutID);
  };
}, [text])
```
これで`text`が変更されてから１秒後に`displayText`を更新するようになりました。
`useEffect`の処理は初回描画後と`text`が変更されるたびに発火します。そして、中では変更された１秒後に`displayText`の値を`text`の値に変更させています。
しかし、1秒経つ前に`text`が再度変更されると、再描画が起きるので次の`useEffect`の処理が走る前に前回のクリーンアップ関数が走って1秒後の更新がキャンセルされます。これによって前回の更新から１秒経っていてもテキストエリアへの入力が続いた場合は更新されません。
さらに、最初の更新は即座に行って欲しいところですが、１秒待つ必要があるという問題もあります。
これを修正するために、最後に実行した時間を`ref`に覚えさせて処理を改めます。
```ts
const interval = 1000;

const [text, setText] = useState('');
const [displayText, setDisplayText] = useState(text);
const lastUpdated = useRef<number>();

useEffect(() => {
  const now = Date.now();

  if (!lastUpdated.current || now >= lastUpdated.current + interval) {
    lastUpdated.current = now;
    setDisplayText(text);
  } else {
    const timeoutId = setTimeout(() => {
      lastUpdated.current = now;
      setDisplayText(text);
    }, interval);

    return () => {
      clearTimeout(timeoutId);
    };
}, [text])
```
まだ一度も実行されていない場合、即ち`lastUpdated.current`が`undefined`の時と、最後に更新してからすでに1秒以上経っていた場合は更新するようにしました。

これをhooksで切り出すと以下のようになります。`interval`は今回だと1000で、遅延させる秒数を入力します。
```ts
const useThrottling = <T extends any>(value: T, interval: number) => {
  const [throttledValue, setThrottledValue] = useState<T>(value);
  const lastUpdated = useRef<number>();

  useEffect(() => {
    const now = Date.now();

    if (lastUpdated.current && now >= lastUpdated.current + interval) {
      lastUpdated.current = now;
      setThrottledValue(value);
    } else {
      const timeoutId = setTimeout(() => {
        lastUpdated.current = now;
        setThrottledValue(value);
      }, interval);

      return () => {
        clearTimeout(timeoutId);
      };
    }
  }, [value, interval]);

  return throttledValue;
};
```

## スロットリング有りの処理を見る
テキストが変更に合わせて内容が下部に入力した文字の長さが記述されていたところから、１秒置きに変更が反映されるようにします。

https://codesandbox.io/embed/vqz5rl?view=Editor+%2B+Preview&module=%2Fsrc%2FApp.tsx

このようにすることでテキストエリアの入力のたびに特定の処理が走らないのでパフォーマンスの低下や処理の結果が前後することを防げます。
インターバルの時間は程よい遅延時間を取り繕って設定する必要があるので慎重に検討しましょう(今回の例では時間を取りすぎて利用者のストレスになりそうです)。

## おわりに
Reactの状態をスロットリングさせる方法を紹介しました。状態の更新が過多で、余計な計算が発生した場合はこのようにして対応を考えましょう。