I am building a React application that uses an input field to allow users to type a recipe title, then, when they submit the form, the recipe should be added to a RecipeList.
At the moment, my code isn’t working correctly because I can only type 1 word before the input field loses focus. If I had to guess, I would guess that the form is re-rendering somehow when I type a letter.
I want to continue typing without my input losing focus.
Here is my code:
function RecipeApp() {
const [recipes, setRecipes] = useState(initialRecipes);
const [recipeInput, setRecipeInput] = useState("");
const [summaryInput, setSummaryInput] = useState("");
const [ingredientsInput, setIngredientsInput] = useState([]);
const [cookTimeInput, setCookTimeInput] = useState("");
function handleAddRecipe(e) {
e.preventDefault();
if (!recipeInput || !summaryInput || !ingredientsInput || !cookTimeInput) {
alert("Please fill out all fields!");
return;
}
const newRecipe = {
id: recipes.length,
title: recipeInput,
summary: summaryInput,
ingredients: ingredientsInput.split(",").reduce(
(acc, ing, idx) => ({
...acc,
[`ingredient${idx + 1}`]: ing.trim(),
}),
{}
),
cookTime: parseInt(cookTimeInput, 10),
};
setRecipes([...recipes, newRecipe]);
setRecipeInput("");
setSummaryInput("");
setIngredientsInput("");
setCookTimeInput("");
}
function InputForm() {
return (
<form onSubmit={handleAddRecipe}>
<p>Recipe name</p>
<input
value={recipeInput}
onChange={(e) => setRecipeInput(e.target.value)}
/>
<p>Summary</p>
<input value={summaryInput} placeholder="Enter a description" />
<p>Ingredients</p>
<input
value={ingredientsInput}
placeholder="List up to four ingredients, seperated by a comma"
/>
<p>Cook Time (minutes)</p>
<input value={cookTimeInput} placeholder="Time in minutes" />
<button>Add</button>
</form>
);
}
function RecipeList() {
return (
<ul>
{recipes.map((recipe) => (
<li key={recipe.id}>
{recipe.title}
<button>View</button>
<button>Delete</button>
</li>
))}
</ul>
);
}
return (
<div className="RecipeApp">
<InputForm />
<h2>List of recipes:</h2>
<RecipeList />
</div>
);
}
2
Answers
CAUSE:
InputForm has input which are calls a setState function to update the value. useState causes re-render of the whole functional component. This is causing your code to re-render complete ReceipeApp where it returns JSX with InputForm and ReceipeList. So, here InputForm is re-rendered with existing values of (recipe, cook time, receipe list etc..) on every letter typed in any of the input.
SOLUTION:
InputForm function must be outside the ReceipeApp function. Try to have every function separately and pass props if required.
Example: InputForm may look like this
}
Note: Add type="submit" to your form button and handle onchange in all input fields
This is a common pitfall
You can read more on the same here : Nesting and organising components
The following two sample codes demonstrate the issue in detail.
Code 1 : Child Component definition not nested
Test run
Observation
The following output is taken when the Child render button was clicked 4 times, and the Parent render button was clicked 2 times. When the Child renders, the parent does not render. However, when the Parent renders, the child is also rendered, this is the default scheme of React. Now adding 1 for the initial render of both Parent and Child, the total renders displayed is 3 and 7 for the Parent and Child respectively.
Code 2 : Child Component definition nested
Test run
Observation
The following output is taken based on the same user interaction. The Child render button was clicked 4 times, and the Parent render button was clicked 2 times.
As we saw earlier, when the Child renders, the parent does not render. However, when the Parent renders, the child is also rendered, this is the default scheme of React. Now adding 1 for the initial render of both Parent and Child, the total renders displayed is 3 and 3 for the Parent and Child respectively.
Now see the difference : 3 & 7 Vs. 3 & 3
The number of times the Child rendered is less in code 2 compared to the code 1.
Let us see what happens over here:
Step 0 : Loading the code 2.
Step 1 : On clicking Render Child 4 times, shows its count 5.
Step 2 : Now on clicking Render Parent, for the first time, it increments the Parent rendered count to 2. However it resets the Child rendered count to 2. This new number is just the same as that of the rendered count of Parent. The issue over here is that the Child component has lost its own state and it had been reset to 0.
Step 3 : Now on clicking Render Parent a second time, it increments the Parent rendered count to 3. However it again resets the Child rendered count to 3. This is also the same as that of the rendered count of Parent. Again the Child component has lost its own state and it had been reset to 0.
The issue is whenever the Parent renders, the Child loses its state. This happens in the code 2, that is when the Child component definition nested inside the Parent. It does not happen in the code 1.
There is a rule governing this behaviour. React will preserve state of components across renders only when the same component has been rendered in the same place in the Render Tree.
Now a question may come here tat the Parent in this case always renders the same component Child. It does not change the component across the renders. It means Parent over here does not change the returned JSX based on any conditional.
However, the Child rendered on every render of Parent is not the same. It is different. Therefore React resets the state in Child component as per Reacts state retention rule. The rule applies here as different component rendered in the same position of the Render Tree does not retain state.
This may surprise us, but it is technically true that the Child component rendered on every render of Parent is different. It becomes different since its definition has been nested within the Parent. Therefore the pitfall cautioned us in the beginning of this post that do not do it.
You may be able to read more on preserving states from here Different components at the same position reset state.