Reputation: 421
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>
);
}
Upvotes: 0
Views: 875
Reputation: 138
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);
};
Upvotes: 1
Reputation: 625
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>
Upvotes: 1