31

I tried to use react-hook-form to validate inputs. But I found that if the input is placed in Material UI's dialog component, react-hook-form's setValue is not working as expected, but it works when I remove Dialog component. I guess the reason is that the value is set before the component mounts, but still can't find out the solution.

The value will be retrieved from the server, so I can't use react-hook-form's defaultValues.

https://codesandbox.io/s/react-hook-form-material-ui-twbbw

I have tried to use useState to control the input value, but there is another problem. When clear the input, click submit button, and error message shows, the first letter I key in will not be displayed.

https://codesandbox.io/s/react-hook-form-material-ui-ve2en

4
  • 1
    You can simply add it as default value to either the Textfield or the form with useForm({defaultValues: {name: 123}) Commented Dec 30, 2019 at 10:02
  • Thanks. It seems work, but the data will be fetched from the server, I'm not pretty sure whether it's good way to update defaultValues after the creation. codesandbox.io/s/react-hook-form-material-ui-viq6q Commented Dec 31, 2019 at 1:12
  • 2
    Why don't you set the default values directly to the Textfield. So you do not have to update the hook object. Its because useEffect is updated before the register is called and during register the Textfield is set to ""(the default values of the hook). If you call setValue after the dialog is shown (async, which you are already doing), it works. setTimeout(() => (setValue("name", 123)), 1000); Commented Dec 31, 2019 at 15:27
  • You can use the useEffect hook to set the value comming from the server Commented Jun 14, 2021 at 10:39

6 Answers 6

24

The problem is with the register function. You are registering the Textfield with register after the ref of the Textfield is called.

The useEffect is called to set the name to 123 with setValue after the initial render. If open is true, the dialog content is rendered after the useEffect. After the content is rendered, the ref with register is called and the default value of Textfield (here undefined) is set to be the value of name.

That is why the value of the Textfield is "" on show. You need to call setValue after the render and ref callback is called, so that the value persists.

You have two options to do that:

  1. Set the value async in the useEffect with an async delay (setTimeout or promise) after open changed. So if you add open to the useEffect dependecy array and set the value async, it works. Here is a Sandbox.
  2. Set the default value of either the Textfield or add the default value to the hook with useForm({defaultValues: {name: '123}}).
Sign up to request clarification or add additional context in comments.

1 Comment

The dialog component will be reused, and default value of input may be different each time. If I use useForm({defaultValues: {name: prop.value}}), the default value is only set at the first time, I have to use setValue to update the default value if the prop.value is different. The first option you mentioned is suitable for my situation, thanks!!
7

In my case, I was using setValue in a modal. It wasn't working as expected. I had to add shouldUnregister: false in useForm.

const { errors, register, setValue, handleSubmit } = useForm({
resolver: useYupValidationResolver(CustomDomainSchema),
mode: 'all',
shouldUnregister: false,
});

I solved this after reading this Discussion on react-hook-form. Here is a working example on sandbox.

Comments

5

For external controlled component

If you are using V3, i would recommend to use react-hook-form-input https://github.com/react-hook-form/react-hook-form-input

import React from 'react';
import useForm from 'react-hook-form';
import { RHFInput } from 'react-hook-form-input';
import Select from 'react-select';

const options = [
  { value: 'chocolate', label: 'Chocolate' },
  { value: 'strawberry', label: 'Strawberry' },
];

function App() {
  const { handleSubmit, register, setValue, reset } = useForm();

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <RHFInput
        as={<Select options={options} />}
        rules={{ required: true }}
        name="reactSelect"
        register={register}
        setValue={setValue}
      />
      <button type="button">Reset Form</button>
      <button>submit</button>
    </form>
  );
}

If you are using V4, i would recommend to use Controller https://react-hook-form.com/api/#Controller

import React from 'react';
import Select from 'react-select';
import { TextField } from "@material-ui/core";
import { useForm, Controller } from 'react-hook-form';

const options = [
  { value: 'chocolate', label: 'Chocolate' },
  { value: 'strawberry', label: 'Strawberry' },
  { value: 'vanilla', label: 'Vanilla' },
];

function App() {
  const { handleSubmit, control } = useForm();

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <Controller
        as={<Select options={options} />}
        control={control}
        rules={{ required: true }}
        onChange={([selected]) => {
          // React Select return object instead of value for selection
          return { value: selected };
        }}
        name="reactSelect"
      />

      <Controller
        as={<TextField />}
        name="firstName"
        control={control}
      />

      <button>submit</button>
    </form>
  );
}

The idea to wrap your controlled component and collecting data within while still isolate re-render inside the external controlled component.

5 Comments

Thank you for the detailed explanation. I'm using V4, and I had tried to use Controller to wrap the controlled component. But setValue is still not working if the form is placed in Material UI's Dialog component. codesandbox.io/s/react-hook-form-material-ui-7hcne
@Sam please give more detail in terms of still not working
In the example, I expect that I use setValue to set the input value "123". But I click the open button, the input is still empty. The problem should be caused by Dialog component, if I remove the component, it will work well.
i think the problem is when to call setValue, you probably should call it when after the Dialogue is opened
Thanks, it's a good choice. Call setValue in Dialog onEnter event to make sure the component is mounted
1

Since setValue has its peculiarities as well explained above by @Domino987, an alternative for those scenarios where a form is filled with data fetched from a server is:

  • use useState to hold the fetched value;
  • Controllers defaultValue to set the value and;
  • a form conditionally rendered.

A pseudo example:


const [state, setState] = useState({name: '', requested: false});

useEffect(() => {
    HTTP_Service.getName().then(name => {
      setCompanyInfo({name, requested: true})
    });

}, []);

const {name, requested} = state

return ({requested ? <Text>Loading...</Text> : <View>
    <Controller
        as={
        <Input               
          label={t('name')}
          placeholder={t('name')}
        />
        }
        defaultValue={name}
        control={control}
        name="name"
        onChange={args => args[0].nativeEvent.text}
    />
</View>});

Comments

0

react-hook-form with controlled input, yup validation, material UI component, setValue

    import React from 'react';
    import {useForm, Controller} from 'react-hook-form';
    import {yupResolver} from '@hookform/resolvers/yup';
    import Autocomplete from '@material-ui/lab/Autocomplete';
    import { TextField } from '@material-ui/core';
    import * as yup from 'yup';
    
    const schema = yup.object().shape({
      firstname: yup.string().required(),
      buyer: yup.string().required(),
    });
    
    const UserForm = () => {
      const {
        watch,
        setValue,
        register,
        handleSubmit,
        control,
        formState: {errors},
      } = useForm({
        defaultValues: {
          item: {"id":2,"name":"item2"},
        },
        resolver: yupResolver(schema),
      });
    
      const itemList = [
        {id: 1, name: 'item1'},
        {id: 2, name: 'item2'},
      ];
      return (
        <div
          style={{
            margin: '200px',
          }}>
          <form onSubmit={handleSubmit(d => console.table(d))}>
          
          
            <Controller
              control={control}
              name="item"
              rules={{required: true}}
              render={({field: {onChange, value}}) => (
                <Autocomplete
                  onChange={(event, item) => {
                    onChange(item);
                  }}
                  value={value}
                  options={itemList}
                  getOptionLabel={item => (item.name ? item.name : '')}
                  getOptionSelected={(option, value) =>
                    value === undefined || value === '' || option.id === value.id
                  }
                  renderInput={params => (
                    <TextField
                      {...params}
                      label="items"
                      margin="normal"
                      variant="outlined"
                      error={!!errors.item}
                      helperText={errors.item && 'item required'}
                      
                    />
                  )}
                />
              )}
            />
    
            <input type="submit" />
            <button
              onClick={() => {
              
                setValue('item', {id: 2, name: 'item2'});
              }}>
              setValue
            </button>
            <h6>data from register</h6>
            {<pre>{JSON.stringify(watch())}</pre>}
          </form>
        </div>
      );
    };
    
    export default UserForm;

Comments

0

I had this same issue, that form.setValue didn't trigger a value change in the form. I found out that it worked when I set shouldValidate to true in the options object.

In my case I had a field and a variable with the data, both called countryCode:

  form.setValue("countryCode", countryCode, {
    shouldValidate: true,
  })

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.