简体   繁体   中英

How can I update the state of an object nested in an array from a child component?

I have RecipeCreate component, and inside of that I want a user to be able to render as many IngredientDetailsInput components as needed to complete the recipe.

I do this by creating an empty object in an ingredients array within RecipeCreate, and then iterate over this array of empty objects, generating a corresponding IngredientDetailsInput for each empty object.

From within IngredientDetailsInput I want to update the empty corresponding empty object in RecipeCreate with data passed up from IngredientDetailsInput. Since IngredientDetailsInput has the index of where it's object lives in the ingredients array in it's parent component, I believe this is possible.

Here is working sandbox that demonstrates the issue

I'm close, but each time the handleChange runs it is creating a new object in the ingredients array and I'm not sure why, or what other options to use besides handleChange - I'd like there not to have to be a form submit if possiblee

And here is code for both components

import React, { useState } from "react";

const RecipeCreate = (props) => {
  const [ingredients, setIngredients] = useState([]);
  const [recipeTitle, setRecipeTitle] = useState("");

  //if an ingredient object has been added to the ingredients array
  //render an IngredientDetailsInput component, passing along the index position
  //so we can update it later
  const renderIngredientComponents = () => {
    if (ingredients) {
      return ingredients.map((_, index) => {
        return (
          <IngredientDetailsInput
            key={index}
            position={index}
            updateIngredientArray={updateIngredientArray}
          />
        );
      });
    }
  };

  //broken function that should find the object position in ingredients
  //and copy it, and non-mutated ingredient objects to a new object, and set the state to this
  //new object
  const updateIngredientArray = (key, value, position) => {
    return setIngredients((prevIngredients) => {
      console.log(ingredients)
      return [...prevIngredients, prevIngredients[position][key] = value]
    });
  };

  //allows the user to add another "ingredient", rendering a new IngredientDetailsInput component
  //does so by adding a new, empty object to the ingredients array
  const addElementToArray = () => {
    setIngredients((prevIngredients) => [...prevIngredients, {}]);
  };

  return (
    <div>
      <div>
        <form>
          <div>
            <label>Recipe Title</label>
            <input
              type="text"
              name="recipeTitle"
              value={recipeTitle}
              onChange={(e) => setRecipeTitle(e.target.value)}
            />
          </div>
          <div>
            <p>Ingredients</p>
            {renderIngredientComponents()}
            <div>
              <p onClick={() => addElementToArray()}>+ ingredient</p>
            </div>
          </div>
          <div></div>
          <button type="submit">Submit</button>
        </form>
      </div>
    </div>
  );
};

export default RecipeCreate;

//child component that should allow changes to bubble up to RecipeCreate

export function IngredientDetailsInput(props) {
  return (
    <div>
      <input
        type="number"
        name="measurement"
        id="measurement"
        placeholder="1.25"
        onChange={(e) =>
          props.updateIngredientArray(
            "measurement",
            e.target.value,
            props.position
          )
        }
      />

      <div>
        <label htmlFor="measurementType">type</label>
        <select
          id="unitType"
          name="unitType"
          onChange={(e) =>
            props.updateIngredientArray(
              "unitType",
              e.target.value,
              props.position
            )
          }
        >
          <option>tbsp</option>
          <option>cup</option>
          <option>tspn</option>
          <option>pinch</option>
          <option>ml</option>
          <option>g</option>
          <option>whole</option>
        </select>
      </div>
      <input
        type="text"
        name="ingredientName"
        id="ingredientName"
        placeholder="ingredient name"
        onChange={(e) =>
          props.updateIngredientArray(
            "ingredientName",
            e.target.value,
            props.position
          )
        }
      />
    </div>
  );
}


May be you can try like this?

 const {useState} = React; const App = () => { const [state, setState] = useState([ { name: "", amount: "", type: "" } ]); const addMore = () => { setState([...state, { name: "", amount: "", type: "" } ]); }; return ( <div className="App"> <h1>Recipe</h1> <h2>Start editing to see some magic happen.</h2> <label>Recipe Title</label> <input type="text" /> <br /> <br /> <div onClick={addMore}>Add More +</div> {state && state,map((val; ikey) => <div> <br /> <label>Ingredients</label> <input type="text" placeholder="Name" /> <input type="text" placeholder="Amount" /> <select> <option>tbsp</option> <option>cup</option> <option>tspn</option> <option>pinch</option> <option>ml</option> <option>g</option> <option>whole</option> </select> </div> )} </div> ). } ReactDOM,render( <App />. document;getElementById("react") );
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script> <div id="react"></div>

The assignment prevIngredients[position][key] = value returns value instead of prevIngredients[position][key] . Thus when you setting the state, it returns the previous stored ingredients as well as that value.

const updateIngredientArray = (key, value, position) => {
    return setIngredients((prevIngredients) => {
      console.log(ingredients)
      return [...prevIngredients, prevIngredients[position][key] = value]
    });
  };

A quick fix would be to recopy a new array of the current ingredient, then changing the position and key that you want.

const updateIngredientArray = (key, value, position) => {
    const tmp = ingredients.map((l) => Object.assign({}, l));
    tmp[position][key] = value;
    setIngredients(tmp);
  };

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM