I have code where if a function is invoked it will call toggleCheckedUser and pass along information about which object property to toggle. Then saves the modified object back to state (selectedSendTo).
However, when I run this, the toggle it works, but when I try to edit a second property, before changing it I try console.log(selectedSendTo) I always get the initial value whether it be an empty object {} or false instead of the previously updated object.
When I use useEffect to spy on selectedSendTo I can see that the setSelectedSendTo() function correctly updated the object. So for some reason when I revisit the object it's empty.
const [selectedSendTo, setSelectedSendTo] = useState(false);
const toggleCheckedUser = (companyID, contactID) => {
console.log(companyID, contactID);
console.log(selectedSendTo); // THIS VALUE IS always the same as INITIAL value
console.log(selectedSendTo[companyID]);
if(selectedSendTo[companyID] &&
selectedSendTo[companyID][contactID] === true){
//remove it
delete(selectedSendTo[companyID][contactID]);
}else{
setSelectedSendTo({
...selectedSendTo,
[companyID]:{
...selectedSendTo[companyID],
[contactID]: true,
}
})
}
}
Here is the DOM:
<CustomCheckbox
className="person__checkbox" name={`checkbox-${contactID}`}
alreadySelected={
selectedSendTo[company.companyID] &&
selectedSendTo[company.companyID][contactID]
}
onChange={() => toggleCheckedUser(company.companyID, contactID)}
/>
UPDATE, A POSSIBLE SOLUTION
I found that the following works:
To be able to access the current value from useState I used useRef
const selectedSendToRef = useRef();
useEffect( () => {
selectedSendToRef.current = selectedSendTo;
}, [selectedSendTo])
Then inside of my function, I can use selectedSendToRef.current to access the most recent value of `selectedSendTo.
When updating state, I can access the most recent version from state using
setSelectedSendTo( prevValue => ....)
const toggleCheckedUser = (companyID, contactID) => {
console.log(companyID, contactID, selectedSendToRef.current);
console.log('selectedSendTo[companyID]: ', selectedSendTo[companyID]);
let newValue;
if(selectedSendToRef.current[companyID] &&
selectedSendToRef.current[companyID][contactID] === true){
newValue = false;
}else{
newValue = true;
}
setSelectedSendTo(prevValue => (
{
...prevValue,
[companyID]:{
...prevValue[companyID],
[contactID]: newValue,
}
}
));
}
UPDATE 2: The Real Solution
Okay so it seems like the problem was that even after a render, the child component was not receiving the updated state because of how I had used nested functions to create the elements.
Here is how I had things
<Main Component>
<div>
{Object_1}
<div>
</Main Componenent
and object_1 was defined something like this:
const Object_1 =
<React.Fragment>
<h1>Random Header</h1>
{StateObject_Containg_Elements}
</React.Fragment>
Now to create the state object that conatined the elements I wanted to display I was using a funciton called by a useEffect hook. Basically when the server sent back data that I needed, I would tell the useEffect hook to run a function called createElements
const createElements = (data) => {
const elements = Object.keys(data).map( item => return(
<ul>
{subFunction1(item)}
</ul>
subFunction1(item){
item.contacts.map( name => {
reutrn <CustomCheckbox name={name} checked={selectedSendTo[name]}
})
}
saveElementsToState(elements);
}
As you can see we basically have a function that runs 1 time (on server response) that triggers a function that creates the array of elements that we want to display which has its own nested subfunction that includes the child component that we are asking to watch a different state object to know whether it should be checked or not.
So What I did was simplify things, I turned {Object_1} into it's own functional component, lets call it <Object1 />. Inside the component instead of calling a function I just put the function code in there to loop through and return the elements (no longer saving elements to state) and lastly I no longer needed the useEffect since just updating the state object with the data once it gets it from the server would cause my subcomponent to re-render and create the elements. Inside the sub-component I simply return null if the data in state is null.
That fixed all my problems.
so now it looks something like this:
const Object1 = () => {
if(!data)return null;
return(
Object.keys(data).map( item => return(
<ul>
{subFunction1(item)}
</ul>
subFunction1(item){
item.contacts.map( name => {
reutrn <CustomCheckbox name={name} checked={selectedSendTo[name]}
})
}
)
}
return(
<div>
<Object1 /> //This is what contains/creates the elements now
</div>
)
Related
I have a dropdown list that contains some cart items from a shop. I want the dropdown to re-render every time a cart item is added, but it doesn't and only shows my new cart addition when I close and open the cart again (remounts).
const CartDropdown = () => {
const {setCartProducts, cartProducts} = useContext(CartContext)
const {setProducts, currentProducts} = useContext(ProductsContext)
// useEffect(() => {}, [cartProducts])
const cleanCart = () => {
const cleanProducts = currentProducts
console.log(cleanProducts)
for (let i in cleanProducts) {
if (cleanProducts[i].hasOwnProperty('quantity')){
cleanProducts[i].quantity = 0
}
}
setProducts(cleanProducts)
setCartProducts([])
}
return(
<div className='cart-dropdown-container'>
<div className='cart-items' forceRemount={force}>
{cartProducts.map((product) => (
<div>
<img src={product.imageUrl}></img>
</div>)
)}
</div>
<button onClick={cleanCart}>LIMPAR</button>
<Button children={'FINALIZE PURCHASE'}/>
</div>
)
}
I want the CartDropdown to remount when the cartProducts changes.
It really depends on what those setters and getters on your useContext are returning.
Assuming they are from a useState(), then you have to make sure you always pass a different object to the setters.
From the docs:
Bailing out of a state update
If you update a State Hook to the same value as the current state,
React will bail out without rendering the children or firing effects.
(React uses the Object.is comparison algorithm.)
In other words, simply mutating currentProducts and calling setProducts passing the same (but now mutated) object will not trigger a reprender.
So your code
const cleanProducts = currentProducts
console.log(cleanProducts)
// Mutating cleanProducts
setProducts(cleanProducts)
Should be something like
const cleanProducts = [...currentProducts] // <--- changed this line
console.log(cleanProducts)
// Mutating cleanProducts
setProducts(cleanProducts)
And you have to do this everywhere you call setters.
Another thing, you should add a key attribute to elements used in a .map(). For instance:
{cartProducts.map((product) => (
<div key={product.imageUrl}>
<img src={product.imageUrl}></img>
</div>)
)}
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 have a function in my parent component which is sent to the child components as a prop.In one of my child component,I want the same function(which was sent as a prop from the parent component) to be run twice.The first will be run with some arguments and return the value from that particular child component.The second will be to pass the same function(which came from the parent component,not the one executed in this component) to a separate component as a props again.So my function is:
//Function defined in the parent component and sent as props to the child components
handleShelfChange = (bookOnChange, newSehlf) => {
!this.isTheBookNew(bookOnChange) && this.setState(state => {
let newBooks = state.books.map(book =>
{ if (bookOnChange.id === book.id)
{ book.shelf = newSehlf; }
return book;
});
return {
books: newBooks
};
}
);
BooksAPI.update(bookOnChange, newSehlf);
};
I am sending this function to one of the child component as shown below:
<BookSearch
books={this.state.books}
handleShelfChange={this.handleShelfChange}/>
Now, in my BookSearch Component,I need to perform 2 actions:
1) Use handleShelfChange() method and return values from BookSearch Component.
2) Pass handleShelfChange() method to another component as props which will use the same method again and return some values.
So,I am facing some issues in the first point, I am using the function as a callback to this.setState as
this.setState({ books : book_search }, this.check()); // callback function to this.setState
and the callback function is as shown below:
check() {
let parent_books = this.props.books;
let shelf;
console.log(this.state.books)
const book_result = this.state.books.map((book) => {
const parent = parent_books.find(parent => parent.title === book.title );
if(parent) {
console.log(parent);
book.shelf = parent.shelf;
let shelfChange = this.props.handleShelfChange(book,book.shelf) //Call to the
//function sent as props from the parent component
shelf = shelfChange
}
console.log(shelf) // undefined
return [book,shelf];
})
//console.log(book_result);
this.setState({books: book_result})
console.log(this.state.books)
}
So,I need to run the handleShelfChange() function here for all the books that satisfy the if condition and return it from the check method(the callback method). So, I tried to declare a variable outside the if condition and assign the output of the function to that variable(declared outside the if condition) .I also need to return the each book from the map function satisfying the condition.So,I have used an array to return both the values. But as I have mentioned in the comment, console.log(shelf) return an empty array.It shows undefined.What is the correct way to get both the values from the callback function? Can anyone please suggest me a solution?
Your problem arises because you are using the setState callback syntax incorrectly. The second argument to setState is a callback function you are just executing the function
You need to write it like
this.setState({ books : book_search }, this.check);
Also console.log() after setState doen't show you the updated state values, you should write it in the callback
this.setState({books: book_result}, () => {console.log(this.state.books)})
Also make sure that you are returning the appropriate value from the handleSelfChange function