This is a tricky question and I have been having a hard time figuring out this. First of all I'm using useContext to make a global state. The global state will hold and serve an array with objects. I have a form rendered together with every object. The form will have an input with an value.
My goal is to be able to find the object and update the value. For example the input in Item1 will update "20" to whatever new number that are being inputted, same with Item2
What is happened now I that every time I submit an input, a whole new array are being created instead on updated. I know its a whole easier way to achieve this, but I need this array to be in a global state.
here's link to my sandbox
https://codesandbox.io/s/muddy-wildflower-h5hhw?file=/src/App.js
Thanks!
You need to specify which Array Item you want to update. First of all you need to specify which item you need to update. For this i've passed the value id through the card props
<Card
name={value.name}
value={value.Itemvalue}
key={value.id}
id={value.id}
/>
And i've used map to update the specific object
const updatedData = value.map((obj) => {
if (obj.id === id) {
return { ...obj, Itemvalue: itemValue };
} else return obj;
});
updateValue(updatedData);
And here is the working Link
The problem is you're adding a new element everything the form is updated. Which is different from what you need. Pass id to the update function, so that you can update that particular item.
Your code
const addValue = (event) => {
event.preventDefault();
// You're not updating existing element, instead adding a new value.
updateValue((prevItems) => [...prevItems, { Itemvalue: itemValue }]);
};
// ...
<form onSubmit={addValue}>
<input
type="text"
name="price"
Value={itemValue} // --> Typo here
onChange={updateItemValue}
/>
<button>Submit</button>
</form>
You should pass the id as props instead of getting it from context. So <Add /> knows exactly where to update the value.
Corrected code
import React, { useContext, useState } from "react";
import { ValueContext } from "./ValueContext";
const Add = ({ id }) => { // --> Passing id from <Card />
const [itemValue, setItemValue] = useState("");
const [value, updateValue] = useContext(ValueContext);
const updateItemValue = (event) => {
setItemValue(event.target.value);
};
const addValue = (event) => {
event.preventDefault();
updateValue((prevItems) => {
const index = prevItems.findIndex(x => x.id === id);
const updatedItem = {
...prevItems[index],
Itemvalue: itemValue
}
return [
...prevItems.slice(0, index),
updatedItem, // --> Updating the item, instead of creating new one
...prevItems.slice(index + 1)
]
});
};
return (
<form onSubmit={addValue}>
<input
type="text"
name="price"
Value={itemValue}
onChange={updateItemValue}
/>
<button>Submit</button>
</form>
);
};
export default Add;
Related
I am new in JS and ReactJS.
I tried to implement a list of boxes as follows:
import React, { useState, useEffect, useRef } from "react";
import "./App.css";
function GenreBox() {
let [input, setInput] = useState('')
let [repr, setRepr] = useState('')
let getClick = () => {
fetch(`/genre/${input}`).then(
(res) => res.json().then(
(data) => { console.log(data); setRepr(data.repr) }
)
)
}
return (<div>
<input type="text" value={input} onChange={(e) => setInput(e.target.value)}></input>
<button onClick={getClick}>Get</button>
<p>repr: {repr}</p>
</div>
)
}
function GenreBoxList() {
let [genreBoxList, setGenreBoxList] = useState([])
let [index, setIndex] = useState(0)
let insertGenreBox = (e) => {
e.preventDefault();
console.log(index);
let t = [...genreBoxList];
t.splice(index, 0, <GenreBox />);
console.log(t);
setGenreBoxList(t);
}
let removeGenreBox = (e) => {
e.preventDefault();
let t = [...genreBoxList];
t.splice(index, 1);
console.log(t);
setGenreBoxList(t);
}
let indexChange = (e) => {
e.preventDefault();
setIndex(e.target.value)
}
return (<div>
<button onClick={insertGenreBox}>+</button>
<button onClick={removeGenreBox}>-</button>
<input type='number' value={index} onChange={indexChange} />
<ol>
{genreBoxList.map((x) => <li>{x}</li>)}
</ol>
</div>)
}
export { GenreBox, GenreBoxList }
When I click the + and - button with index == 0,
I expect the front of the list to be modified.
However, it appears that no matter what number I set the index to,
it is always operating on the tail of the list...
What am I doing wrong or is this a bad design practice?
EDIT 1:
OK, it seems to be the problem with the key. React seems to treat objects with the same type and key to be equal and hence does not update the page.
EDIT 2:
Now I have added keys to both and and it seems to be functioning correctly. So is this how react is proposed to be used?
function GenreBoxList() {
let [genreBoxList, setGenreBoxList] = useState([])
let [index, setIndex] = useState(0)
let [counter, setCounter] = useState(0)
let insertGenreBox = (e) => {
e.preventDefault();
console.log(index);
let t = [...genreBoxList];
t.splice(index, 0, <GenreBox key={counter} ></GenreBox>);
setCounter(counter + 1);
console.log(t);
setGenreBoxList(t);
}
let removeGenreBox = (e) => {
e.preventDefault();
let t = [...genreBoxList];
t.splice(index, 1);
console.log(t);
setGenreBoxList(t);
}
let indexChange = (e) => {
e.preventDefault();
setIndex(e.target.value)
}
return (<div>
<button onClick={insertGenreBox}>+</button>
<button onClick={removeGenreBox}>-</button>
<input type='number' value={index} onChange={indexChange} />
<ol>
{genreBoxList.map((x) => <li key={x.key}>{x}</li>)}
</ol>
</div>)
}
As you realized in your edit, the key here is key.
Your list is an array of items where each item is created by the expression <GenreBox {...props} /> (which is in fact translated into React.createElement(GenreBox, props) ). When React sees an array of such, say, 10 items - it has no way to know which of them was added first. All it knows is that there are 10 of them.
For a moment, let's ignore the fact that the code later wraps each of them inside it's own <li> element, and assume we are rendering the array as-is into the <ol> container.
React sees there are 10 items of the component that should be rendered, and it invokes the rendering function for each. That function also uses state via useState() so React has to pass the correct state to each render. React looks in the state data remained from the previous render, and sees that there are 9 sets of state data since there were only 9 items in the previous render. How would it associate each set of state data to a component in the list? The only way would be to provide the first set of state data to the first item, the second set to the second item, etc. and leave the last item to initialize it's own new state.
By providing a unique key attribute, on the other hand, you are giving the item an identity. React would now be able to associate the item with the correct set of state data (and other hooks data as well) regardless of it's position in the list.
(In fact, even if you don't provide a key React would provide one, but this key would simply be the index of the item so everything said above still apply).
Lastly, since the code later maps the original array to a new array where each item is wrapped inside a <li> element, the actual relevant list is this list of <li> items, so the key should be provided there - as you indeed did.
Reference:
https://reactjs.org/docs/reconciliation.html#recursing-on-children
I am using an array of components that are interested depending on various conditions i.e the order and number of elements in the array are dynamic as shown below:
useEffect(() => {
const comp = [];
// if(condition1===true){
comp.push(<MyComp onChange={onValueChange} />);
// }
// if(condition2===true){
comp.push(<YourComp onChange={onValueChange} />);
// }
// if(condition3===true){
comp.push(<HisComp onChange={onValueChange} />);
// }
setComponents(comp);
}, []);
To each of the components in the array, there could be some sort of input control like input-text, input-number, text-area, chips, radio, checkbox, etc.
So there is an onChange event linked to each of these components.
I am using a common onValueChange function which is passed as a callback to these components. In the onValueChange I need 2 things:
changed value (from child component)
activeIndex (from same component)
const onValueChange = (val) => {
console.log("onChange Valled", val, "activeIndex==", activeIndex);
};
But here I am not able to fetch the updated value on activeIndex, it always gives zero no matter in what active step I am in.
Sandbox DEMO
useEffect(() => {
setComponents((previousValues)=>{
// if you want to add on previous state
const comp = [...previousValues];
// if you want to overwrite previous state
const comp = [];
if(condition1===true){
comp.push();
}
if(condition2===true){
comp.push();
}
if(condition3===true){
comp.push();
}
return comp;
});
}, []);
Try using useCallback with dependency array. Also try to avoid storing components in state - the office advice - what shouldn’t go in state?
const onValueChange = useCallback((val) => {
console.log("onChange Valled", val, "activeIndex==", activeIndex);
},[activeIndex];
For rendering try something like below.
condition1===true && <MyComp onChange={onValueChange} />
or create a function which returns the component eg: getComponent(condition) and use this in render/return. Make sure you wrap getComponent in useCallback with empty dependency array []
Hi i am new ish to JavaScript/React and I am currently making a project to practice it more.
I have an expenses list with some expenses all with a unique Id stored as props.items but i'm trying to add a delete button so that an expense will be removed from props.items when its clicked. Is there a way i can remove an item from props.items with the use of the unique ID?
Currently I have this where idNumber is the unique id sent back from the child component ExpenseItem
const onRemoveExpense = (idNumber) => {
console.log("remove clicked", idNumber)
console.log(props.items, "<- all items")
}
return (
<ul className='expenses-list'>
{props.items.map((expense) => (
<ExpenseItem
key={expense.id}
value={expense.id}
title={expense.title}
amount={expense.amount}
date={expense.date}
removeExpense={onRemoveExpense}
/>
))}
</ul>
);
}
Thanks for the help!
The biggest hurdle I see here is that your items array is not in the state of the component in question-- it is passed in as props. So you'd want to define your deletion script in which component is holding the items in its component state. You'd write it somewhere along the lines of:
const onRemoveExpense = (idNumber) => {
this.setState((state, props) => {
// get items from current state
const { items } = state;
// get a new array with only those items that do *not* have the id number passed
const newItems = items.filter((item) => item.id !== idNumber);
// return it to set the new state
return newItems;
});
}
This would obviously need to be adjusted to your specific state and component structure. You'd then pass this as a prop along with the items to the component in question, and call it to trigger a deletion.
For a "hide" function instead of a delete one, you could try adding a shown boolean prop and then change that on click.
But to actually delete it, you'll need to have your items stored in state.
You could try something like this:
const [items, setItems] = useState(props.items)
// set the initial state as `props.items`
// (I'm assuming the code snippet you shared exists inside a functional component)
const onRemoveExpense = (idNumber) => {
console.log("remove clicked", idNumber)
console.log(props.items, "<- all items")
const newItems = items.filter(({ id }) => id !== idToDelete)
setItems(newItems)
}
return (
<ul className='expenses-list'>
{items.map((expense) => (
<ExpenseItem
key={expense.id}
value={expense.id}
title={expense.title}
amount={expense.amount}
date={expense.date}
removeExpense={() => onRemoveExpense(expense.id)}
/>
))}
</ul>
);
}
I might be forgetting something though—I haven't tested the above code. You might need to have a useEffect() to make it re-render properly when the state changes.
Or you can manage the state in the component that is defining items for this component.
I have a parent component that having some props passing from grandparent component and I am using one prop (object) and pass the value of that object to children component as props. I also pass a function to child component in order to get the updated value back from child component.
ParentComponent.js
const ParentComponent = props => {
const { record, saveRecord } = props;
const editedRecord = {...record}
const handleRecordValues = (name, value) => {
editedRecord[name] = value;
};
...
const content = <div>
<ChildComponent name={record.name} value={record.value} setValue={handleRecordValues} />
<Button onClick={() => saveRecord(editedRecord)} />
</div>
return content;
}
ChildrenComponent.js
const ChildComponent = props => {
const { name, value, setValue } = props;
const [input, setInput] = useState(value);
const handleChange = (e, text) => {
setInput(text);
setValue(name, value);
}
return <TextField value={input} onChange={handleChange}/>
}
Above are the sample components I have. The issue is when I pass the editedRecord to saveRecord func to grandparent component the editedRecord is always the same as record as it is copied from record and value is not updated for that variable. I expect the editedRecord being updated by the handleRecordValues func.
For example, the record that I get is {}. And I create a new const editedRecord which is also {}.
After I input some value from ChildComponent the editedRecord should be updated to {name: value}. However when I click on Button in ParentComponent the editedRecord parameter is still {}.
Updated
Instead of using const I use
const [editedRecord, setEditedRecord] = useState(record);
const handleRecordValues = (name, value) => {
const newRecord = {
...editedRecord
};
newRecord[name] = value;
setEditedRecord(newRecord);
};
Now the editedRecord value got updated but another issue came up:
when I have multiple components as child components it only update the last one entry I have entered.
Your setValue/handleRecordValues function changes a variable ... but React has no way of knowing when that variable changes.
To let React know, you have to call saveRecord(editedRecord) after you make the change, or in other words you have to invoke a state-setting function, so that React knows about the change.
In general in React, if you don't change context/state/props (and for context/state, that means doing so using the appropriate React functions), React can't know to re-render your components in response. This means that any data that your components depend on to render needs to be changed via one of those three mechanisms, not just via ordinary Javascript, ie. a.b = c.
EDIT: To clarify a point in the comments. When you make a state variable:
const [myState, myStateSetter] = useState('');
there is nothing "magic" about myState; it's just another JS variable. Javascript doesn't give React any way to know when that variable changes, so if you just do:
myState = 4;
React has no idea that you did so. It only knows that it changed if you tell it that it changed ... ie. if you call:
myStateSetter(4);
Here's how I would alter the parent component to make everything work with react. The main issue you were having is that react needs to know that a change has occurred, so we need to set up the values as state/set state.
const ParentComponent = props => {
const { record, saveRecord } = props;
const [editedRecord,setEditedRecord] = useState(record);
useEffect(()=>{
//This sets the record straight whenever the parent passes a new record.
//You'd need to make sure the record is referentially stable when it isn't being updated, though
setEditedRecord(record);
},[record])
const handleRecordValues = (name, value) => {
setEditedRecord(record=>{...record,[name]:value});
};
...
const content = <div>
<ChildComponent name={editedRecord.name} value={editedRecord.value} setValue={handleRecordValues} />
<Button onClick={() => saveRecord(editedRecord)} />
</div>
return content;
}
I am trying to update state on click event using react hooks. State changes, but component doesn't rerender. Here is my code snippet:
function ThirdPage() {
const [selectedIngredients, setSelectedIngredients] = useState([])
const DeleteIngredient = (ingredient) => {
let selectedIngredientsContainer = selectedIngredients;
selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1);
setSelectedIngredients(selectedIngredientsContainer);
console.log(selectedIngredients);
}
const selectedIngredientsDiv = selectedIngredients.map(ingredient =>
(
<div className={styles.selectedIngredientsDiv}>{ingredient}
<div className={styles.DeleteIngredient}
onClick={() => {
DeleteIngredient(ingredient)}}>x</div></div>
))
return (
...
What am I doing wrong? Thanks in advance!
Issue with you splice as its not being saved to selectedIngredientsContainer. I would do following:
selectedIngredientsContainer = selectedIngredientsContainer.filter(value => value !== ingredient);
or
selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1 );
setSelectedIngredients([...selectedIngredientsContainer]);
Hope it helps.
normally I would leave an explanation on what's going on but tldr is that you should check first to make sure that you're array isn't empty, then you you can filter out the currentIngredients. Also you don't need curly brackets to call that function in the jsx but that can be personal flavor for personal code. I apologize if this doesn't help but I have to head out to work. Good luck!
function ThirdPage() {
const [selectedIngredients, setSelectedIngredients] = useState([]);
const DeleteIngredient = ingredient => {
// let selectedIngredientsContainer = selectedIngredients;
// selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1);
// setSelectedIngredients(selectedIngredientsContainer);
// console.log(selectedIngredients);
if (selectedIngredients.length > 0) {
// this assumes that there is an id property but you could compare whatever you want in the Array.filter() methods
const filteredIngredients = setSelectedIngredients.filter(selectedIngredient => selectedIngredient.id !== ingredient.id);
setSelectedIngredients(filteredIngredients);
}
// nothing in ingredients - default logic so whatever you want
// log something for your sanity so you know the array is empty
return;
};
const selectedIngredientsDiv = selectedIngredients.map(ingredient => (
<div className={styles.selectedIngredientsDiv}>
{ingredient}
<div className={styles.DeleteIngredient} onClick={() => DeleteIngredient(ingredient)}>
x
</div>
</div>
));
}
The answer is very Simple, your state array selectedIngredients is initialized with an empty array, so when you call map on the empty array, it will not even run once and thus DeleteIngredient is never called and your state does not change, thus no re-render happens