11
votes

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
You can simply add it as default value to either the Textfield or the form with useForm({defaultValues: {name: 123})Domino987
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-viq6qSam
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);Domino987
You can use the useEffect hook to set the value comming from the serverdeepak Sharma

4 Answers

11
votes

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}}).
5
votes

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.

1
votes

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>});
0
votes

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;