skip to Main Content

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


  1. 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

    function InputForm() {
    const [recipes, setRecipes] = useState([]);
    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("");
    }
    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" 
          onChange={(e) => {setSummaryInput(e.target.value)}}/>
    
        <p>Ingredients</p>
        <input
          value={ingredientsInput}
          placeholder="List up to four ingredients, seperated by a comma"
          onChange={(e) => {setIngredientsInput(e.target.value)}}
        />
    
        <p>Cook Time (minutes)</p>
        <input value={cookTimeInput} placeholder="Time in minutes" 
          onChange={(e) => {setCookTimeInput(e.target.value)}}/>
    
        <button type='submit'>Add</button>
      </form>
    );
    

    }

    Note: Add type="submit" to your form button and handle onchange in all input fields

    Login or Signup to reply.
  2. This is a common pitfall

    Components can render other components, but you must never nest their
    definitions

    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

    import { useState } from 'react';
    
    export default function Parent() {
      const [parentRenderCount, setParentRenderCount] = useState(1);
    
      return (
        <>
          I am the Parent component, rendered for the times : {parentRenderCount}
          <br />
          <button onClick={() => setParentRenderCount(parentRenderCount + 1)}>
            Do another Render - Parent
          </button>
          <br />
          <Child />
          <br />
        </>
      );
    }
    
    function Child() {
      const [childRenderCount, setChildRenderCount] = useState(1);
    
      return (
        <>
          I am the Child component, rendered for the times : {childRenderCount}
          <br />
          <button onClick={() => setChildRenderCount(childRenderCount + 1)}>
            Do another Render - Child
          </button>
          <br />
        </>
      );
    }
    

    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.

    enter image description here

    Code 2 : Child Component definition nested

    import { useState } from 'react';
    
    export default function Parent() {
      const [parentRenderCount, setParentRenderCount] = useState(1);
    
      function Child({ parentRenderCount }) {
        const [childRenderCount, setChildRenderCount] = useState(0);
        return (
          <>
            I am the Child component, rendered for the times :{' '}
            {parentRenderCount + childRenderCount}
            <br />
            <button onClick={() => setChildRenderCount(childRenderCount + 1)}>
              Do another Render - Child
            </button>
            <br />
          </>
        );
      }
    
      return (
        <>
          I am the Parent component, rendered for the times : {parentRenderCount}
          <br />
          <button onClick={() => setParentRenderCount(parentRenderCount + 1)}>
            Do another Render - Parent
          </button>
          <br />
          <Child parentRenderCount={parentRenderCount} />
          <br />
        </>
      );
    }
    

    Test run

    enter image description here

    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.

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