This ppx validates the rules of React hooks in reason-react's components
Raku
42
165 commits
updated Sep 3, 2026
A ppx that validates React's Rules of Hooks at compile time.
useEffect, useMemo, useCallback, etc.[@react.component], [@react.client.component], and
[@react.async.component].[@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:
useState/useReducer setters and useRef results.use* wrappers around them, transitively:
let useStateValue = initial => useReducer((_, next) => next, initial);
makes setters returned by useStateValue exempt in that file.set[A-Z]..., set_..., or dispatch... destructured from any hook
call. This one is a heuristic; plain closures (let setLocal = ...),
whole-return bindings (let setAll = Hook.use()), and other names are
still checked.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.
opam install react-rules-of-hooks-ppx
(preprocess (pps reason-react react-rules-of-hooks-ppx))
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.
Append to the pps line:
-disable-exhaustive-deps turns off dependency checking.
-disable-order-of-hooks turns off order-of-hooks checking.
-corrections writes .ppx-corrected files with suggested fixes for
missing dependencies; review and accept them with dune promote:
- React.useEffect0(() => {
+ React.useEffect1(() => {
Js.log(dep1);
None;
- });
+ }, [| dep1 |]);
Report any unexpected behaviour in the bug tracker
Thanks to @jchavarri
165 commits
Raku
63.6%
OCaml
31.4%
Perl
3.5%
This ppx validates the rules of React hooks in reason-react's components
Raku
42
165 commits
updated Sep 3, 2026
A ppx that validates React's Rules of Hooks at compile time.
useEffect, useMemo, useCallback, etc.[@react.component], [@react.client.component], and
[@react.async.component].[@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:
useState/useReducer setters and useRef results.use* wrappers around them, transitively:
let useStateValue = initial => useReducer((_, next) => next, initial);
makes setters returned by useStateValue exempt in that file.set[A-Z]..., set_..., or dispatch... destructured from any hook
call. This one is a heuristic; plain closures (let setLocal = ...),
whole-return bindings (let setAll = Hook.use()), and other names are
still checked.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.
opam install react-rules-of-hooks-ppx
(preprocess (pps reason-react react-rules-of-hooks-ppx))
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.
Append to the pps line:
-disable-exhaustive-deps turns off dependency checking.
-disable-order-of-hooks turns off order-of-hooks checking.
-corrections writes .ppx-corrected files with suggested fixes for
missing dependencies; review and accept them with dune promote:
- React.useEffect0(() => {
+ React.useEffect1(() => {
Js.log(dep1);
None;
- });
+ }, [| dep1 |]);
Report any unexpected behaviour in the bug tracker
Thanks to @jchavarri
165 commits
Raku
63.6%
OCaml
31.4%
Perl
3.5%