I have been using react development for 1 year, so I can’t say I am very proficient, but in the process of using it, I did understand and summarize some tips that can speed up our subsequent development.

1. some tips in useState

When we use react to develop function comppoent, we can’t help but see useState everywhere, so what are the pitfalls and things to watch out for in useState?

1.1 useState is asynchronous

Some developers who have been using react for a short time often write it like this: after calling the set method to set the value of the state, they immediately go and get the value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const [data, setData] = useState();

const func = () => {
  console.log(data);
};

useEffect(() => {
  setData({ name: '蚊子' });
  func();
}, []);

However, when using the log output, no change is found. This is because set in useState is an asynchronous operation, while the function func is called synchronously, which causes func() to be executed before set.

So how to solve this asynchronous problem? Here are a few ways to solve it.

1.1.1 Using the useEffect helper

1
2
3
4
5
6
useEffect(() => {
  if (data) {
    // 当满足条件时,执行func()
    func();
  }
}, [data]);

1.1.2 Passing the required data directly into the func method

1
2
3
4
5
useEffect(() => {
  const data = { name: '蚊子' };
  setData(data);
  func(data);
}, []);

1.1.3 Delayed triggering

A delayed trigger can also solve this problem, but this approach is least recommended. An asynchronous problem may create some other unknown problems if another asynchronous operation with a larger delay is used to underwrite it.

1
2
3
4
useEffect(() => {
  setData({ name: '蚊子' });
  setTimeout(func, 10);
}, []);

1.2 The state in setTimeout is always the initial value

We want to delay setting some data, for example to make a counter fixed simply by +1, we might write it like this:

1
2
3
4
5
6
7
8
const [count, setCount] = useState(0);

useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(timer); // 注意清除定时器
}, []);

However, when we run it, we find that the counter just goes from 0 to 1 and then never increases. When we log the callback function in setInterval, we find that the callback function is still executing, but the count is not changing.

This is due to the second parameter dependency of useEffect. In our example above, the dependency is an empty array, which means that after the first initialization, the count never changes internally, so the count will always depend on the initial value at the beginning.

But how can we achieve continuous state manipulation in the timer? Here are a few solutions for you.

1.2.1 Adding a state to a dependency

As we said earlier, because of the dependencies, each read is the initial value. Then we can add count to the dependency.

1
2
3
4
5
6
7
8
const [count, setCount] = useState(0);

useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(timer);
}, [count]);

This will allow the counter to keep increasing.

But this leads to another problem: every time the count changes, clearInterval is executed and then a new timer setInterval is restarted, and setInterval becomes setTimeout, which will be cleaned up after only one setInterval. So here it’s actually setTimeout instead.

1
2
3
4
5
6
useEffect(() => {
  const timer = setTimeout(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearTimeout(timer);
}, [count]);

There is also a simpler and more brutal way of not adding any dependencies, but this way, it causes all state changes to trigger a re-execution of useEffect.

1
2
3
4
5
6
useEffect(() => {
  const timer = setTimeout(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearTimeout(timer);
});

This approach, without any dependencies, is recommended for states with only one count in the component!

1.2.2 Using useRef to save the last state

We can use the useRef feature to save the value of the state that has just changed, so that the next time we use it, we can get it directly from useRef. useRef values are not constrained by dependencies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const [count, setCount] = useState(0);
const countRef = useRef(0); // 创建一个ref来保存count的值

useEffect(() => {
  countRef.current = count; // count发生变化时,将其存储起来
}, [count]);

useEffect(() => {
  const timer = setInterval(() => {
    setCount(countRef.current + 1); // 每次从countRef.current中获取到最新的值
  }, 1000);
  return () => clearInterval(timer);
}, []);

2. The beauty of useRef

We’ve just tried out useRef for a little while. Now let’s see how it’s used.

2.1 Saving all variable values

useRef can hold all variable values, such as the values in the state above, as well as the timer of the timer, the id of the requestAnimationFrame, and so on. For example, in recursive operations, timer and id are different every time, so when canceling, it’s a headache to decide which one to cancel, so we can use useRef to store them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const requestRef = useRef(0);

// 开始渲染
const render = () => {
  console.log('render', Date.now());

  requestRef.current = requestAnimationFrame(() => {
    render();
  });
};

// 取消渲染
const cancelRender = () => {
  cancelAnimationFrame(requestRef.current);
};

2.2 Saving multiple variables at the same time

In the above examples, we have saved only one value and then overwritten it with another new value. But what if we want to save multiple values at the same time?

If it’s just data, then just initialize the ref to an array and operate as an array.

1
2
3
4
5
6
7
8
const listRef = useRef([]);

useEffect(() => {
  const timer = setInterval(() => {
    listRef.current.push(Date.now());
  }, 1000);
  return () => clearInterval(timer);
}, []);

But we have a list of dom elements and we want to store all the dom elements?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const domListRef = useRef([]);

const getDomList = useCallback((dom: HTMLDivElement | null) => {
  if (dom) {
    domListRef.push(dom);
  }
}, []);

<>
  {
    list.map((item) => <div key={item.ename} ref={getDomList}></div>);
  }
</>

Pay special attention here, the getDomList method should be wrapped in useCallback, otherwise it will cause the domListRef data to increase every time the state changes!

2.3 Customizing the hook for useInterval

We learned about the performance of setTimeout(setInterval) and useRef in useEffect in Sections 1 and 2, and based on these two, we can implement a useTimeout or useInterval ourselves.

The useInterval implementation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const useInterval = (callback, delay) => {
  const saveCallback = useRef();

  useEffect(() => {
    // 每次callback发生变化时,都将最新的callback保存起来
    saveCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      saveCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay); // setInterval只运行ref.current里的方法
      return () => clearInterval(id);
    }
  }, [delay]);
};

When you perform a set operation on a state in useInterval, you don’t have to worry about whether the state will be updated or not.

1
2
3
4
5
const [count, setCount] = useState(0);

useInterval(() => {
  setCount(count + 1);
}, 1000);

3. The magic of createPortal

When we introduce a component and render it, it will be rendered according to the position it was introduced. However, for some public components, such as popups, toast, hover pendants, etc., we don’t want the style of these components to be affected by the style of the parent element (e.g. overflow: hidden, transform (if the parent element has transform style, position: fixed will be downgraded to position: absolute"), z-index, etc.), it is better if she can render to the root directory or other specified directory. What should we do at this point?

We’ll use createPortal, the rules of this method are very simple, just 2 parameters:

  1. the component to be rendered.
  2. the DOM node to be mounted.

The code explains the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { createPortal } from 'react';

const Modal = () => {
  return createPortal(
    <div className="i-modal">
      <div className="i-modal-content"></div>
    </div>,
    document.body,
  );
};
export default Modal;

This <Modal /> component can be referenced anywhere and then rendered to the end of document.body when rendered (similar to the appendChild operation).

1
2
3
4
5
6
7
8
9
import Modal from './modal';

const User = () => {
  return (
    <div className="user">
      <Modal />
    </div>
  );
};

4. Summary

In fact, there are a lot of tips and tricks in react, so if we are familiar with them, we can quickly build our applications with react.