Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Re-render form if array changes with React-hook-form

I have a form for adding a cocktail recipe. I am using React-Hook-Form to build it. One of the sections is list of ingredients. For a start there should be only a single row. If the "Add ingredient" button is clicked, another row should show.

I can get it to add a row, but I cannot get it to re-render, when that happens. I am aware that I could probably do it with useState, but I cannot figure out how to use that with React-Hook-Forms, if that's even possible.

Here is how it looks in the initial render:

Ingredients at initial render.

And after clicking the "Tilføj ingediens" (add ingredient), it should look like this:

Desired outcome after cliking "Add ingredient".

However, this doesn't happen. From using console.log it appears that the ingredients array is in fact updated. It just doesn't re-render the form. Does anyone know how to do that, without abusing React (like triggering a re-render by changing some hidden component, etc.)? I have tried looking into the watch feature of React-Hook-Forms. It feels like a step in the right direction, but I cannot figure out how exactly it can be done.

Code

import FileInput from "../components/FileInput";
import Input from "../components/Input";
import Label from "../components/Label";
import TextArea from "../components/TextArea";
import { useForm, SubmitHandler } from "react-hook-form";

interface Cocktail {
    name: string;
    description: string;
    imageUrl: string;
    ingredients: Ingredient[];
}

interface Ingredient {
    name: string;
    quantity: string;
    unit: string;
}

const CreateCocktail = () => { 
    const {register, getValues, handleSubmit} = useForm<Cocktail>({ 
        defaultValues: { 
            name: '',
            description: '',
            imageUrl: '',
            ingredients: [
                {name: '', quantity: '', unit: ''}, {name: '', quantity: '', unit: ''}
            ]
        } 
    });

    const onAddIngredient = () => {
        // This part should somehow trigger a re-render of the form.
        console.log("Adding ingredient");
        getValues().ingredients.push({name: '', quantity: '', unit: ''});
    }
    
    const onSubmit: SubmitHandler<Cocktail> = async data => {
        try {
            console.log("Submitting cocktail", data);

            data.description = "Test description";
            data.imageUrl = "Test image url";

            const response = await fetch('/api/cocktails', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(data),
            });

            if (response.ok) {
                console.log('Cocktail created');
            } else {
                console.log('Error creating cocktail');
            }
        } catch (error) {
            console.log(error);
        }
    };
    
    return (
        <>
            <div>
                <form onSubmit={handleSubmit(onSubmit)} >  
                        // Rest of the form isn't relevant for this example                      
                        <Label htmlFor="ingredients" text="Ingredienser" />                            
                        <div>
                            {getValues().ingredients.map(() => (
                            <div key={Math.random()}>
                                <input
                                    type="text"
                                    placeholder="Lys rom" />
                                
                                <input 
                                    type="text" 
                                    placeholder="60" />

                                <select>
                                    <option value="ml">ml</option>
                                    <option value="dashes">stænk</option>
                                    <option value="stk">stk</option>    
                                </select>                 
                            </div>
                            ))} 
                            <div>
                                <button 
                                    type="button"
                                    onClick={onAddIngredient}>
                                    Tilføj ingrediens
                                </button>
                            </div>
                        </div> 
                                            
                    </div>
                    <div>
                        <input type="submit" 
                            value="Gem" />
                    </div>
                </form>
            </div>
        </>
    );
}

export default CreateCocktail;
like image 508
Jakob Busk Sørensen Avatar asked Jul 16 '26 23:07

Jakob Busk Sørensen


2 Answers

Problem

The code you provided uses getValues incorrectly becaus getValues will only give you the values when it is called but does not update itself or others when they are changed (get is only for fetching current values).

Instead, we must use setValue to change form values.

Furthermore, if you want to get real-time/onChange updates from hook-form values you cannot use getValues (this only gets the current value when called but does not update when changed), you must use watch('fieldNameGoesHere') //tracks specific field by name OR watch() //tracks all values for this instead.

Refer to the code below for a working example of what you are trying to achieve.

Solution

import { useForm, SubmitHandler } from "react-hook-form";

interface Cocktail {
    name: string;
    description: string;
    imageUrl: string;
    ingredients: Ingredient[];
}

interface Ingredient {
    name: string;
    quantity: string;
    unit: string;
}

export const CreateCocktail = () => { 
    const { getValues, handleSubmit, setValue, watch} = useForm<Cocktail>({ 
        defaultValues: { 
            name: '',
            description: '',
            imageUrl: '',
            ingredients: [
                {name: '', quantity: '', unit: ''}, {name: '', quantity: '', unit: ''}
            ]
        } 
    });
    //create a watch for ingredients, you could also pass a string array to watch multiple values if needed
    const IngridientsList = watch('ingredients')
    //update item on click via setValue
    const onAddIngredient = () => {
        setValue('ingredients', [...getValues('ingredients'), {name: '', quantity: '', unit: ''}])
    }
    
    const onSubmit: SubmitHandler<Cocktail> = async data => {
        try {
            console.log("Submitting cocktail", data);

            data.description = "Test description";
            data.imageUrl = "Test image url";

            const response = await fetch('/api/cocktails', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(data),
            });

            if (response.ok) {
                console.log('Cocktail created');
            } else {
                console.log('Error creating cocktail');
            }
        } catch (error) {
            console.log(error);
        }
    };
    
    return (
        <>
            <div>
                <form onSubmit={handleSubmit(onSubmit)} >  
                        <div>
                            {IngridientsList.map(() => (
                            <div key={Math.random()}>
                                <input
                                    type="text"
                                    placeholder="Lys rom" />
                                
                                <input 
                                    type="text" 
                                    placeholder="60" />

                                <select>
                                    <option value="ml">ml</option>
                                    <option value="dashes">stænk</option>
                                    <option value="stk">stk</option>    
                                </select>                 
                            </div>
                            ))} 
                            <div>
                                <button 
                                    type="button"
                                    onClick={onAddIngredient}>
                                    Tilføj ingrediens
                                </button>
                            </div>
                        </div> 
                                            
                    
                    <div>
                        <input type="submit" 
                            value="Gem" />
                    </div>
                </form>
            </div>
        </>
    );
}

export default CreateCocktail;
like image 181
Kyle The Wizard Avatar answered Jul 19 '26 13:07

Kyle The Wizard


First, you need to use setValue method to update form values not getValues.push(). Second getValues will not trigger rerender, you need to use watch and pass the name of field that you want to listen i.e. ingredients. If you do not pass the name to watch then it returns all form values.

Here's the complete code.

import { type SubmitHandler, useForm } from 'react-hook-form';

interface Cocktail {
    name: string;
    description: string;
    imageUrl: string;
    ingredients: Array<Ingredient>;
}

interface Ingredient {
    name: string;
    quantity: string;
    unit: string;
}

const CreateCocktail = () => {
    const { getValues, handleSubmit, setValue, watch } = useForm<Cocktail>({
        defaultValues: {
            name: '',
            description: '',
            imageUrl: '',
            ingredients: [{ name: '', quantity: '', unit: '' }]
        }
    });

    const onAddIngredient = () => {
        setValue('ingredients', [...getValues().ingredients, { name: '', quantity: '', unit: '' }]);
    };

    const onSubmit: SubmitHandler<Cocktail> = async (data) => {
        try {
            console.log('Submitting cocktail', data);

            data.description = 'Test description';
            data.imageUrl = 'Test image url';

            const response = await fetch('/api/cocktails', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(data)
            });

            if (response.ok) {
                console.log('Cocktail created');
            } else {
                console.log('Error creating cocktail');
            }
        } catch (error) {
            console.log(error);
        }
    };

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <label htmlFor='ingredients'>Ingredienser</label>
            <div>
                {watch('ingredients').map(() => (
                    <div key={Math.random()}>
                        <input type='text' placeholder='Lys rom' />

                        <input type='text' placeholder='60' />

                        <select>
                            <option value='ml'>ml</option>
                            <option value='dashes'>stænk</option>
                            <option value='stk'>stk</option>
                        </select>
                    </div>
                ))}
                <div>
                    <button type='button' onClick={onAddIngredient}>
                        Tilføj ingrediens
                    </button>
                </div>
            </div>

            <div>
                <input type='submit' value='Gem' />
            </div>
        </form>
    );
};

export default CreateCocktail;
like image 20
Muhammad Nouman Rafique Avatar answered Jul 19 '26 15:07

Muhammad Nouman Rafique



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!