The body of the

I have been using React Hooks on projects since their official release. However, in using Hooks, I also fell into some pitfalls, resulting in bug-hiding and unmaintainable code. In this article, I will analyze these problems and summarize some good practices for your reference.

Question 1: Should I use a single state variable or multiple state variables?

The advent of useState allows us to use multiple state variables to hold state, such as:

const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);Copy the code

But we can also put all the states in an object, as we did in the Class component this.state, so that only one state variable is needed:

const [state, setState] = useState({
  width: 100,
  height: 100,
  left: 0,
  top: 0
});Copy the code

So the question is, should I use a single state variable or multiple state variables?

If a single state variable is used, the previous state needs to be merged each time the state is updated. Because the setState returned by useState replaces the original value. This is different from the this.setState of a Class component. This. setState automatically merges updated fields into the this.state object.

const handleMouseMove = (e) => {
  setState((prevState) => ({ ... prevState, left: e.pageX, top: e.pageY, })) };Copy the code

Using multiple state variables makes state more granular and easier to logically split and combine. For example, we can extract the associated logic into a custom Hook:

function usePosition() {
  const [left, setLeft] = useState(0);
  const [top, setTop] = useState(0); useEffect(() => { // ... } []);return [left, top, setLeft, setTop];
}Copy the code

We find that top is updated every time we update left. Therefore, splitting top and left into two state variables is a bit redundant.

Before using state, we need to consider the “granularity” of state splitting. If the granularity is too fine, the code becomes redundant. If the granularity is too coarser, the reusability of the code decreases. So, which states should be combined and which should be split? I summarize the following two points:

  1. Split completely unrelated states into groups of states. Such assizeposition.
  2. If some states are related or need to change together, they can be combined as a group of states. Such aslefttop.
function Box() {
  const [position, setPosition] = usePosition();
  const [size, setSize] = useState({width: 100, height: 100});
  // ...
}

function usePosition() {
  const [position, setPosition] = useState({left: 0, top: 0}); useEffect(() => { // ... } []);return [position, setPosition];
}Copy the code

Deps has too many dependencies that cause Hooks to be difficult to maintain?

Using useEffect Hook, we usually pass a second parameter “Dependency Array” (hereafter referred to as dependency array) to avoid executing its callback every time render is executed. Thus, the useEffect callback is executed only when the dependent array changes.

function Example({id, name}) {
  useEffect(() => {
    console.log(id, name);
  }, [id, name]);
}Copy the code

In the example above, the log is printed only if the ID or name changes. The dependency array must contain all the values involved in the React data flow that are used inside the callback, such as state, props, and their derivatives. If something is missing, it could cause a bug. This is the JS closure problem. If you don’t know the closure, you can Google it.

functionExample({id, name}) {useEffect(() => {console.log(id, name) cannot be printed when name changes; }, [id]); }Copy the code

In React, in addition to useEffect, other hooks that accept dependent arrays as parameters include useMemo, useCallback, and useImperativeHandle. As we mentioned earlier, never leave out the values of the dependencies inside the callback. However, if relying on arrays depends on too much, it can make your code difficult to maintain. I saw this code in the project:

const refresh = useCallback(() => {
  // ...
}, [name, searchState, address, status, personA, personB, progress, page, sizCopy the code

Not to mention internal logic, just looking at this pile of dependencies makes my head turn! If your project is littered with code like this, you can imagine how painful it is to maintain. How do you avoid writing code like this?

First, you need to rethink whether all these DEPs are really needed. Look at this example:

function Example({id}) {
  const requestParams = useRef({});
  requestParams.current = {page: 1, size: 20, id};

  const refresh = useCallback(() => {
    doRefresh(requestParams.current); } []); useEffect(() => { id && refresh(); }, [id, refresh]); // The deps list is valid. }Copy the code

Although the useEffect callback relies on the ID and refresh methods, a look at the Refresh method shows that it never changes after the first render is created. Therefore, it is redundant to use it as a deps for useEffect.

Second, if all of these dependencies are really needed, should the logic be in the same hook?

function Example({id, name, address, status, personA, personB, progress}) {
  const [page, setPage] = useState();
  const [size, setSize] = useState();

  const doSearch = useCallback(() => { // ... } []); constdoRefresh = useCallback(() => { // ... } []); useEffect(() => { id &&doSearch({name, address, status, personA, personB, progress});
    page && doRefresh({name, page, size});
  }, [id, name, address, status, personA, personB, progress, page, size]);
}Copy the code

It can be seen that there are two pieces of logic in useEffect, which are independent of each other. Therefore, we can put these two pieces of logic into different use effects:

useEffect(() => {
  id && doSearch({name, address, status, personA, personB, progress});
}, [id, name, address, status, personA, personB, progress]);

useEffect(() => {
  page && doRefresh({name, page, size});
}, [name,  page, size]);Copy the code

What if the logic can’t continue to split, but relying on arrays still depends on too much? Like our code above:

useEffect(() => {
  id && doSearch({name, address, status, personA, personB, progress});
}, [id, name, address, status, personA, personB, progress]);Copy the code

UseEffect in this code relies on seven values, which is too much. Looking closely at the code above, you can see that these values are all part of the “filter criteria” by which the data on the page can be filtered. Therefore, we can treat them as a whole, which is the merged state we talked about earlier:

const [filters, setFilters] = useState({
  name: "",
  address: "",
  status: "",
  personA: "",
  personB: "",
  progress: ""
});

useEffect(() => {
  id && doSearch(filters);
}, [id, filters]);Copy the code

If state cannot be combined and the setState method is used inside callback, consider using setState callback to reduce some dependencies. Such as:

const useValues = () => {
  const [values, setValues] = useState({
    data: {},
    count: 0
  });

  const [updateData] = useCallback(
      (nextData) => {
        setValues({data: nextData, count: values.count + 1 // Because callback internally depends on the external Values variable, it must be specified in the dependency array}); }, [values], );return [values, updateData];
};Copy the code

In the above code, we must specify values in the dependency array of the useCallback, otherwise we cannot get the latest values state in the callback. However, with the setState callback, we no longer have to rely on the external values variable, so we do not need to specify it in the dependency array. Like this:

const useValues = () => {
  const [values, setValues] = useState({});

  const [updateData] = useCallback((nextData) => {
    setValues((prevValues) => ({data: nextData, count: prevvalues. count + 1, // PasssetThe State callback gets the latest values State, and the callback no longer depends on the external values variable, so no values need to be specified in the dependent array})); } []); // This callback will never be recreatedreturn [values, updateData];
};Copy the code

Finally, you can store mutable variables through ref. Previously we only used ref as a tool to keep references to DOM nodes, but useRef Hook can do much more than that. We can use it to hold references to values and read and write to them. Here’s an example:

const useValues = () => {
  const [values, setValues] = useState({});
  const latestValues = useRef(values);
  latestValues.current = values;

  const [updateData] = useCallback((nextData) => {
    setValues({ data: nextData, count: latestValues.current.count + 1, }); } []);return [values, updateData];
};Copy the code

Be careful when using ref because it can be assigned at will, so be sure to control how you modify it. In particular, some low-level modules should not expose the REF directly when encapsulation, but provide some methods to modify it.

All of this comes down to cleaner, maintainable code. If we find too many dependencies on arrays, we need to revisit our code.

  • It is best not to have more than three dependent array dependencies, otherwise the code becomes difficult to maintain.
  • If we find too many dependent array dependencies, we should do something to reduce them.
  • Remove unnecessary dependencies.
  • Break up the hooks into smaller units, with each Hook dependent on its own dependency array.
  • Multiple dependent values are aggregated into one by merging the associated states.
  • throughsetStateThe callback function gets the latest state to reduce external dependencies.
  • throughrefTo read the value of a mutable variable, but you need to control how you modify it.

Question 3: Should I use useMemo?

Should I use useMemo or not? Some people have never thought about it, and some people don’t even think it’s a problem. In either case, “wrapping up” with useMemo or useCallback seems to insulate applications from performance problems. But is it really true? Sometimes useMemo does nothing and even affects application performance.

Why do you say that? First, we need to know that useMemo has its own overhead. UseMemo “remembers” some values and, in subsequent render, retrieves the values from the dependent array and compares them to the last recorded value. If they are not equal, the callback is reexecuted, otherwise it returns the “remembered” value. The process itself consumes memory and computing resources. Therefore, excessive use of useMemo may affect the performance of a program.

To use useMemo properly, we need to understand the scenarios for which useMemo is applicable:

  • Some calculations are expensive, so we need to “remember” the return value to avoid recalculating every render.
  • We also need to “remember” this value as the reference to it changes, causing downstream components to be re-rendered.

Let’s look at an example:

interface IExampleProps {
  page: number;
  type: string;
}

const Example = ({page, type}: IExampleProps) => {
  const resolvedValue = useMemo(() => {
    return getResolvedValue(page, type);
  }, [page, type]);

  return <ExpensiveComponent resolvedValue={resolvedValue}/>;
};Copy the code

In the above example, ExpensiveComponent is expensive to render. Therefore, when the reference to the resolvedValue changes, the author does not want to re-render the component. Therefore, the author uses useMemo to avoid changing its reference to the resolvedValue every time render recalculates, thus making the downstream component re-render.

This is a valid concern, but before using useMemo, we should consider two questions:

  1. Passed to theuseMemoHow expensive is the function of? In the example above, considergetResolvedValueIs the function expensive? Most methods in JS are optimized, for exampleArray.map,Array.forEachAnd so on. If the operation you are performing is not expensive, there is no need to remember the return value. Otherwise, useuseMemoThe cost itself may exceed the cost of recalculating the value. Therefore, for some simple JS operations, we do not need to use ituseMemoTo “remember” its return value.
  2. Does the reference to the “memory” value change when the input is the same? In the example above, whenpagetypeAt the same time,resolvedValueWill the reference to the That’s where we need to think about itresolvedValueType of. ifresolvedValueIs an object, and due to the use of “functional programming” on our project, a new reference is generated for each function call. However, ifresolvedValueIs a primitive value (string.boolean.null.undefined.number.symbol), there is no such thing as a “reference”, the calculated value must be the same every time. In other words,ExpensiveComponentComponents are not re-rendered.

Therefore, if the overhead of getResolvedValue is small and resolvedValue returns a raw value like a string, we can remove useMemo altogether, as follows:

interface IExampleProps {
  page: number;
  type: string;
}

const Example = ({page, type}: IExampleProps) => {
  const resolvedValue = getResolvedValue(page, type);
  return <ExpensiveComponent resolvedValue={resolvedValue}/>;
};Copy the code

Another myth is estimating the cost of creating a function. Some people feel that creating functions in Render can be too expensive, so to avoid creating functions more than once, use useMemo or useCallback. But for modern browsers, the cost of creating functions is minuscule. Therefore, there is no need to use useMemo or useCallback to save on this performance overhead. Of course, you can safely use useMemo or useCallback if you want to ensure that the references to the callback are equal every time you render.

Const onSubmit = () => {const onSubmit = useCallback() => {doSomething(); } []);return <form onSubmit={onSubmit}></form>;
};Copy the code

I read an article earlier (link at the end of the article) where it was suggested that it is better to use useRef instead of useMemo if you simply want to keep references to values unchanged during re-rendering. I don’t agree with that. Let’s look at an example:

/ / use useMemofunction Example() {
  const users = useMemo(() => [1, 2, 3], []);

  return<ExpensiveComponent Users ={users} />} // Use useReffunction Example() {
  const {current: users} = useRef([1, 2, 3]);

  return <ExpensiveComponent users={users} />
}Copy the code

In the example above, we use useMemo to “remember” the Users array, not because the array itself is expensive, but because the users reference changes every time we render, This results in a costly re-rendering of the child component ExpensiveComponent.

The authors argue that useMemo should not be used semantically, but useRef should be used instead, otherwise it consumes more memory and computing resources. Although useRef and useMemo are implemented slightly differently in React, useMemo and useRef have similar overhead when useMemo’s dependency array is an empty array. UseRef can even be implemented directly with useMemo, as follows:

const useRef = (v) => {
  return useMemo(() => ({current: v}), []);
};Copy the code

Therefore, I don’t see much of a problem with using useMemo to keep values referenced consistently.

When writing custom hooks, the return value must be consistent with the reference. Because you can’t be sure how the external is going to use its return value. Bugs can occur if the return value is used as a dependency on other hooks and the references are inconsistent every time re-render (when the value is equal). Such as:

function Example() {
  const data = useData();
  const [dataChanged, setDataChanged] = useState(false);

  useEffect(() => {
    setDataChanged((prevDataChanged) => ! prevDataChanged); // Call when data changessetThe State. If the data values are the same but the references are different, unexpected results can be produced. }, [data]); console.log(dataChanged);return<ExpensiveComponent data={data} />; } const useData = () => {// Get asynchronous data const resp = getAsyncData([]); // Process the asynchronous data obtained, using array.map. Therefore, even if the data is the same, each call will get a different reference. const mapper = (data) => data.map((item) => ({... item, selected:false}));

  return resp ? mapper(resp) : resp;
};Copy the code

In the example above, we get data using useData Hook. The value of data does not change each time render, but the references are inconsistent. If you use data in a useEffect dependent array, you can produce unexpected results. ExpensiveComponent component Re-render also causes performance problems due to different references.

Re-render child components with different references to prop because they have the same value is not necessarily a performance issue. Because Virtual DOM re-render ≠ DOM re-render. However, the Virtual DOM Diff overhead is also high when the child component is particularly large. Therefore, the child re-render should be avoided as much as possible.

Therefore, before using useMemo, we might ask ourselves a few questions:

  1. Is the function to remember expensive?
  2. Is the value returned original?
  3. Will the remembered value be used by other hooks or child components?

With these questions answered, it’s no longer difficult to decide whether you should use useMemo. However, in a real project, it is better to define a set of uniform specifications to facilitate collaboration among multiple teams. For example, the first question, how do you define the cost? Without clear standards, it can be very difficult to implement. Therefore, I have summarized the following rules:

  1. If the value returned is the original value:string.boolean.null.undefined.number.symbol(excluding dynamically declared Symbol), is not requireduseMemo.
  2. For object, array, function, etc. used in the component, if it is not used in the dependent array of other hooks, or causes sub-component re-render, it can not be useduseMemo.
  3. All objects, arrays, functions, etc. exposed in custom hooks should be useduseMemo. To ensure that references do not change when the values are the same.

Q4: Can Hooks replace higher-order components and Render Props?

Before Hooks came along, we had two ways to reuse component logic: Render Props[1] and higher-order components [2]. But both of these approaches can cause problems with JSX “nested regions.” Hooks make it easier to reuse component logic and solve the “nested territories” problem. Hooks are to React what async/await is to Promise.

Can Hooks replace higher-order components and Render Props? The official answer is that hooks provide a simpler way to Render higher-order components or Render Props that only Render a subcomponent. In my opinion though, Hooks are not a complete replacement for Render Props and higher-order components. Next, we’ll examine this question in detail.

High-level component HOC

A higher-order component is a function that takes a component as an argument and returns a new component.

functionEnhance (Comp) {// Add some additional functionalityreturn class extends Component {
    // ...
    render() {
      return<Comp />; }}; }Copy the code

Higher-order components use decorator mode, which allows us to enhance the functionality of the original component without destroying its original features. Such as:

const RedButton = withStyles({
  root: {
    background: "red",
  },
})(Button);Copy the code

In the above code, we want to preserve the logic of the Button component, but at the same time we want to use its original style. Therefore, we inject custom styles with the Higher-order component withStyles and generate a new component RedButton.

Render Props

The Render Props encapsulate reusable logic through the parent component and provide the data to the child components. It’s up to the child component to render the data once it gets it, so it’s very flexible. In higher-order components, the rendering result is determined by the parent component. Render Props does not generate new components and is more intuitive in the parent-child relationship.

<Parent> {(data) => {// Your father has already finished the battle for you and left you a pile of gold coins. How to spend it is up to youreturn <Child data={data} />;
  }}
</Parent>Copy the code

Render Props, as part of JSX, can easily use the React lifecycle, Props, and State to Render, with a very high degree of freedom in rendering. Also, unlike Hooks that need to follow some rules, you can use if/else, map, etc.

In most cases, high-order components and Render Props are interchangeable, that is, what can be done with high-order components can be done with Render Props. It’s just a matter of which is easier to use in different scenarios.

Changing the HOC example above to Render Props is a bit more “cumbersome” to use:

<RedButton>
  {(styles)=>(
    <Button styles={styles}/>
  )}
</RedButton>Copy the code

summary

Before Hooks, higher-order components and Render Props were essentially promoting reuse logic into the parent component. With Hooks, we extracted the reuse logic to the top of the component instead of pushing it to the parent component. This avoids the “nested territory” that HOC and Render Props bring. However, when you have a parent-child hierarchy like Context’s and
, you can only use Render Props or HOC.

Hooks, Render Props, and higher-order components all have their own use scenarios:

  • Hooks:
  • Replace most use cases for Class, exceptgetSnapshotBeforeUpdatecomponentDidCatchNot yet supported.
  • Extract reuse logic. Use Hooks for all scenarios except those with explicit parent-child relationships.
  • Render Props: Has higher degree of freedom on component rendering, allowing dynamic rendering based on data provided by the parent component. Suitable for situations where there is a clear father-son relationship.
  • Higher-order components: Suitable for injection and for generating a new reusable component. Good for writing plug-ins.

However, scenarios that can use Hooks should use Hooks first, before Render Props and HOC. Of course, Hooks, Render Props, and HOC are not opposites. We can write Render Props and HOC with Hooks, and we can use Render Props and Hooks in HOC.

What are some other good practices when using Hooks?

  1. If the Hook types are the same and the dependency arrays are the same, they should be combined into one Hook. Otherwise, there will be more overhead.
const dataA = useMemo(() => {
  return getDataA();
}, [A, B]);

const dataB = useMemo(() => {
  returngetDataB(); }, [A, B]); // Should be merged with const [dataA, dataB] = useMemo(() => {return [getDataA(), getDataB()]
}, [A, B]);Copy the code
  1. Referring to the design of native Hooks, the return value of custom Hooks can be of type Tuple for easy external renaming. However, if more than three values are returned, it is recommended to return one object.
export const useToggle = (defaultVisible: boolean = false) => {
  const [visible, setVisible] = useState(defaultVisible);
  const show = () => setVisible(true);
  const hide = () => setVisible(false);

  return[visible, show, hide] as [typeof visible, typeof show, typeof hide]; }; const [isOpen, open, close] = useToggle(); Const [visible, show, hide] = useToggle();Copy the code
  1. refDo not expose the value directly for external use, but provide a way to modify the value.
  2. In the use ofuseMemooruseCallbackMake sure that the returned function is created only once. That is, functions are not created twice based on changes in dependent arrays. Here’s an example:
export const useCount = () => {
  const [count, setCount] = useState(0);

  const [increase, decrease] = useMemo(() => {
    const increase = () => {
      setCount(count + 1);
    };

    const decrease = () => {
      setCount(count - 1);
    };
    return [increase, decrease];
  }, [count]);

  return [count, increase, decrease];
};Copy the code

In useCount Hook, a change in the count state causes the increase and Decrease functions in useMemo to be recreated. Because of the closure nature, if these two functions are used by other hooks, we should add them to the dependency array of the corresponding Hook as well, otherwise there will be a bug. Such as:

function Counter() { const [count, increase] = useCount(); useEffect(() => { const handleClick = () => { increase(); // Count is always 1}; document.body.addEventListener("click", handleClick);
    return () => {
      document.body.removeEventListener("click", handleClick); }; } []);return <h1>{count}</h1>;
}Copy the code

In useCount, increase is recreated as count changes. However, useEffect is not executed again after increase is recreated, so increase in useEffect is always the same as increase when it was first created. The value of count is 0 when it is first created, so it will always be 1 no matter how many times it is clicked.

Why not put the increase function in the useEffect dependency array? In fact, it can cause more problems:

  • increaseChanges to cause frequent binding of event listeners and disengagement of event listeners.
  • The requirement is to execute once only when the component is mounteduseEffect, butincreaseThe change will lead touseEffectMultiple executions cannot meet requirements.

How to solve these problems?

Make the function independent of external variables through setState callback. Such as:

export const useCount = () => {
  const [count, setCount] = useState(0);

  const [increase, decrease] = useMemo(() => {
    const increase = () => {
      setCount((latestCount) => latestCount + 1);
    };

    const decrease = () => {
      setCount((latestCount) => latestCount - 1);
    };
    return[increase, decrease]; } []); // Keep the dependency array empty so that the increase and Decrease methods are created only oncereturn [count, increase, decrease];
};Copy the code

2. Save variable variables by ref. Such as:

export const useCount = () => {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  countRef.current = count;

  const [increase, decrease] = useMemo(() => {
    const increase = () => {
      setCount(countRef.current + 1);
    };

    const decrease = () => {
      setCount(countRef.current - 1);
    };
    return[increase, decrease]; } []); // Keep the dependency array empty so that the increase and Decrease methods are created only oncereturn [count, increase, decrease];
};Copy the code

The last

We summarize some common problems in practice and propose some solutions. Finally, let’s review:

  1. Split completely unrelated states into groups of states.
  2. If some states are related or need to change together, they can be combined as a group of states.
  3. It is best not to have more than three dependent array dependencies, otherwise the code becomes difficult to maintain.
  4. If we find too many dependent array dependencies, we should do something to reduce them.
  5. Remove unnecessary dependencies.
  6. Break up the hooks into smaller units, with each Hook dependent on its own dependency array.
  7. Multiple dependent values are aggregated into one by merging the associated states.
  8. throughsetStateThe callback function gets the latest state to reduce external dependencies.
  9. throughrefTo read the value of a mutable variable, but you need to control how you modify it.
  10. To make sure it’s not abuseduseMemo, we define the following rules:
  11. If the value returned is the original value:string.boolean.null.undefined.number.symbol(excluding dynamically declared Symbol), is not requireduseMemo.
  12. For object, array, function, etc. used in the component, if it is not used in the dependent array of other hooks, or causes sub-component re-render, it can not be useduseMemo.
  13. All objects, arrays, functions, etc. exposed in custom hooks should be useduseMemo. To ensure that references do not change when the values are the same.
  14. Hooks, Render Props, and higher-order components all have their own use scenarios, depending on the situation.
  15. If the Hook types are the same and the dependency arrays are the same, they should be combined into one Hook.
  16. The return value of custom Hooks can be of type Tuple for easy external renaming. If too many values are returned, it is not recommended.
  17. refDo not expose the value directly for external use, but provide a way to modify the value.
  18. In the use ofuseMemooruseCallbackWhen, we can usereforsetStateCallback, ensuring that the returned function is created only once. That is, functions are not created twice based on changes in dependent arrays.