skip to Main Content

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;

3

Answers


  1. 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;
    
    Login or Signup to reply.
  2. Problem

    This code uses getValues incorrectly, it cannot update hookform values via getValues(get is only for fetching current values), we must use setValue to change form values.

    Furthermore, if you want to get real-time updates from form values you cannot use getValues (this only gets the current value when called but does not update when changed), you must use watch 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: ''}
                ]
            } 
        });
    const IngridientsList = watch('ingredients')
        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;
    
    Login or Signup to reply.
  3. What you need is called Dynamic Form.
    Using setValue and getValue might be very tricky.
    You should use useFieldArray hook from react-hook-form.
    I wrote something for you here so you can see the exact usage.
    BTW this is a link to documentation.
    And this is a link to a good video make by the creator of react-hook-form.
    Hope it helps.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search