Skip to content
Moteeb Asadimoteeb.asad@gmail.com

Blog

React Compiler skipped my component and hid a lint error

I turned on the React Compiler lint rules and checked one form component. The compiler skipped it twice, for try/finally and for react-hook-form's watch(). Only after both fixes did it find a real error that my normal lint had missed the whole time.

3 min read#react#react-compiler#eslint#react-hook-form

I did a small test with React Compiler. I turned on its lint rules in one of my projects and checked a single form component: the modal that invites a new user.

The lint rules ship in eslint-plugin-react-hooks. They run the compiler over each component and report what it finds.

Three things happened, one after the other.

1. try/finally: the compiler skipped the whole component

The first warning was this:

warning  Todo: (BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause
react-hooks/todo

The submit handler looked roughly like this (simplified):

Before
const onSubmit = async (data: InviteForm) => {
  setIsSubmitting(true);
  setSuccess(false);
  try {
    const result = await inviteUserAction(data.fullName, data.email, data.roles);
    if (result.success) setSuccess(true);
    else setError(result.error);
  } finally {
    setIsSubmitting(false);
  }
};

The compiler doesn't support a try with a finally clause yet. It's a "Todo", not a mistake in my code. But the result is the same: it can't compile the component, so it skips all of it.

To keep going, I removed the finally and reset the flag after the try/catch instead:

After
const onSubmit = async (data: InviteForm) => {
  setIsSubmitting(true);
  setSuccess(false);
  try {
    const result = await inviteUserAction(data.fullName, data.email, data.roles);
    if (result.success) setSuccess(true);
    else setError(result.error);
  } catch {
    setError("Something went wrong");
  }
  setIsSubmitting(false);
};

2. watch(): the compiler skipped it again

With the finally gone, a new warning showed up:

warning  Compilation Skipped: Use of incompatible library
React Hook Form's `useForm()` API returns a `watch()` function which cannot be memoized safely.
react-hooks/incompatible-library

On these two lines:

const selectedRoles = watch("roles");
const primaryRole = watch("primaryRole");

watch() returns values that change behind the same function. React can't tell when they changed, so a memoized component could show old values. To avoid that, the compiler skips the component again.

The fix is useWatch(), which is a real hook that React can track:

After
const selectedRoles = useWatch({ control, name: "roles" });
const primaryRole = useWatch({ control, name: "primaryRole" });

3. The real error: setState inside useEffect

After switching to useWatch(), the compiler could finally read the component. And it found an actual error: I was resetting state inside a useEffect when the modal closed.

Before (simplified)
// Reset form when modal closes
useEffect(() => {
  if (!isOpen) {
    reset();
    setSuccess(false);
    setError(null);
  }
}, [isOpen, reset]);

Setting state inside an effect makes React render twice: once with the old state, then again after the effect runs. The cleaner place for a reset is the event that causes it, the close handler:

After (simplified)
const handleClose = () => {
  reset();
  setSuccess(false);
  setError(null);
  onClose();
};

The interesting part

That setState rule was already on in my ESLint config, set to error. But my normal lint showed 0 errors.

The component was being skipped the whole time. First because of try/finally, then because of watch(). The rule never got to look at it, so it never reported anything.

Check your own project

Run ESLint and look at the warnings, not only the errors:

npx eslint . | grep -E "react-hooks/(todo|incompatible-library)"

Every match is a component the compiler skipped. Until you fix the reason it was skipped, the compiler-based rules aren't checking it at all.

All posts