How to change React-Hook-Form defaultValue with useEffect()?
Asked Answered
S

16

150

I am creating a page for user to update personal data with React-Hook-Form. Once paged is loaded, I use useEffect to fetch the user's current personal data and set them into default value of the form.

I put the fetched value into defaultValue of <Controller />. However, it is just not showing in the text box. Here is my code:

import React, {useState, useEffect, useCallback} from 'react';
import { useForm, Controller } from 'react-hook-form'
import { URL } from '../constants';

const UpdateUserData = props => {
    const [userData, setUserData] = useState(null);
    const { handleSubmit, control} = useForm({mode: 'onBlur'});

    const fetchUserData = useCallback(async account => {
        const userData = await fetch(`${URL}/user/${account}`)
                            .then(res=> res.json());
        console.log(userData);
        setUserData(userData);
    }, []);

    useEffect(() => {
        const account = localStorage.getItem('account');
        fetchUserData(account);
    }, [fetchUserData])

    const onSubmit = async (data) => {
        // TODO
    }

    return (
        <div>
            <form onSubmit={handleSubmit(onSubmit)}>
                <div>
                    <label>User Name:</label>
                    <Controller
                        as={<input type='text' />}
                        control={control}
                        defaultValue={userData ? userData.name : ''}
                        name='name'
                    />
                </div>
                
                <div>
                    <label>Phone:</label>
                    <Controller
                        as={<input type='text' />}
                        control={control}
                        defaultValue={userData ? userData.phone : ''}
                        name='phone'
                    />
                </div>
                <button>Submit</button>
            </form>
        </div>
    );
}

export default UpdateUserData;

The called API is working well and the value is actually set to userData state.

{
  name: "John",
  phone: "02-98541566"
  ...
}

I also tried to setUserData with mock data in useEffect(), and it doesn't work either. Is there any problem in my above code?

Shoring answered 7/6, 2020 at 8:16 Comment(1)
sorry but i didn't get what are you trying to achieve can you explain more.Bimolecular
W
91

You can use setValue (https://react-hook-form.com/api/useform/setvalue).

Import it from useForm:

const { handleSubmit, control, setValue} = useForm({ mode: 'onBlur' });

Then call it with the user data after it's received:

useEffect(() => {
    if (userData) {
        setValue([
            { name: userData.name }, 
            { phone: userData.phone }
        ]);
    }
}, [userData]);

You can remove the default values from the form.

EDIT: See alternative answers below if this does not work.

Wohlert answered 7/6, 2020 at 9:2 Comment(8)
looking for typescript version of setValue for all FormValues at onceSchwartz
@tommcandrew You are right, but we still need to add defaultValues to the form.Assessment
how can setValue be used for checkboxes and radio buttons?Camfort
did not work for me. got path split error. rather answer below worked.Hoopen
I'm guessing the setValue definition has changed since this answer was written. It now takes a single name and value, as in setValue(fieldName, fieldValue). Setting a single field works with this method works; but is unfortunately not the goal.Employ
This is working fine, but isDirty falg is set to true. Is there a way without making isDirty falg to true.Birck
@SridharNatuva yes , setValue(fieldName, fieldValue, {shouldDirty: boolean});Settera
@Employ You can use methods such as replace or update for field array, however, they will cause the component to unmount and remount for the targeted field array.Moth
F
181

@tam's answer is halfway to what is needed to make it work with version 6.8.3.

You need to provide the default value but also to useEffect to reset. That particular distinction is required if you have a form that you reload with another entity. I have a complete example in CodeSanbox here.

In a nutshell, you need to define your defaultValues in the useForm:

const { register, reset, handleSubmit } = useForm({
    defaultValues: useMemo(() => {
        return props.user;
    }, [props])
});

Then you need to listen to potential change.

useEffect(() => {
    reset(props.user);
}, [props.user]);

The example in the Code Sandbox allows swapping between two users and have the form change its values.

Forde answered 18/2, 2021 at 21:20 Comment(7)
This is definitely one of the cleaner answers here.Scion
@ Patrick, this works. Just a doubt when i tried without wrapping in the useMemo also it worked, the reason why we are using useMemo is that it won't recalculate on the next render until unless any of the dependency changed. Also any other diff using a useMemo and without using a useMemo directly passing the object since useEffect is already there? Can you correct me if this wrong - New to react :)Stinkhorn
Just curious why are you using useMemo here?Afire
This is correct.Ramer
Please explain why useMemo is usedDinge
You don't have to put second snippet in useEffect. Component will rerender on props change anyway. It would've work without wrapping in useEffect.Literalminded
Hi @Patricks, can u please give me this question answer please #76860196Swipple
W
91

You can use setValue (https://react-hook-form.com/api/useform/setvalue).

Import it from useForm:

const { handleSubmit, control, setValue} = useForm({ mode: 'onBlur' });

Then call it with the user data after it's received:

useEffect(() => {
    if (userData) {
        setValue([
            { name: userData.name }, 
            { phone: userData.phone }
        ]);
    }
}, [userData]);

You can remove the default values from the form.

EDIT: See alternative answers below if this does not work.

Wohlert answered 7/6, 2020 at 9:2 Comment(8)
looking for typescript version of setValue for all FormValues at onceSchwartz
@tommcandrew You are right, but we still need to add defaultValues to the form.Assessment
how can setValue be used for checkboxes and radio buttons?Camfort
did not work for me. got path split error. rather answer below worked.Hoopen
I'm guessing the setValue definition has changed since this answer was written. It now takes a single name and value, as in setValue(fieldName, fieldValue). Setting a single field works with this method works; but is unfortunately not the goal.Employ
This is working fine, but isDirty falg is set to true. Is there a way without making isDirty falg to true.Birck
@SridharNatuva yes , setValue(fieldName, fieldValue, {shouldDirty: boolean});Settera
@Employ You can use methods such as replace or update for field array, however, they will cause the component to unmount and remount for the targeted field array.Moth
C
56

setValue didn't work for me. Alternatively, you can use the reset method:

Reset either the entire form state or part of the form state.

Here is working code:

 /* registered address */
const [registeredAddresses, setRegisteredAddresses] = useState([]);

const { register, errors, handleSubmit, reset } = useForm <FormProps> ({
    validationSchema: LoginSchema,
});

/**
 * get addresses data
 */
const getRegisteredAddresses = async () => {
    try {
        const addresses = await AddressService.getAllAddress();
        setRegisteredAddresses(addresses);
        setDataFetching(false);
    } catch (error) {
        setDataFetching(false);
    }
};

useEffect(() => {
    getRegisteredAddresses();
}, []);

useEffect(() => {
    if (registeredAddresses) {
        reset({
            addressName: registeredAddresses[0].name,
            tel: registeredAddresses[0].contactNumber
        });
    }
}, [registeredAddresses]); 
Cottonweed answered 10/8, 2020 at 15:47 Comment(1)
Thanks for this, reset({ ...reduxState }) is what I had been looking for to retain consistency between formState and reduxState.Winson
B
36

Found another easy way, I used reset API from useForm

 const { handleSubmit, register, reset } = useForm({ resolver });

After you call API and get back response data, you call reset with new apiData, make sure apiData key's are same as input keys (name attribute):

 useEffect(() => {
    reset(apiData);
  }, [apiData]);

form's default values are cached and hence once you get the data from API, we reset the form state with new data.

Babbitt answered 12/10, 2021 at 7:46 Comment(2)
You dont need useEffect, just do this when you have recieved the apiData directly in the .then section.Fermanagh
That works too, It depends on Implementation as usually API calls and handling data are made in different files. @FermanaghBabbitt
L
21

@tommcandrew's setValue parameter formatting didn't work for me.

This format did:

useEffect(() => {
  const object = localStorage.getItem('object');
  setValue("name", object.name);
}, [])
Logue answered 28/11, 2020 at 6:18 Comment(2)
This is working for me with React Native and a controller wrapper.Hemielytron
At last. I did it. Thanks broCliftonclim
K
16

although this post is 2 months old, I stumbled upon this issue today and searched for a couple of ways to do it. The most effective way I've come up with is using useMemo to set your defaultValues, like this :

const { control, errors, handleSubmit } = useForm({
    reValidateMode: 'onChange',
    defaultValues: useMemo(() => yourDefaultValues, [yourDefaultValues]),
});

This allows you to properly set values in your form, without the struggle of multiple implementations if you happen to have field arrays (which was my case).

This also works while using the advanced smart form component exemple from the official documentation. Let me know if you have any questions !

Keyes answered 27/1, 2021 at 15:50 Comment(2)
I am running on 6.8.3 and the default value gets reevaluated each time the yourDefaultValues change but nothing happens on the form (I added debug statement in the useMemo). I have verified that the name are the same from the name of the component and the components are using inputRefand register. Do you have any idea?Forde
Repro link: codesandbox.io/s/usereacthookformdefaultvalue-h6d71?file=/src/…Forde
M
9

This works for nested objects (I'm using version 6.15.1)

useEffect(() => {
    for (const [key, value] of Object.entries(data)) {
        setValue(key, value, {
            shouldValidate: true,
            shouldDirty: true
        })
    }
}, [data])
Mythify answered 1/4, 2021 at 1:54 Comment(3)
cleanest implementation imoTess
Although, eslint won't like the for of loop. A possible solution is Object.keys(data).forEach((val, i) => {})Tess
this doesnt work, when you reset form you get empty fields and not with defaultValues. reset() instead of setValues mostly works but its flaky and loads undefined async values if you have few of them, i am still looking for proper solutionVestavestal
O
6

As of react-hook-form 7.41, you can use defaultValues with async functions like this:

const {
  formState: { isLoading },
} = useForm({
  defaultValues: fetch('API'),
  // resetOptions: {
  //   keepDirtyValues: true
  // }
});

now the defaultValue field type look like this:

type AsyncDefaultValues<TFieldValues> = (payload?: unknown) => Promise<TFieldValues>;

isLoading for the async defaultValues loading state.

Offense answered 2/2, 2023 at 11:43 Comment(3)
The most up to date answerAttila
If you're fetching data inside the defaultValues key, this is either assuming that react-hook-form is going to be your state management tool or you're duplicating network requests. If you were using something like RTK Query, or Zustand, you'd want to make your calls in there and pass the data on to the forms, not having react-hook-form be the source of truth for your data.Bistre
i am using this method, how can i pass an default Image/avatar to control like webphoto?Phytohormone
L
4

Using reset is a simple solution.

const { reset } = useForm();


onClick={()=> reset({ firstname: 'Joe' }, { lastname: 'Doe' }) }
Lafond answered 22/7, 2022 at 10:58 Comment(0)
S
3

Documentation mentions a way to set the default values if the state changes here https://react-hook-form.com/api/useform/#values

function App({ values }) {
  useForm({
    values  // will get updated when values props updates       
  })
}

I have had issues reseting the form using above method, as it is leading to an infinite loop.

react hook form Maximum update depth exceeded

So, instead i tried to reset these values using useEffect, but still it did not worked.

Now, i realised that if i have to reset entire form, then i have did something like below and it worked.

  useEffect(()=>{
    reset({...dataFromApi});
  }, [quote])
Sholokhov answered 24/4, 2023 at 4:10 Comment(0)
E
1

If you are using version 7, you may no longer need useEffect to do "loading defaultValue from async call (e.g. fetch)" on the form component mounts

example from v7.47 doc:

// set default value async
useForm({
  defaultValues: async () => fetch('/api-endpoint');
})

note:

  1. that async function defined in defaultValues should return the object required for defaultValues
  2. the call (defaultValues()) will be made on component mounts (same as useEffect with empty dependency array)
  3. if you need other dependency, use reset() to load your defaultValues (because they are cached)
Envious answered 18/10, 2023 at 11:0 Comment(1)
This approach works, but I really don't see why you need to do all of this in the component itself. See my other reply here about using the HOC approachBirkner
E
0
 // single value change
    useEffect(() => {
        setValue("username", 'changed username', {
            shouldValidate: true,
            shouldDirty: true,
            shouldTouch: true
        })
    }, []);
Eaves answered 7/11, 2023 at 9:39 Comment(0)
C
0

we can set the default values of hook forms using reset provided by react-hook-forms

const { handleSubmit, control,reset} = useForm();
    const fetchUserData = useCallback(async account => {
        const userData = await fetch(`${URL}/user/${account}`)
                            .then(res=> res);
        console.log(userData);
        if(userData.status===200||userData.status===201){
            reset(userData.data);
        }
    }, []);
useEffect(() => {
        const account = localStorage.getItem('account');
        fetchUserData(account);
        
    }, [fetchUserData])

this sets the values of the fields in form with the data fetched. this works only when the field names in both form and fetched matches. you can also specify the type of data for forms using interface if you are using typescript.

interface

interface props{
name:string,
id:number
}

useForm

const { handleSubmit, control,reset} = useForm<props>();
Clipper answered 6/1 at 6:28 Comment(0)
T
0

This solution worked for me

sessionData is the result of fetching data

useEffect(() => {
  sessionData &&
    reset({
      name: sessionData?.user?.name,
      username: sessionData?.user?.username,
      email: sessionData?.user?.emails.email,
      password: "",
      confirmPassword: "",
    });
}, [sessionData]);
Titanothere answered 10/2 at 22:16 Comment(0)
W
0

I know this post is a few years old but I stumbled upon it and found that there is now a better way than the accepted answer.

The documentation now has a prop called values that will reactively update (see https://react-hook-form.com/docs/useform#values).

Here is the example code:

function App() {
  const values = useFetch("/api")

  useForm({
    defaultValues: {
      firstName: "",
      lastName: "",
    },
    values, // will get updated once values returns, this can be a state variable like OP's userData state variable.
  })
}
Wilber answered 12/3 at 20:19 Comment(1)
As a side note, to fetch data from the server I value using something like react-query or TRPC which makes it cleaner not having to manage useEffect hooks or state variables yourself.Wilber
B
0

There is a much better way to do this that I may have missed in the comments or in one of the answers.

Just send your defaultValues in as a prop to the form itself.

This higher order pattern works quite a bit better.

<UpdateAppointmentForm defaultValues={query.data} />

When I fetch from my system, I have the model I'm working with populated. Passing it directly down to the function ensures it re-renders with the data in the way I need.

In the <UpdateAppointmentForm> component, I can just use the useForm hook with the values instantiated:

interface Props {
  defaultValues: SchemaType;
}

export const UpdateAppointmentForm = (props: Props) => {

  const form = useForm<SchemaType>({
    resolver: zodResolver(Schema),
    reValidateMode: "onBlur",
    shouldFocusError: true,
    defaultValues,
    //...other stuff
  });

  // ... return your form
}

The approaches in here to manipulate the state on the fly just add unnecessary complication and other state juggling you really don't need to worry about. Let React's rendering cycle and component tree do the work for you.

Happy coding!

Birkner answered 22/3 at 22:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.