Search code examples
reactjsnext.jsreact-state-management

How do I manage the complex state of a list of components? (Nextjs/ReactJS)


I have a form that I use to create a new recipe with. This form contains a list of ingredients a user can add to and delete from. It looks like this:

Ingredient List

I have an Ingredient Component which is just the quantity, unit, and title input fields. What I'm struggling with is how to keep track of the list of ingredients, each with its own quantity, unit, and title states.

Hierarchy is
Recipe Form Component
-> Individual Ingredient components

When I click the plus button, it appends an Ingredient Component to the list of ingredients state which is just a list of Ingredient components. I dont think that's the right approach because I don't see how I can extract each ingredient's state (quantity, unit, title) when I need to save.

So:

  1. How do I store the collective state in the recipe form component? JSON? When I click plus, do I add an empty object to the list? And would I then just render the ingredient components by mapping this json object?
  2. How do I modify this list of ingredients by the new ingredient information in each individual component if that makes sense. Do I pass the whole list to each ingredient and edit the list based on an index? That doesn't sound like the right thing to do.

Thanks in advance.


Solution

  • To store the collective state of the ingredients in the Recipe Form Component, you should use an array of objects. Each object represents an ingredient with its own quantity, unit, and title. When you click the plus button, you can add an empty ingredient object to the list. Then, you can render the ingredient components by mapping over the array of ingredients.

    To modify the list of ingredients based on the new ingredient information in each individual component, you can pass the entire list of ingredients to each ingredient component and edit the list based on the index of the ingredient being modified or deleted. This approach allows you to update the state in the Recipe Form Component, which holds the collective state of all the ingredients.

    By using this approach, each ingredient component can have its own state for quantity, unit, and title, while the Recipe Form Component maintains the collective state of the ingredients. here is the code, give it try:

        import React, { useState } from "react";
    
    const RecipeForm = () => {
      const [ingredients, setIngredients] = useState([
        { quantity: "", unit: "", title: "" },
      ]);
    
      const handleAddIngredient = () => {
        setIngredients((prevIngredients) => [
          ...prevIngredients,
          { quantity: "", unit: "", title: "" },
        ]);
      };
    
      const handleIngredientChange = (index, updatedIngredient) => {
        setIngredients((prevIngredients) => {
          const newIngredients = [...prevIngredients];
          newIngredients[index] = updatedIngredient;
          return newIngredients;
        });
      };
    
      const handleDeleteIngredient = (index) => {
        setIngredients((prevIngredients) => {
          const newIngredients = [...prevIngredients];
          newIngredients.splice(index, 1);
          return newIngredients;
        });
      };
    
      return (
        <div>
          {ingredients.map((ingredient, index) => (
            <Ingredient
              key={index}
              ingredient={ingredient}
              onChange={(updatedIngredient) =>
                handleIngredientChange(index, updatedIngredient)
              }
              onDelete={() => handleDeleteIngredient(index)}
            />
          ))}
          <button onClick={handleAddIngredient}>Add Ingredient</button>
        </div>
      );
    };
    
    const Ingredient = ({ ingredient, onChange, onDelete }) => {
      const { quantity, unit, title } = ingredient;
    
      const handleInputChange = (event) => {
        const { name, value } = event.target;
        onChange({ ...ingredient, [name]: value });
      };
    
      return (
        <div>
          <input
            type="text"
            name="quantity"
            value={quantity}
            onChange={handleInputChange}
          />
          <input
            type="text"
            name="unit"
            value={unit}
            onChange={handleInputChange}
          />
          <input
            type="text"
            name="title"
            value={title}
            onChange={handleInputChange}
          />
          <button onClick={onDelete}>Delete</button>
        </div>
      );
    };
    
    export default RecipeForm;

    By using this approach you are not passing the whole state to each ingredient instead you pass only the onChange method to Ingredient, which only changes that object in the state.