How to get updated state in useEffect when using update loop - javascript

So as you probably know, in normal mode, we use update dependencies to get notice when the state updated, like this:
const [val, setVal] = useState();
useEffect(() => {}, [val]]);
But in my case, I have an array in my state and I'm trying to update it in a loop in my useEffect like this:
const [val, setVal ] = useState([...]);
useEffect(() => {
anotherArr.forEach(i => {
// get val and modify some indexes
setVal(modifiedValuesArray);
}
}, []);
In this case, every time forEach loop runs, I'm getting the initial val (I know because val is not a
dependency of useEffect) but if I put it as a dependency, it will update twice. what is the solution for this?
EDIT: Basically, I mean when I update state in a round of loop in useEffect, on the next round, I'm not getting the updated state but the initial state before entering the loop. And I know, that is because of the nature of useEffect which gives us a memorized value of state (since we didn't pass it as a dependency to avoid the additional execution), but what is the solution in these types of scenarios.

I came across this answer:(https://stackoverflow.com/a/59422750/2728431) and it solved my problem.
for getting updated state, (as #usafder said in a comment), we need to pass state as a value in an arrow function just like this:
const [val, setVal ] = useState([...]);
useEffect(() => {
anotherArr.forEach(i => {
setVal(val => {
// modify based on provided val on arrow function
return modifiedValuesArray
});
}
}, []);

Whenever you need to update the state using its current value, you need to send in a function instead to the state setter which would give you the updated current value of the state as a param. So in your case it would be something like below:
const [val, setVal ] = useState([...]);
useEffect(() => {
anotherArr.forEach(i => {
setVal((currVal) => {
let modifiedValuesArray = [];
// your update logic here (use currVal instead of val)
return modifiedValuesArray;
});
});
}, []);

Use setVal only once, not during an forEach loop. setState is async so you can't depend on it like its synchronous. In your example setVal will actually be executed some time in the future.. Do you maybe have a codesandbox example?
EDIT: You don't get updated state on "next round". setState will be executed N times, and will put it in an update queue, and React will probably only update the last setState value for optimisation. Also, your example useEffect will run only once..

Related

Execute useEffect() only on specific setState()

I am using a hook component and several state variables. I have read about using useEffect() with params to get a kind of callback after updating a state. Example:
export const hookComponent = () => {
const [var, setVar] = useState(null);
useEffect(() => {
//do things
}, [var])
}
In this example, useEffect() would be executed on every setVar() call. In my case, I do not want to execute useEffect() everytime, but only on specific occasions.
I would like to give the setVar() some kind of information which I can use in useEffect() like setVar(newValue, true).
Note: I do not want to store this information in var.
Is there a way to do this?
Like Nizar said, simple conditional check on 'var' in useEffect
If expensive calc you can
const expensiveValue = useMemo(() => {
// other logic here if needed
// could even be simple return var=='x'?true:false, although this would be easier to do in the useEffect hook?
return computeExpensiveValue(var);
},[var]);
useEffect(() => {
//do things
//expensiveValue only changes when you want it to from the memo
}, [expensiveValue])
Thank you sambomartin and Nizar for your input.
For everyone looking for an answer:
After some further research I found 3 possible solutions:
Use a class component. If you really are dependent on that state update to be completed switch to a class component, which allows you to give the setState() a callback as a second param.
Use the useRef hook to determine where your state update is comming from. You can use this information in the useEffect() method.
Get independent from the state. I used this solution and externalized my callback function with the drawback of giving it every parameter on every call, although they are present in the component the states are saved.
As far as I know, the useEffect only triggers if the dependency value changes, not simply by executing setValue.
I offer you three solutions, the first one, close to what you want but without using useEffect hook, the second one is an extension of the first one, that may be required if you need control over the previous state, and the third, more general, like comments say, though it won't be triggered if the state is the same, even if you execute setValue.
First solution: Wrap your set value with another function that definitely controls what may happen after or before the new state:
export default function MyComponent() {
const [state, setState] = useState(null);
const handleChangeSetState = (nextState, flag) => {
if (flag) {
specialUseCaseCb();
}
setState(nextState);
};
return <div>{/* ... */}</div>;
}
Second solution: Wrap your set value with another function, like in the solution 1, and ask for the previous or next state within setState inner callback:
export default function MyComponent2() {
const [state, setState] = useState(0);
const handleChangeSetState = (increment, flag) =>
setState((prevState) => {
const nextState = prevState + increment;
// you may need prevState or nextState for checking your use case
if (flag) {
specialUseCaseCb();
}
return nextState;
});
return <div>{/* ... */}</div>;
}
Third solution: use useEffect hook to follow changes, remember though that setState won't re-trigger useEffect hook if the state is the same:
export default function MyComponent3() {
const [state, setState] = useState("");
// notice that this will only be triggered if state changes
useEffect(() => {
if (state !== "my-special-use-case") return;
specialUseCaseCb();
}, [state]);
return <div>{/* ... */}</div>;
}

setState() does not stop rendering

I want to update useState array values by calling a function that maps through an array (called from the database) and the useState array will be updated for each item in the (database array) so I have tried the following approach:
const [snapshots, setSnapshots] = useState();
const [items, setItems] = useState([]);
// *** get from the database ***** //
useEffect(()=> {
db.collection("users").doc("4sfrRMB5ROMxXDvmVdwL").collection("basket")
.get()
.then((snapshot) => {
setSnapshots(snapshot.docs)
}
) ;
}, []);
// *** get from the database ***** //
// *** update items value ***** //
return <div className="cart__items__item">
{snapshots && snapshots.map((doc)=>(
setItems([...items, doc.data().id]),
console.log(items)
))
}
</div>
// *** update items value ***** //
but the following error appears:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
I have tried to console.log the result to see the check the issue and the Items array was logged in the console continuously I have tried to include the code in a useEffect but it did not work as well .
Never call a state setter at the top level of your component function. With function components, the key thing to remember is that when you change state, your function will get called again with the updated state. If your code has a state change at the top level of the function (as yours does in the question), every time the function runs, you change state, causing the function to run, causing another state change, and so on, and so on. In your code:
const initialArray = []; // *** 1
const [Items, setItems] = useState(initialArray) // *** 2
initialArray.push("pushed item")
setItems(initialArray) // *** 3
Creates a new array every time
Only uses the first one to set the initial value of Items when the component is created
Sets new array in state, causing the function to be called again
Instead, you should be setting state only in response to some change or event, such as a click handler, or some other state changing, etc.
Also note that you must not directly modify an object (including an array) that you have in state. Your code doesn't technically do that (since there's a new initialArray every time), but it looks like what you meant to do. To add to an array in state, you copy the array and add the new entry at the end.
An example of the above:
function Example() {
const [items, setItems] = useState([]);
const clickHandler = e => {
setItems([...items, e.currentTarget.value]);
};
return <div>
<div>
{items.map(item => <div key={item}>{item}</div>)}
</div>
<input type="button" value="A" onClick={clickHandler} />
<input type="button" value="B" onClick={clickHandler} />
<input type="button" value="C" onClick={clickHandler} />
</div>;
}
(Slightly odd UI just to keep the code example simple.)
Note that conventionally Items would be called items.
Re your update:
That code calls setItems at the top level of the function, so it has the problem above. Instead, you do that work in the useEffect querying the database.
There's no reason to call setItems repeatedly during the map operation.
The code should handle the component unmounting while the DB operation is outstanding
The code should actually render something in the map in the JSX
The code should handle errors (rejections)
E.g., something like this:
const [snapshots, setSnapshots] = useState();
const [items, setItems] = useState(); // *** If you're going to use `undefined`
// as the initial state of `snapshots`,
// you probably want to do the same with
// `items`
useEffect(()=> {
let cancelled = false;
db.collection("users").doc("4sfrRMB5ROMxXDvmVdwL").collection("basket")
.get()
.then((snapshot) => {
// *** Don't try to set state if we've been unmounted in the meantime
if (!cancelled) {
setSnapshots(snapshot.docs);
// *** Create `items` **once** when you get the snapshots
setItems(snapshot.docs.map(doc => doc.data().id));
}
})
// *** You need to catch and handle rejections
.catch(error => {
// ...handle/report error...
});
return () => {
// *** The component has been unmounted. If you can proactively cancel
// the outstanding DB operation here, that would be best practice.
// This sets a flag so that it definitely doesn't try to update an
// unmounted component, either because A) You can't cancel the DB
// operation, and/or B) You can, but the cancellation occurred *just*
// at the wrong time to prevent the promise fulfillment callback from
// being queued. (E.g., you need it even if you can cancel.)
cancelled = true;
};
}, []);
// *** Use `items` here
return <div className="cart__items__item">
{items && items.map(id => <div>{id}</div>)/* *** Or whatever renders ID */}
</div>;
Note that that code assumes that doc.data().id is a synchronous operation.
What you are seeing here is standard react lifecycle behavior. Your component will be mounted and then render (run alle the code inside your component). After rendering the first time it "listens" to changes to the values that you handle in your component and re-renders if any changes are detected.
Your case:
const initialArray = [];
const [Items, setItems] = useState(initialArray)
initialArray.push("pushed item")
setItems(initialArray)
I see 2 bad things here:
You modify the array which you use as the initial value of your state and keep using that same array to update your state.
You push a new item and update the state on each render when calling setItems(initialArray)
Lets focus on the second one, since thats the one that causes your issues. If you want to avoid the endless render cycle then you should move your setItems() call to a method which doesn't run on each render. Before functional components this would be done in the componentDidMount() function. In a functional component this is done using the useEffect hook:
useEffect(() => {
// Your code here
}, [])
Notice the empty array supplied to useEffect. This array lists which dependencies should cause this useEffect to run. If you leave it empty it will only run once and act like the old componentDidMount() function.
So to solve your issue of endless rendering, you need to move setItems() into the useEffect hook.

React hooks: callback as prop re-render every hooks depending on it when it's called

In the example below, I'm getting in a loop when I call the onError prop in fetchItems(). I don't understand why, when called, it triggers hooks depending on it. How can I fix this? Thanks!
const Component = ({onError}) => {
const [items, setItems] = useState([]);
const itemsRef = useRef(items);
const fetchItems = useCallback(() => {
const [first] = itemsRef.current;
fetchNewItemsSince(first || 0).then((newItems) => {
setItems((oldItems) => [...oldItems, ...newItems]);
}).catch(onError);
}, [onError]);
// Update ref to dispose closure on `items` state
useEffect(() => {
itemsRef.current = items;
}, [items]);
// Call once on mount
useEffect(() => {
fetchItems();
}, [fetchItems]);
// Make an interval
useEffect(() => {
const id = setInterval(fetchItems, ONE_MINUTE);
return () => {
clearInterval(id);
};
}, [fetchItems]);
};
Try setting the initial state with a function
Const [foo, setFoo] = useState(() => ‘foo’)
Your useCallback for that state instance probably runs once, correct me if I’m wrong, so if you set a function for useState it will only run once, consider is a component did mount but no update.
Maybe this is practice but I have never called something like [onError] without setting an error state, because that’s what I think recognizes it.
So React is this great thing that renders certain components etc. UseEffect is great, I personally don’t use it to change the state, that for me is usually done in the JSX.
I would do like a onClick handler with the UseState method and watch for changes there. Instead your running a function that runs a setState event and watches for an event.
Let me know if that works or not if you need any explanation.
As I said in the comments, onError is triggering a re-render on the parent and therefore the children will also render again.
Someone suggested removing onError from the useCallback array of dependencies. Although it might work, it is considered a bad practice because can lead to memory-leak. Have you tried to remove the useCallback wrap around your function?

React hooks function dependency

I am finding myself in a weird situation. I am implementing an hook and I cannot manage to achieve what I want.
I have something like this:
const appHook = props => {
const [foo, setFoo] = React.useState([]);
const [bar, setBar] = React.useState([]);
React.useEffect(() => {
setFoo(getFoo(props.fooList, props.fooId));
setBar(getBar(foo.listToFilter));
}, [props.fooId]);
const getCurrentBlockTrade = (arrayToFilter, number) =>
arrayToFilter.filter(array => array.id === number);
const getSubOutList = (...) => {
...
};
return (<div>something</div>)
}
My issue is that the function setFoo is properly executed, so foo state is a new array, but setBar that depends on the state of foo, receives an empty array. Basically setBar is executed before setFoo finished so the getBar function receives an empty array.
What is the right way to manage this kind of dependency?
Thanks,
F.
TL;DR; Your solution is likely kind user's answer
Below I'll describe what I've thought and learned so far throughout researches, and come up with 5 suggestions/solutions from people, via blogs,...
You've said:
My issue is that the function setFoo is properly executed, so foo state is a new array, but setBar that depends on the state of foo, receives an empty array. Basically setBar is executed before setFoo finished so the getBar function receives an empty array.
You're true. Basically because in React (both Hooks and class component), setState is asynchronous. What does it mean? It means that setSomething just tells React to re-render the component later. It doesn't magically replace the const something variable in the current running function — that's not possible.
const [foo, setFoo] = useState(0)
function handleClick() {
setFoo(42)
// we declared foo with const, you "obviously" shouldn't expect this
// to "somehow" immediately change `foo` to 42
console.log(foo);
// it's 0 in this render, BUT on next render, `foo` will be 42
}
Solution 1.
The easiest technique to you is to store the newly calculated value of foo in a variable, then use that newly calculated value to both setFoo and setBar - a quite popular technique tho.
React.useEffect(() => {
const newFoo = getFoo(props.fooList, props.fooId);
setFoo(newFoo);
setBar(getBar(newFoo.listToFilter));
}, [props.fooId]);
// or: shouldn't use this, only to demonstrate the callback syntax in
// the new setState Hook (different than the old callback syntax setState):
React.useEffect(() => {
setFoo(() => {
const newFoo = getFoo(props.fooList, props.fooId);
setBar(getBar(newFoo.listToFilter));
return newFoo;
})
}, [props.fooId]);
Solution 2.
Another technique can be found here: https://stackoverflow.com/a/54120692/9787887 is using useEffect to setBar with the dependency list whose foo.
React.useEffect(() => {
setFoo(getFoo(props.fooList, props.fooId));
}, [props.fooId]);
React.useEffect(() => {
setBar(getBar(foo.listToFilter));
}, [foo]);
Despite the answer get 27 upvotes, I think it's just overcomplicated the situation, and also (as I know) make the component unnecessarily rerender 2 times instead of 1, should be avoided.
Solution 3.
Another solution that might work is to use async/await to make the state changes triggered asynchronously, to lead the changes not be batched (regarding this answer https://stackoverflow.com/a/53048903/9787887)
React.useEffect(async () => {
await setFoo(getFoo(props.fooList, props.fooId));
await setBar(getBar(foo.listToFilter));
}, [props.fooId]);
// no, actually this will not work!! it'll throw you an (annoyed) error
// the actual working code is:
React.useEffect(() =>
const setFooAndBar = async () => {
await setFoo(getFoo(props.fooList, props.fooId));
await setBar(getBar(foo.listToFilter));
}
setFooAndBar();
}, [props.fooId]);
You see, the working code is again another overcomplicated (and bad) solution, (but should be introduced anyway??).
Solution 4.
Another solution that gaearon mentioned is to use useReducer
With Hooks you could also useReducer to centralize state update logic and avoid this pitfall.
Another his insight:
the recommended solution is to either use one variable instead of two (since one can be calculated from the other one, it seems), or to calculate the next value first and update them both using it together. Or, if you're ready to make the jump, useReducer helps avoid these pitfalls.
But it again seems to be another overcomplex suggestion to this case, doesn't it?
Solution 5.
The last suggestion is a comment of gaearon, tell you to rethink about your state dependence, is the state dependence really needed?
the best solution is simply to not have state that is calculated from another state. If this.state.y is always calculated from this.state.x, remove this.state.y completely, and only track this.state.x. And calculate what you need when rendering instead
Thank you for being patient enough to read to here :)).
Setting a state is an asynchronus process. So setBar(getBar(foo.listToFilter)); calling this foo is the empty array. You can use another useEffect for this
React.useEffect(() => {
setFoo(getFoo(props.fooList, props.fooId));
}, [props.fooId]);
React.useEffect(() => {
setBar(getBar(foo.listToFilter));
}, [foo]);
setState is an asynchronous function, that's why you are receiving an empty array in setBar function. Basically you can't be sure that the state will be updated before the second setState evaluates.
Why not to simply refer to the props in both cases?
React.useEffect(() => {
const newFoo = getFoo(props.fooList, props.fooId);
setFoo(newFoo);
setBar(getBar(newFoo.listToFilter));
}, [props.fooId]);

React setState with function

I am attempting to setState in a React component using a function callback which is now the recommended way.
It is in the componentDidMount and when I get my jobs data back I need to update the state.
It works when I set it directly but I have attempted may functions callbacks and cannot get it to work.
Sample code provided below with one of my many attempts.
async componentDidMount(){
const jobs = await loadJobs();
this.setState({jobs});
//this.setState((prevState, jobs) => {return {jobs: [prevState,...jobs]}})
}
What is the correct syntax?
You only need to make use of functional setState when you want to update current state based on prevState, in your case it seems like you just want to set the state jobs, you would simply write
this.setState({jobs});
However if you want to use functional setState, you would still write
this.setState((prevState) => {
return {jobs};
})
You need to update the jobs by getting value from prevState and appending the result to the previous state, you would do it like
async componentDidMount(){
const jobs = await loadJobs();
this.setState((prevState) => {
return {jobs: [...prevState.jobs, ...jobs]}
})
}

Categories