Solve - A component is changing an uncontrolled input to be controlled

The warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.

A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component

This warning happens because during the rendering phase of the input component the value went from undefined to a defined value.

The warning can be solved in two ways:

  1. You need to provide a defaultValue to the input defaultValue = "".
  2. Define the value property by using the short-circuit evaluation value = {undefined || "" }

Here is an example of how the warning was caused

Below is the input component ./src/components/FormInput.tsx I defined with the Material UI TextField component.


import { TextField, TextFieldProps } from '@mui/material';
import { FC } from 'react';
import { Controller, useFormContext } from 'react-hook-form';

type IFormInputProps = {
  name: string;
} & TextFieldProps;

const FormInput: FC<IFormInputProps> = ({ name, ...otherProps }) => {
  const {
    control,
    formState: { errors },
  } = useFormContext();
  return (
    <Controller
      control={control}
      name={name}
      // ? defaultValue = '' is missing
      render={({ field }) => (
        <TextField
          {...otherProps}
          {...field}
          error={!!errors[name]}
          helperText={errors[name] ? errors[name].message : ''}
        />
      )}
    />
  );
};

export default FormInput;

From the code snippets above you can see the Controller component didn’t have a defaultValue so later when we try to change the value of the input during the rendering cycle, the value property of the input field went from undefined to the defined value.

Here is how I included the Custom Input component into my form. You can see that FormInput has a name but the value, onBlur, onChange, ref, etc are all managed by React Hook Form. I removed some of the input fields in the form and some logic to make things simple.


import { Box, Typography } from '@mui/material';
import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
import { object, string, TypeOf } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect, useState } from 'react';
import { LoadingButton } from '@mui/lab';
import FormInput from '../components/FormInput';

const registerSchema = object({
  name: string()
    .nonempty('Name is required')
    .max(32, 'Name must be less than 100 characters'),
});

type RegisterInput = TypeOf<typeof registerschema>;

const RegisterPage2 = () => {
  const [loading, setLoading] = useState(false);

  const methods = useForm<RegisterInput>({
    resolver: zodResolver(registerSchema),
  });

  const { reset, handleSubmit } = methods;

  useEffect(() => {
    reset({
      name: 'John Doe',
    });
  }, [reset]);

  const onSubmitHandler: SubmitHandler<RegisterInput> = (values) => {
    setLoading(true);
    console.log(values);
  };

  return (
    <Box sx={{ maxWidth: '30rem' }}>
      <Typography variant='h4' component='h1' sx={{ mb: '2rem' }}>
        Register
      </Typography>
      <FormProvider {...methods}>
        <Box
          component='form'
          noValidate
          autoComplete='off'
          onSubmit={handleSubmit(onSubmitHandler)}
        >
          <FormInput
            name='name'
            required
            fullWidth
            label='Name'
            sx={{ mb: 2 }}
          />
          <LoadingButton
            variant='contained'
            fullWidth
            type='submit'
            loading={loading}
            sx={{ py: '0.8rem', mt: '1rem' }}
          >
            Register
          </LoadingButton>
        </Box>
      </FormProvider>
    </Box>
  );
};

export default RegisterPage2;

Now, my goal was to display the user’s name in the input when the form renders for the first time.

A good tool to make that work is the useEffect hook. I called the reset method I destructured from the useForm hook which is a hook from React Hook Form.


  useEffect(() => {
    reset({
      name: 'John Doe',
    });
  }, [reset]);

Manually setting the value of the input this way is really bad because during the rendering phase the input value was undefined then when the useEffect hook was evoked it called the reset method to set the value of the input to (‘John Doe ‘).

Solution One

To solve the warning: A component is changing an uncontrolled input to be controlled, you need to provide the input a defaultValue of an empty string.


 <Controller
      control={control}
      name={name}
      defaultValue = '' // ? set defaultValue to ""
      render={({ field }) => (
        <TextField
          {...otherProps}
          {...field}
          error={!!errors[name]}
          helperText={errors[name] ? errors[name].message : ''}
        />
      )}
    />

Solution Two

In the second solution, I removed the defaultValue property on the Controller component and in the TextField component itself, I added the value property and used the short-circuit evaluation to give it an empty string when the field.value is undefined during the rendering phase.


 <Controller
      control={control}
      name={name}
      render={({ field }) => (
        <TextField
          {...otherProps}
          {...field}
          value={field.value || ''} // ? Solution Two {undefined || ""}
          error={!!errors[name]}
          helperText={errors[name] ? errors[name].message : ''}
        />
      )}
    />

You can also read: