react-rules-of-hooks-ppx

A ppx that validates React's Rules of Hooks at compile time.

Exhaustive dependencies

[@react.component]
let make = (~randomProp) => {
  let (show, setShow) = React.useState(() => false);

  React.useEffect1(
    () => {
      /* reads randomProp, but only [|show|] is declared */
      if (randomProp) {
        setShow(prevShow => !prevShow);
      };
      None;
    },
    [|show|],
  );

  <div />;
};
15 |     [|show|],
         ^^^^^^^^
         Error (warning 22): exhaustive-deps: Missing 'randomProp' in the dependency array.
         To suppress this warning, add [@disable_exhaustive_deps] before the expression

Dependencies are tracked at member-path granularity, like eslint-plugin-react-hooks. input.page and input.limit are distinct dependencies. A declared input.page covers deeper uses such as input.page.size, but not input.limit and not the whole input; passing the whole record somewhere requires input itself. Duplicate detection, missing and unnecessary reports, and -corrections output all use the full member path.

Effects without a dependency array (React.useEffect(fn)) run after every render and can never observe stale values, so they get no exhaustiveness checking, same as eslint-plugin-react-hooks. A direct useState/useReducer setter call in such an effect warns about an infinite update chain, and unsuffixed useMemo/useCallback warn that the memoization does nothing.

Stable values are exempt from dependency arrays, with no configuration:

Order of hooks

Calling hooks inside conditionals, loops, or nested functions fails the build:

[@react.component]
let make = (~condition) => {
  if (condition) {
    let (state, _) = React.useState(() => 0); /* That's wrong */
    ();
  };
  <div />;
};
4 |     let (state, _) = React.useState(() => 0);
                         ^^^^^^^^^^^^^^^^^^^^^^^^
Error: Hooks can't be called conditionally and must be called at the top-level of your component. Move this hook call outside of conditionals, loops, or nested functions.

Install

opam install react-rules-of-hooks-ppx
(preprocess (pps reason-react react-rules-of-hooks-ppx))

Suppress locally

Attach the attribute to the hook call:

[@disable_exhaustive_deps]
React.useEffect1(() => {...}, [|dep|]);

if (condition) {
  [@disable_order_of_hooks]
  useMyHook();
};

In OCaml syntax the attribute goes after the expression: (useMyHook ())[@disable_order_of_hooks].

[@disable_order_of_hooks] remains the escape hatch for genuinely conditional hooks.

Flags

Append to the pps line:

Issues

Report any unexpected behaviour in the bug tracker

Acknowledgements

Thanks to @jchavarri