Tags: React

React で lodash.throttle みたいなことをしたい

もう少し具体的に言うと、親コンポーネントから子コンポーネントに渡した関数を、子は自由に実行するのだが、実際にその関数が実行されるのは最大で 1 秒に 1 回にしたい。

lodash.throttle でさっと囲めばどうにかなるかと思ったがならなかったので、自力で頑張ることにした。

修正元

下記が修正元のソースコードである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { memo, useCallback, useEffect, useState } from "react";

export default function Parent() {
  const [text, setText] = useState("");
  const [count1, setCount1] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => setCount1((prev) => prev + 1), 500);
    return () => {
      clearInterval(interval);
    };
  }, []);

  const callback = useCallback(() => {
    setText(`${count1}`);
  }, [count1]);

  return (
    <section>
      <p>{count1}</p>
      <p>{text}</p>
      <ChildFunc func={callback} />
    </section>
  );
}

type ChildProps = {
  func: () => void;
};
const ChildFunc = memo<ChildProps>(function Child({ func }: ChildProps) {
  useEffect(() => {
    const interval1 = setInterval(() => {
      func();
    }, 100);
    return () => {
      clearInterval(interval1);
    };
  }, [func]);
  return null;
});

これを実行すると 14~16 行目で定義した callback は 500 ms に 1 回以上の頻度で呼ばれる。

私はこれを 2000 ms に 1 回の頻度に抑えたい。

修正後

下記が修正後である。変更したのは 14 行目、24 行目、44 行目以降。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { memo, useCallback, useEffect, useState } from "react";

export default function Parent() {
  const [text, setText] = useState("");
  const [count1, setCount1] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => setCount1((prev) => prev + 1), 500);
    return () => {
      clearInterval(interval);
    };
  }, []);

  const callback = useCallback(() => {
    setText(`${count1}`);
  }, [count1]);

  const { call } = useThrottle(callback, 2000);

  return (
    <section>
      <p>{count1}</p>
      <p>{text}</p>
      <ChildFunc func={call} />
    </section>
  );
}

type ChildProps = {
  func: () => void;
};
const ChildFunc = memo<ChildProps>(function Child({ func }: ChildProps) {
  useEffect(() => {
    const interval1 = setInterval(() => {
      func();
    }, 100);
    return () => {
      clearInterval(interval1);
    };
  }, [func]);
  return null;
});

function useThrottle(callback: () => void, wait: number) {
  const [exec, setExec] = useState(true);
  const [trigger, setTrigger] = useState(false);

  useEffect(() => {
    if (!exec) return;

    callback();
    setExec(false);
  }, [exec, callback]);

  useEffect(() => {
    if (!trigger) return;
    const timer = setTimeout(() => {
      setExec(true);
      setTrigger(false);
    }, wait);

    return () => clearTimeout(timer);
  }, [trigger, wait]);

  return {
    call: useCallback(() => setTrigger(true), []),
  };
}

仕組みとしては、子コンポーネントは実行したい関数を直接実行するのではなく、関数を実行したいタイミングで triggertrue に変更する。

triggertrue になると、2000 ms 後に exectrue となるタイマーが起動する。

そして exectrue になると初めて本当に実行したかった関数が実行される。

かなりまどろっこしい実装になるが、基本的にはレンダリングのたびに関数を再生成する React ではこのような方法になると思う。

以上