← All React exercises

React implementation · 02

UseTimeout()

StatusNot taken

Problem

Create a hook to easily use setTimeout(callback, delay). reset the timer if delay changes DO NOT reset the timer if only callback changes

Solution

Review after your attempt
solution.tsx
1Let's recall a bit on setTimeout(), below is the basic usage.
2
3const id = setTimeout(callback, delay);
4window.clearTimeout(id);
5
6We can see it is a side effect from that requires cleanup, so we can put it inside useEffect().
7
8function useTimeout(callback: () => void, delay: number) {
9  useEffect(() => {
10    const id = setTimeout(callback, delay);
11    return () => clearTimeout(id);
12  }, [callback, delay]);
13}
14
15By default we need to put all dependencies in the deps array to make sure callback and delay are up to date, and this results in timer been reset when any of them changes.
16
17But we are requireed to only reset the timer when delay changes, this means callback has to be stabalized. In React, we can stablize a variable by useRef().
18
19function useTimeout(callback: () => void, delay: number) {
20  const callbackRef = useRef(callback);
21  useEffect(() => {
22    const id = setTimeout(() => callbackRef.current(), delay);
23    return () => clearTimeout(id);
24  }, [delay]);
25}
26
27🚨 A common mistake here is to use setTimeout(callbackRef.current), if done so a timer is set with the value of callbackRef.current, which is actually the first callback value and it won't get updated. To make it dynamically retrieved, we wrap it inside a function.
28
29To update the ref value as early as possible, we can put it inside useLayoutEffect().
30
31function useTimeout(callback: () => void, delay: number) {
32  const callbackRef = useRef(callback);
33  useLayoutEffect(() => {
34    callbackRef.current = callback;
35  }, [callback]);
36  useEffect(() => {
37    const id = setTimeout(() => callbackRef.current(), delay);
38    return () => clearTimeout(id);
39  }, [delay]);
40}
41
42🚨 Some may think about just updating the ref value directly in rendering, l like below.
43
44function useTimeout(callback: () => void, delay: number) {
45  const callbackRef = useRef(callback);
46  callbackRef.current = callback;
47  ...
48}
49
50It passes on BFE but it is not recommended - generally it is not safe to update ref value during rendering, which is stated clearly on React.dev:
51
52    Do not write or read ref.current during rendering, except for initialization. This makes your component’s behavior unpredictable.
53
54Here is how we can roughly understand why. In concurrent mode, React might render parts of our app, pause and do something more important, and then come back to render the rest of our app. This means that if we update ref.current during rendering, it could result in different parts of app using different values and thus inconsistency occurs.
55
56Well for our simple case here, it might not be a big issue.