As the author of the React Hooks library Ahooks, I should be a very, very experienced React Hooks user. After more than two years of using React Hooks, I have increasingly found that many people (including myself) are wrong about the use of React Hooks, and that the official documentation is, in the end, lax and misleading.

1. Not all dependencies need to be in the dependency array

React-hooks install eslint-plugin-react-hooks plugin to remind yourself that you forgot some of the variables.

The above consensus comes from the official document:

This rule, which I would call the root of all evil, is highlighted and valued by all new recruits, including myself. However, in actual development, this is not found to be the case.

If the props. Count and count change, all data will be reported.

This example is relatively simple, first paste the source code:

function Demo(props) {
  const [count, setCount] = useState(0);
  const [text, setText] = useState(' ');
  const [a, setA] = useState(' ');

  useEffect(() = > {
    monitor(props.count, count, text, a);
  }, [props.count, count]);

  return (
    <div>
      <button
        onClick={()= > setCount(c => c + 1)}
      >
        click
      </button>
      <input value={text} onChange={e= > setText(e.target.value)} />
      <input value={a} onChange={e= > setA(e.target.value)} />
    </div>)}Copy the code

UseEffect does not comply with React’s official recommendations, text and a variables are not placed in dependency arrays, ESLint warns:

What if, according to the specification, we put all the dependencies in the second array parameter?

  useEffect(() = > {
    monitor(props.count, count, text, a);
  }, [props.count, count, text, a]);
Copy the code

The above code conforms to the official React specification, but does not meet our business requirements. When text and A change, function execution is triggered.

At this point, the dilemma is that when the useEffect usage specification is met, the business requirements cannot be met. UseEffect is not canonical when business requirements are met.

Here are my suggestions:

  1. Don’t useeslint-plugin-react-hooksPlug-in, or you can optionally ignore the warning of the plug-in.
  2. There is only one case where you need to put a variable into a DEPS array, and that is when the useEffect function is triggered to execute when the variable changes. Not because useEffect uses this variable!

2. The DEPS parameter does not alleviate the closure problem

If I were to write my code exactly as suggested in the second recommendation, many people would worry about creating unnecessary closure problems. My conclusion is that the closure problem has little to do with the DEps parameter of useEffect.

For example, I have a requirement that outputs the current count three seconds after entering the page. The code is as follows:

function Demo() {
  const [count, setCount] = useState(0);

  useEffect(() = > {
    const timer = setTimeout(() = > {
      console.log(count)
    }, 3000);
    return () = > {
      clearTimeout(timer); }}, [])return (
    <button
      onClick={()= > setCount(c => c + 1)}
    >
      click
    </button>)}Copy the code

The above code, to achieve the initialization of 3s, output count. Unfortunately, there is definitely a closure problem here, even if we click the button multiple times after coming in, the output count is still 0.

So what if we put count in dePS?

  useEffect(() = > {
    const timer = setTimeout(() = > {
      console.log(count)
    }, 3000);
    return () = > {
      clearTimeout(timer);
    }
  }, [count])
Copy the code

As shown above, there is no closure problem at this point, but each time the count changes, the timer unloads and restarts, not meeting our initial requirements.

The only way to solve this problem is:

const [count, setCount] = useState(0);

// use ref to remember the latest count
const countRef = useRef(count);
countRef.current = count;

useEffect(() = > {
  const timer = setTimeout(() = > {
    console.log(countRef.current)
  }, 3000);
  return () = > {
    clearTimeout(timer); }}, [])Copy the code

The code above is very convoluted, but really, this is the only solution. Remember this code, it’s really powerful.

const countRef = useRef(count);
countRef.current = count;
Copy the code

As you can see from the example above, closure problems cannot be avoided by simply following the React rule. We need to be clear about the situations in which closure problems occur.

2.1 Normally there is no closure problem

const [a, setA] = useState(0);
const [b, setB] = useState(0);

const c = a + b;

useEffect(() = >{
	console.log(a, b, c)
}, [a]);

useEffect(() = >{
	console.log(a, b, c)
}, [b]);

useEffect(() = >{
	console.log(a, b, c)
}, [c]);
Copy the code

In normal use, there is no closure problem. In the code above, there is no closure problem at all, regardless of how dePS is written.

2.2 Closure issues arise with delayed invocation

In the case of delayed invocation, there are bound to be closure issues. What is delayed call?

  1. Use setTimeout, setInterval, promise.then, etc
  2. UseEffect uninstall function
const getUsername = () = > {
  return new Promise((resolve, reject) = > {
    setTimeout(() = > {
      resolve('John');
    }, 3000); })}function Demo() {
  const [count, setCount] = useState(0);

  // setTimeout causes closure problems
  useEffect(() = > {
    const timer = setTimeout(() = > {
      console.log(count);
    }, 3000);
    return () = > {
      clearTimeout(timer); }}, [])// setInterval causes closure problems
  useEffect(() = > {
    const timer = setInterval(() = > {
      console.log(count);
    }, 3000);
    return () = > {
      clearInterval(timer); }}, [])// promise. then causes closure problems
  useEffect(() = > {
    getUsername().then(() = > {
      console.log(count);
    });
  }, [])

  UseEffect Uninstalling functions causes closure problems
  useEffect(() = > {
    return () = > {
      console.log(count); }} []);return (
    <button
      onClick={()= > setCount(c => c + 1)}
    >
      click
    </button>)}Copy the code

In the example code above, closure problems occur in all four cases, always printing 0. The root of all four cases is the same, so let’s look at the order in which the code is executed:

  1. The component is initializedcount = 0
  2. UseEffect is executed, at which point the useEffect function executes, and the JS reference chain records the values ofcount=0Reference relation of
  3. Click button, count changes, but there’s nothing you can do about the previous reference

As you can see, closure problems occur in the context of delayed invocation. The solutions are as follows:

const [count, setCount] = useState(0);

// use ref to remember the latest count
const countRef = useRef(count);
countRef.current = count;

useEffect(() = > {
  const timer = setTimeout(() = > {
    console.log(countRef.current)
  }, 3000);
  return () = > {
    clearTimeout(timer); }} [])...Copy the code

Use useRef to ensure that countref.current is up to date at all times to address closure issues.

Here, I would like to reiterate my recommendation for useEffect:

  1. Only variables that need to be re-executed using effect when they change should be placed in DEPS. Instead of useEffect variables are placed in dePS.
  2. In the case of delayed invocation scenarios, the closure problem can be solved with ref.

3. Try not to use useCallback

I recommend not using useCallback in your projects as much as possible. In most cases, it will not improve performance, but make the code less readable.

3.1 useCallback Performance is not improved in most scenarios

UseCallback can remember functions and avoid repeated generation of functions, so that functions can be passed to child components without repeated rendering, improving performance.

const someFunc = useCallback(() = >{ doSomething(); } []);return <ExpensiveComponent func={someFunc} />
Copy the code

Based on the above knowledge, many students (including myself) add useCallback whenever it is a function when writing code, is that you? I used to be.

However, we should note that there is another requirement to improve performance. The child component must use shouldComponentUpdate or React. Memo to repeat rendering ignoring the same parameters.

If the ExpensiveComponent component is just a generic component, it does not have any use. Like this:

const ExpensiveComponent = ({ func }) = > {
  return (
    <div onClick={func}>
    	hello
    </div>)}Copy the code

ExpensiveComponent must be covered with react. Memo to avoid repeated rendering with unchanged parameters and improve performance.

const ExpensiveComponent = React.memo(({ func }) = > {
  return (
    <div onClick={func}>
    	hello
    </div>)})Copy the code

So, useCallback is going toshouldComponentUpdate/React.memoIt comes with it. Are you using it right? Of course, I recommend that you leave performance tuning out of general projects, that is, don’t use useCallback, unless you have a very complex component that can be used alone.

3.2 useCallback makes code less readable

I’ve seen some code that looks something like this with useCallback:

const someFuncA = useCallback((d, g, x, y) = > {
   doSomething(a, b, c, d, g, x, y);
}, [a, b, c]);

const someFuncB = useCallback(() = > {
   someFuncA(d, g, x, y);
}, [someFuncA, d, g, x, y]);

useEffect(() = >{
  someFuncB();
}, [someFuncB]);
Copy the code

In the above code, variables are passed layer by layer, and it can be a headache to determine which variable changes trigger useEffect execution.

I wish I didn’t use useCallback and just wrote the function naked:

const someFuncA = (d, g, x, y) = > {
   doSomething(a, b, c, d, g, x, y);
};

const someFuncB = () = > {
   someFuncA(d, g, x, y);
};

useEffect(() = >{ someFuncB(); }, [...]. );Copy the code

In scenarios where useEffect is invoked late, closure problems can be caused, which can be solved by our universal method:

const someFuncA = (d, g, x, y)=> {
   doSomething(a, b, c, d, g, x, y);
};

const someFuncB = ()=> {
   someFuncA(d, g, x, y);
};

+ const someFuncBRef = useRef(someFuncB);
+ someFuncBRef.current = someFuncB;

useEffect(()=>{
+ setTimeout(()=>{
+ someFuncBRef.current();
+}, 1000)}, [...]. );Copy the code

One word of advice for useCallback: Don’t use useCallback if you have nothing to do.

4. UseMemo recommends appropriate use

Compared to useCallback, the benefits of useMemo are obvious.

// No useMemo is used
const memoizedValue = computeExpensiveValue(a, b);

/ / use useMemo
const memoizedValue = useMemo(() = > computeExpensiveValue(a, b), [a, b]);
Copy the code

ComputeExpensiveValue is executed at every render if useMemo is not used. If useMemo is used, a computeExpensiveValue is executed only if A and B change.

You can probably do this, so I suggest that useMemo can be used appropriately.

Of course, it is not excessive use, and useMemo may not be cost-effective for very simple base type calculations.

const a = 1;
const b = 2;

const c = useMemo(() = > a + b, [a, b]);
Copy the code

In the example above, what is the cost of calculating a+b? Or record a/ B and compare whether a/ B changes are costly?

Obviously a plus B consumes less.

const a = 1;
const b = 2;

const c = a + b;
Copy the code

This account can be calculated by yourself, I recommend simple basic type calculation, do not use useMemo ~

5. Use estate correctly

UseState is the simplest of which are Hooks, but there are a number of tricks that can be used to double the maintainability of the code if the following are strictly followed.

5.1 It is not necessary to declare states separately if it can be calculated with other states

A state must not be computed directly from other states /props, otherwise state is not defined.

const SomeComponent = (props) = > {
  
  const [source, setSource] = useState([
      {type: 'done'.value: 1},
      {type: 'doing'.value: 2},])const [doneSource, setDoneSource] = useState([])
  const [doingSource, setDoingSource] = useState([])

  useEffect(() = > {
    setDoingSource(source.filter(item= > item.type === 'doing'))
    setDoneSource(source.filter(item= > item.type === 'done'))
  }, [source])
  
  return (
    <div>.</div>)}Copy the code

In the example above, the variables doneSource and doingSource can be calculated using the source variable, so don’t define doneSource and doingSource!

const SomeComponent = (props) = > {
  
  const [source, setSource] = useState([
      {type: 'done'.value: 1},
      {type: 'doing'.value: 2},])const doneSource = useMemo(() = > source.filter(item= > item.type === 'done'), [source]);
  const doingSource = useMemo(() = > source.filter(item= > item.type === 'doing'), [source]);
  
  return (
    <div>.</div>)}Copy the code

In general, such problems are obscure in projects and pass through layers, which is hard to see at a glance in Code Review. If the variables can be clearly defined, that’s half the battle.

5.2 Ensure unique data source

Ensure that the same data is stored in only one place within a project.

Do not exist in redux and define a state store in the component.

Do not have a state store defined in the current component that exists in the parent component.

Do not have a URL Query and define a state store in the component.

function SearchBox({ data }) {
  const [searchKey, setSearchKey] = useState(getQuery('key'));
  
  const handleSearchChange = e= > {
    const key = e.target.value;
    setSearchKey(key);
    history.push(`/movie-list? key=${key}`);
  }
  
  return (
      <input
        value={searchKey}
        placeholder="Search..."
        onChange={handleSearchChange}
      />
  );
}
Copy the code

In the above example, the searchKey is stored in two places, both on the URL Query and with a state defined. It can be optimized as follows:

function SearchBox({ data }) {
  constsearchKey = parse(localtion.search)? .key;const handleSearchChange = e= > {
    const key = e.target.value;
    history.push(`/movie-list? key=${key}`);
  }
  
  return (
      <input
        value={searchKey}
        placeholder="Search..."
        onChange={handleSearchChange}
      />
  );
}
Copy the code

In actual project development, such problems are also relatively obscure, and should be paid attention to when coding.

5.3 useState is merged properly

Have you ever written code in a project like this:

const [firstName, setFirstName] = useState();
const [lastName, setLastName] = useState();
const [school, setSchool] = useState();
const [age, setAge] = useState();
const [address, setAddress] = useState();

const [weather, setWeather] = useState();
const [room, setRoom] = useState();
Copy the code

Anyway, I originally wrote that the useState split was too thin, resulting in a large useState in the code.

I suggest that variables with the same meaning can be combined into a single state to make the code much more readable:

const [userInfo, setUserInfo] = useState({
  firstName,
  lastName,
  school,
  age,
  address
});

const [weather, setWeather] = useState();
const [room, setRoom] = useState();
Copy the code

When changing a variable, make sure you include the old field, for example, if you just want to change firstName:

setUserInfo(s= > ({
  ...s,
  fristName,
}))
Copy the code

React Class state merges automatically:

this.setState({
  firstName
})
Copy the code

In Hooks, can this be used? This is possible because we can package Hooks. For example, useSetState of ahooks encapsulates similar logic:

const [userInfo, setUserInfo] = useSetState({
  firstName,
  lastName,
  school,
  age,
  address
});

// Automatic merge
setUserInfo({
  firstName
})
Copy the code

I used useSetState extensively in my project instead of useState to manage complex types of state, and mom loved me more.

Six, summarized

As a longtime React Hooks user, I appreciate the benefits of React Hooks, which is why I’ve embraced Hooks completely for years. I also feel increasingly that React Hooks are difficult to control, especially with the coming of React 18 Concurrent mode.

Finally, three suggestions:

  1. Use more advanced Hooks that someone else has packaged, such as the Ahooks library
  2. Learn more about React Hooks by reading some of the react-hooks source code, such as the Ahooks library
  3. You can follow my official account [Front-end Tech Bricks], and I’ll always post some of the tech articles I’ll write myself, and the guys I understand you guys better, love you guys