facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
223.76k stars 45.56k forks source link

[Compiler Bug]: value read during render does not update when using `react-hook-form` `.watch` API #29144

Closed erikshestopal closed 2 weeks ago

erikshestopal commented 2 weeks ago

What kind of issue is this?

Link to repro

https://github.com/erikshestopal/react-compiler-bug

Repro steps

Reproduction steps:

  1. Clone the repo
  2. Run bun install
  3. Run bun dev to start the dev server
  4. Type in a value into the customer ID text field
  5. Expect the address ID text field to show up but it does not

When trying to read a value during render using react-hook-form's .watch API with the babel-plugin-react-compiler plugin enabled, the value is not updated and the component that is supposed to be rendered conditionally never renders.

How often does this bug happen?

Every time

What version of React are you using?

19.0.0-rc-3f1436cca1-20240516

josephsavona commented 2 weeks ago

Thanks for filing this. If i'm understanding correctly, the issue is with the following two lines:

const form = useForm({ defaultValues: { customerId: "", addressId: "" } });
const customerId = form.watch("customerId");

Where the customerId isn't updating when React Compiler is applied.

React Compiler is applying the equivalent of the following to this code:

const form = useForm({ defaultValues: { customerId: "", addressId: "" } });
const customerId = useMemo(() => form.watch("customerId"), [form]);

If you do the same, and apply useMemo (without the compiler), does customerId update as you'd expect? Note that React components must be pure/idempotent. If props, state, and context haven't changed, then the component is expected to return the same result. Since form is the same, it's expected that customerId would be the same too. So it would help to understand if customerId updates if you use the useMemo version without compiler applied.

erikshestopal commented 2 weeks ago

@josephsavona Thanks for the quick response.

If you do the same, and apply useMemo (without the compiler), does customerId update as you'd expect?

const form = useForm({ defaultValues: { customerId: "", addressId: "" } });
const customerId = useMemo(() => form.watch("customerId"), [form]);

If I apply this change manually without the compiler enabled, I still get the same result (customerId not being updated) because as you mentioned, form remains the same across re-renders.

I can get around issue using the useWatch hook react-hook-form provides but it was unexpected that the behavior changed with the compiler enabled.

josephsavona commented 2 weeks ago

Thanks for confirming. As I noted above, React expects components to be idempotent and only update if their props, state, or context changes. It looks like the watch() API isn't using state to tell React that something has changed, which goes against React's rules around idempotency.

The useWatch() API looks like it does use internal state to tell React when changes occur, so that should work just fine (both with useMemo and React Compiler).

erikshestopal commented 2 weeks ago

Got it - thanks! Will close out then since this works as expected when using useWatch.