airjp73/rvf

[Bug]: Value of `useControlField` is `undefined` on first render

AlexanderKulia opened this issue · 5 comments

Which packages are impacted?

  • remix-validated-form
  • @remix-validated-form/with-zod
  • @remix-validated-form/with-yup
  • zod-form-data

What version of these packages are you using?

  • @remix-validated-form/with-zod": "^2.0.7"
  • "remix-validated-form": "^5.1.5"
  • "zod": "^3.22.4"
  • "zod-form-data": "^2.0.2"

Please provide a link to a minimal reproduction of the issue.

https://github.com/AlexanderKulia/use-control-field-example

Steps to Reproduce the Bug or Issue

  1. Clone repo
  2. Run yarn install
  3. Run yarn dev
  4. Open browser, open console
  5. Refresh page, field is undefined before its values changes to "alex"

Expected behavior

I expected that the field's initial value is set to whatever is passed in defaultValues similar to useState<string>("alex"). Am I doing something wrong? Because with this behavior I have to change the type to string | undefined and handle undefined which is annoying in complicated dynamic forms

Screenshots or Videos

No response

Platform

  • OS: macOS, Linux
  • Browser: Chrome
  • Version: 121.0.6167.189

Additional context

No response

Have you tried using useEffect?

export const MyForm = () => {
  const [value, setValue] = useControlField(
    "myField",
    "myForm"
  );

  useEffect(() => {
    // Now we can update the value of the field
    // for whatever reason we need.
    setValue("Some value");
  });

  return (
    <ValidatedForm validator={myValidator} id="myForm">
      <MyInput
        name="myField"
        // This assumes your input component
        // has a `value` and `onChange` prop
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
    </ValidatedForm>
  );
};

Example in here:
https://www.remix-validated-form.io/reference/use-control-field

I'm not quite sure what the example demostrates. I've added useEffect to my example repo and it does not seem to do anything regardless of how I set it up. In the particular example in the docs useEffect is executed on every render (deps array is missing) which does not make sense and leads to infinite recursion

image

P.S. I had a small mistake in my initial example: value and onChange were missing. It's corrected now, but it doesn't change the fact that the value is undefined on first render. Moreover, now I get this

image

I consider it a bug as well. The workaround is to read the defaultValue from useField and set the form element value as value={value ?? defaultValue} where value would be read through useControlField.

This happens only when using useControlField outside the context of the form itself. This hook has the same limitations mentioned in the "Other Considerations" section on this page. Though admittedly it doesn't look like I documented that anywhere.

I think a potential API improvement we could make here is to allow passing a default value directly into the hook like you mentioned.

But this is more of an intentional (if unfortunate) trade-off of the current API, rather than a bug. If you think about it, in code that looks like this, it's impossible for the useControlField hook to know anything about the default values of the form.

const { defaultValues } = useLoaderData();

const [value, setValue] = useControlField("myField");

return (
  <ValidatedForm
    defaultValues={defaultValues}
    // ...etc
  />
)

There are 3 main ways to fix the issue your observing.

Manually provide a default

const [firstName = defaultValues.firstName, setFirstName] = useControlField("firstName");

Use setFormDefaults instead of the defaultValues prop

export const loader = async () => {
  return setFormDefaults(formId, {
    firstName: "alex",
    lastName: "k",
  });
};

Call useControlField inside the form

const ControlledInput = (props) => {
  const [value, setValue] = useControlField(props.name);
  return (
    <ValidatedInput
      value={value}
      onChange={e => setFirstName(e.target.value)}
      {...props}
    />
  );
}

RVF v6 has been released 🎉

This issue no longer exists in v6. If it's still a problem in that version, please feel free to open a new issue.

You can find the documentation here and the migration guide here.