Blog, COLDSURF

Rules of React Hook (at the top level)

This is a translation based on the article from https://ko.legacy.reactjs.org/docs/hooks-rules.html.


Hooks are JavaScript functions, but there are two important rules you need to follow when using them.

You can also enforce these rules by using a linter plugin:

https://www.npmjs.com/package/eslint-plugin-react-hooks

Only Call Hooks at the Top Level

Do not call hooks inside loops, conditions, or nested functions.

Instead, always call hooks at the top level of a React function, before any early returns.

By following this rule, React guarantees that hooks are called in the same order every time a component renders.

This ensures that React can correctly maintain the state of hooks, even if useState and useEffect are called multiple times.

Example code:

function Form() {
	const [name, setName] = useState('Mary');
	useEffect(() => {
		localStorage.setItem('formData', name);
	}, [])
	const [surname, setSurname] = useState('Poppins');
	useEffect(() => {
		document.title = name + ' ' + surname;
	}, [])
}

So how does React know which state corresponds to which useState call?

The answer is that React depends on the order in which hooks are called.

Since the order of hook calls is always the same during every render, the example can function correctly.

// ------------
// First Render
// ------------
useState('Mary')           // 1. Declare the 'name' state variable as 'Mary'.
useEffect(persistForm)     // 2. Add an effect to save the form data.
useState('Poppins')        // 3. Declare the 'surname' state variable as 'Poppins'.
useEffect(updateTitle)     // 4. Add an effect to update the document title.

// -------------
// Second Render
// -------------
useState('Mary')           // 1. Read the 'name' state variable (arguments are ignored).
useEffect(persistForm)     // 2. The effect to persist the form is updated.
useState('Poppins')        // 3. Read the 'surname' state variable (arguments are ignored).
useEffect(updateTitle)     // 4. The effect to update the title is updated.

// ...

If the order of hook calls is the same between renders, React can correctly link the state for each hook.

But what happens if you call a hook inside a condition (e.g., the persistForm effect)?

// šŸ”“ This breaks the first rule by using a hook inside a conditional statement.
if (name !== '') {
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });
}

In the first render, name !== '' is true, so the hook works as expected. However, if the user resets the form and the condition becomes false during the next render, the hook will be skipped. Since hooks are skipped between renders, the order of hook calls changes.

useState('Mary')           // 1. Read the 'name' state variable (arguments are ignored).
// useEffect(persistForm)  // šŸ”“ The hook was skipped!
useState('Poppins')        // šŸ”“ Failed to read the 'surname' state variable.
useEffect(updateTitle)     // šŸ”“ Failed to update the title effect.

React no longer knows what to return for the second useState hook. React expected the second hook to match the persistForm effect as it did in the first render, but it didn't. From this point on, the hooks that follow the skipped one are misaligned, causing a bug.

This is why hooks must always be called at the top level of the component. If you want to run an effect conditionally, place the condition inside the hook, like this:

useEffect(function persistForm() {
    // šŸ‘ This no longer violates the first rule
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });
ā† Go home