Recommended React Hooks Convention
Tags: react, hook, eslint
Published: 2022-11-15 11:45:26

# Related Plugin

- [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react)
  - `react/hook-use-state` 
- [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks) 
  - `react-hooks/rules-of-hooks`
  - `react-hooks/exhaustive-deps`

# ESLint React Hooks Plugin's Rules

## Rules of Hooks (error)

### useHookInsideLoop
   React Hook "XXX" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render.
   desc: disallowed hook inside loop
    ```jsx
    function ComponentWithHookInsideLoop() {
      while (cond) {
        useHookInsideLoop();
      }
    }
    ```
### useConditionalHook
   React Hook "XXX" is called conditionally. React Hook must be called in the exact same order in every component render. ~Did you accidentally call a React Hook after an early return?
   desc: disallowed conditionally hook
    ```jsx
    function ComponentWithConditionalHook() {
      if (cond) {
        useConditionalHook();
      }
    }
    
    function useHook() {
      if (a) return;
      useState();
    }
    
    function useHook() {
      try {
        f();
        useState();
      } catch {}
    }
    ```
### classHook
   React Hook "XXX" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.
   desc: disallowed hook in class 
    ```jsx
    class C {
      m() {
        This.useHook();
        Super.useHook();
      }
    }
    ```
### functionError
   React Hook "XXX" is called in function "SSS" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".
desc: only use hook in Component or hook function
    ```jsx
    function normalFunctionWithHook() {
      useHookInsideNormalFunction();
    }
    ```
### topLevelError
   React Hook "XXX" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function.
desc: disallowed use hook at the top level
    ```jsx
    Hook.useState();
    Hook._useState();
    Hook.use42();
    Hook.useHook();
    Hook.use_hook();
    ```
### useHookInsideCallbackError
   React Hook "XXX" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.
desc: disallowed use hook in callback or hook
    ```jsx
    function ComponentWithHookInsideCallback() {
      useEffect(() => {
        useHookInsideCallback();
      });
    }
    ```
### useEventError
   function created with React Hook "useEvent", and can only be called from the same component. They cannot be assigned to variables or passed down
    ```jsx
    // bad 
    function MyComponent({ theme }) {
      const onClick = useEvent(() => {
        showNotification(theme);
      });
      return <Child onClick={onClick}></Child>;
    }
    
    // good 
    const MyComponent = ({ theme }) => {
      const onClick = useEvent(() => {
        showNotification(theme);
      });
      return <Child onClick={() => onClick()}></Child>;
    };
    ```

## Exhaustive Deps (warning)

### Missing dependency
warn: React Hook useCallback has a missing dependency: 'props.foo'. Either include it or remove the dependency array.

```jsx
function MyComponent(props) {
  useCallback(() => {
    console.log(props.foo?.toString());
  }, []);
}
```

### Does nothing
warn: React Hook useMemo (and useCallback) does nothing when called with only one argument. Did you forget to pass an array of dependencies?

```jsx
function MyComponent(props) {
  const value = useMemo(() => { return 2*2; }); // failed
  const fn = useCallback(() => { alert('foo'); }); // failed
}
```

### No callback
warn: React Hook useEffect requires an effect callback. Did you forget to pass a callback to the hook?

```jsx
function MyComponent() {
  useEffect()
  useLayoutEffect()
  useCallback()
  useMemo()
}
```

### Unnecessary dependency
warn: React Hook useMemo has an unnecessary dependency: 'local2'. Either exclude it or remove the dependency array. 

```jsx
function MyComponent() {
  const local1 = {};
  const local2 = {};
  useMemo(() => {
    console.log(local1);
  }, [local1, local2]);
}

// useRef doesn't cause re-render 
function MyComponent({ activeTab }) {
  const ref1 = useRef();
  const ref2 = useRef();
  useEffect(() => {
    ref1.current.scrollTop = 0;
    ref2.current.scrollTop = 0;
  }, [ref1.current, ref2.current, activeTab]);
}
```

### Out of scope dependency
warn: Outer scope values like 'local1' aren't valid dependencies because mutating them doesn't re-render the component.

```jsx
function MyComponent() {
  const local1 = someFunc();
  function MyNestedComponent() {
    const local2 = {};
    useCallback(() => {
      console.log(local1);
      console.log(local2);
    }, [local1]); // failed
  }
}

function MyComponent() {
  useCallback(() => {}, [window]); // failed
}
```

### Duplicate dependency
warn: React Hook useEffect has a duplicate dependency: 'local'. Either omit it or remove the dependency array.

```jsx
function MyComponent() {
  const local = {};
  useEffect(() => {
    console.log(local);
    console.log(local);
  }, [local, local]);
}
```

### Immutable dependency
warn: The 'foo' literal is not a valid dependency because it never changes. You can safely remove it.

```jsx
function MyComponent() {
  useEffect(() => {}, ['foo']);
}
```

### Preferred array literator
warn: React Hook useEffect was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies.

```jsx
// incorrect
function MyComponent() {
  const local = {}
  const dependencies = [local];
  useEffect(() => {}, dependencies);
}

// correct
function MyComponent() {
  const local = {}
  useEffect(() => {}, [local]);
}
```

### Spread element dependency
warn: React Hook useEffect has a spread element in its dependency array. This means we can't statically verify whether you've passed the correct dependencies.

```jsx
function MyComponent() {
  const local = someFunc();
  useEffect(() => {
    console.log(local);
  }, [local, ...dependencies]);
}
```

### Complex expression dependency
warn: React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked.

```jsx
function MyComponent(props) {
  useEffect(() => {
    console.log(props.items[0]);
  }, [props.items, props.items[0]]);
}
```

### Assign value inside hook
warn: Assignments to the 'value2' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect.

```jsx
function MyComponent(props) {
  let value;
  let value2;
  let value3;
  let value4;
  let asyncValue;
  useEffect(() => {
    if (value4) {
      value = {};
    }
    value2 = 100;
    value = 43;
    value4 = true;
    console.log(value2);
    console.log(value3);
    setTimeout(() => {
      asyncValue = 100;
    });
  }, []);
}
```

### Ref value on clean up
warn: The ref value 'myRef.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'myRef.current' to a variable inside the effect, and use that variable in the cleanup function.

```jsx
function MyComponent() {
  const myRef = useRef();
  useEffect(() => {
    const handleMove = () => {};
    myRef.current.addEventListener('mousemove', handleMove);
    return () => myRef.current.removeEventListener('mousemove', handleMove);
  }, []);
  return <div ref={myRef} />;
}
```

### Always change dependency
warn: The 'handleNext' function makes the dependencies of useEffect Hook (at line 11) change on every render. Move it inside the useEffect callback. Alternatively, wrap the definition of 'handleNext' in its own useCallback() Hook.

```jsx
function MyComponent(props) {
  let [, setState] = useState();

  function handleNext(value) {
    setState(value);
  }

  useEffect(() => {
    return Store.subscribe(handleNext);
  }, [handleNext]);
}

function Component() {
  const foo = class {};
  useMemo(() => foo, [foo]);
}
```

### No dependency
warn: React Hook useEffect contains a call to 'setData'. Without a list of dependencies, this can lead to an infinite chain of updates. To fix this, pass [] as a second argument to the useEffect Hook.

```jsx
function Hello() {
  const [data, setData] = useState(0);
  useEffect(() => {
    fetchData.then(setData);
  });
}
```

### Async useEffect
warn: Effect callbacks are synchronous to prevent race conditions. Put the async function inside useEffect

```jsx
function Thing() {
  useEffect(async () => {}, []);
}
```

### Prefer inline function
warn: React Hook useEffect received a function whose dependencies are unknown. Pass an inline function instead.

```jsx
function MyComponent() {
  const local = {};
  useEffect(debounce(() => {
    console.log(local);
  }, delay), []);
}
```