Why react doesn't update state in "then" promise? - javascript

I have a table with Delete button in each row. When you delete the row, the Client will send request do the API asynchronously. Which means, that if you delete multiple rows quickly, you don't have to wait for one request to finish, to send another Delete request. When you click Delete button and request was send, the button will be disabled (before the response comes back and the row will disappear).
This is the function that handles delete in the Container:
const [deletingTeams, setDeletingTeams] = useState([]);
const handleDelete = (teamId) => {
setDeletingTeams([...deletingTeams, teamId]);
deleteTeam(teamId).then(() => {
console.log(deletingTeams); <---- This prints EMPTY array. Why????
let newState = deletingTeams.filter((id) => id !== teamId);
setDeletingTeams(newState);
});
};
Very simple. deletingTeams is an array that holds ids of teams that are being deleted at the moment (the response didn't comeback from that API, that deletion was successful).
In line 3 (setDeletingTeams([...deletingTeams, teamId]);) I'm adding new teamId to the array, and it works. New array is being passed to the List component and Delete button is indeed disabled. BUT...
...when response comes back, in then promise, I want to remove that teamId from the array. The problem is, that deletingTeams array is already empty (before the filter). Why??
Thanks for the explanation,

State updates are async - you are not guaranteed to see latest value of the updated state straight away.
Use the callback approach to filter and set the state.
const [deletingTeams, setDeletingTeams] = useState([]);
const handleDelete = (teamId) => {
setDeletingTeams([...deletingTeams, teamId]);
deleteTeam(teamId).then(() => {
// console.log(deletingTeams); //<---- state updates are async - you will see the latest value of the state in the next re-render
// let newState = deletingTeams.filter((id) => id !== teamId); //<---- remove this
setDeletingTeams((prev) => prev.filter((id) => id !== teamId)); //<--- use call back approach to set the state
});
};

In short, the reason this is happening is because deletingTeams gets its value before your asynchronous call. The reasons why are subtle!
What's going on is, when you use detetingTeams in the then function for deleteTeam, the callback function you pass in is using a JavaScript feature called function closures. The variable deletingTeams is used in your then function - but it's not defined by that function! So it can't have local function scope. Where exactly is it coming from, then?
The answer is nuanced. The variable is defined in your first line: const [deletingTeams, setDeletingTeams] = useState([]);. At this point, the variable is assigned a value based on your components' current state - the teams you currently have before deleting. Then, you reference it in your function. At this time, the variable deletingTeams becomes added to the then functions' scope. It is a reference to the same variable outside the function!
Ok, so the reason that deletingTeams is an empty array is because it's an empty array when you create the then function, right? Not quite. Any changes to deletingTeams are reflected when you execute the then function. But in your example, deletingTeams never changes its value. Your component's state changes when you call setDeletingTeams, but you only look up the state once - at the beginning of the block of code. There's no magic that will change deletingTeams' value; for that, you have to call useState again.
deletingTeams is given a fresh value each time your component rerenders. But unfortunately, the then function's closure scope is only determined once. (Each time you rerender, you actually generate a new then function, scoped to that render's version of the deletingTeams variable!) The component may have rerendered, but the then block is still called with the same stale value.
So, altogether, here's what's going on:
deletingTeams is assigned a value in your first line: const [deletingTeams, setDeletingTeams] = useState([]);
You update the state with setDeletingTeams, but useState is only called once, so deletingTeams is never updates to match new values.
deletingTeams gets a new value on each new render of your component, but by then, your then function is unfortunately already bound to a specific, past deletingTeams variable that never updates.
So when your then block executes, deletingTeams has a stale value.
So what is the correct approach, then? We can read the state in the then function to get the freshest value of deletingTeams. It's simply this.state.deletingTeams. Here's more information: https://reactjs.org/docs/hooks-state.html#reading-state
(Because you're using arrow functions, the then function automatically uses the same this as the component, so everything should work.)
So this should do the trick:
const [deletingTeams, setDeletingTeams] = useState([]);
const handleDelete = (teamId) => {
setDeletingTeams([...deletingTeams, teamId]);
deleteTeam(teamId).then(() => {
let newState = this.state.deletingTeams.filter((id) => id !== teamId);
setDeletingTeams(newState);
});
};
(Be sure to use this.state.deletingTeams instead of calling useState again in this kind of situation. Calling useState inside of nested functions or callbacks can lead to bugs: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level)

Related

React useState doesn't update even with useEffect added

Probably it is a classic issue with useState which is not updating.
So there is a tree with some checkboxes, some of them are already checked as they map some data from an endpoint.
The user has the possibility to check/uncheck them. There is a "cancel" button that should reset them to the original form.
Here is the code:
const [originalValues, setOriginalValues] = useState<string[]>([]);
...
const handleCancel = () => {
const originalValues = myData || []; //myData is the original data stored in a const
setOriginalValues(() => [...myData]);
};
...
useEffect(() => {
setOriginalValues(originalValues);
}, [originalValues]);
However, it is not working, the tree is not updating as it should. Is it something wrong here?
Just do the following, no need for ()=> the state will update inside the hook if called, plus change the constant it will cause confusion inside your code and protentional name clash later on, with the current state variable name, and also make sure your data are there and you are not injection empty array !!!! which could be the case as well !.
// Make sure data are available
console.log(myData)
// Then change the state
setOriginalValues([...myData]);

State not updating inside recursion

export default function App() {
const [actionId, setActionId] = useState("");
const startTest = async () => {
const newActionId = actionId + 1;
setActionId(newActionId);
const request = {
actionId: newActionId
}
console.log({ request });
// const response = await api.runTests(request)
await new Promise((resolve) => setTimeout(resolve, 4000));
startTest();
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={startTest}>Start</button>
</div>
);
}
request actionId is always 1, though I changed it every 4 seconds.
I know setState is async, but it's weird that the state is not updated after 4 seconds.
Theory
A rendered React component contains functions that belong to that rendering. If these functions reference stale variables, this will affect the result. Search for stale closures to find out more about this. It is particularly easy to end up with stale closures when dealing with the traditional Javascript functions setTimeout and setInterval in React.
So what's going on?
So on your first render, a particular instance of startTest exists. When you click the button, THAT instance of startTest runs. This instance of startTest contains a closure of actionId (by the way, why do you set actionId to "" and then do addition to this? Would be more expected to start with 0 or do addition by + "1"). When the state is set, React rerenders with actionId set to "1" and with a new version of startTest containing a closure where actionId is "1". However, this function would only be triggered if you click the button anew. Instead, what happens is that the timeout from the first render triggers a new call to the old startTest from the first render. This timeout does not know that the component has rerendered and that there is a new version of startTest some place else with an updated closure of actionId. When the function is retriggered, it calculates the same value as the first time for newActionId and so we are trapped in a loop which only uses that very first instance of startTest containing a stale closure.
How to solve it?
If you want to use timeouts and intervals in React, you gotta do it the React way. There are probably packages out there for this but if you want, you can change your example in a small way to make it work.
Instead of calculating the value of newActionId, you can supply a function to setActionId that takes the previous value as input:
setActionId(oldActionId => oldActionId + 1)
Now, since the previous value is always passed as input, the stale closure does not affect the outcome of the function and your example will work. However, I'm not sure about the design anyway because if a user hits the button again, you will have multiple timeouts running simultaneously which will cause a havoc in the long run. Nonetheless, if you want to make sure it works as expected, you could try it that way. If you could guarantee that the function would only be executed once, by restricting the button to only be pressed once, it would be a better design.
Good luck!

UseState shows previous value always [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 2 years ago.
This is a popular question among all the new react developers but somehow I'm not able to understand the logic behind available solutions. I'm trying to update the state variable using hooks and trying it read the updated value but always it returns a previous value instead of new value. Below is the sequence of my code execution.
onClick={setTransactionAccountId}
on button click, it executes the below code and updates the state but the console.log shows the old value.
const [accountId, setAccountId] = useState(0);
const setTransactionAccountId = e => {
console.log("Clicked ID:", e.currentTarget.value);
setAccountId(e.currentTarget.value);
console.log("accountId:", accountId);
};
console log:
first button click:
Clicked ID: 0
accountId: 0
second button click:
Clicked ID: 1
accountId: 0
could anyone please tell me the reason behind this behaviour and how to tackle it.
accountId won't have been updated this render. You have to wait for the next render for it to be updated. accountId only gets populated at the top of the function component when useState is called. You're in the middle of the render. If you need the actual value, keep pulling it out of e.currentTarget.value.
From react docs
React may batch multiple setState() calls into a single update for
performance.
Because this.props and this.state may be updated asynchronously, you
should not rely on their values for calculating the next state.
State updates may be batched and updated asynchronously, therefore the state may have not updated when console.log() is called. You will get the guaranteed updated result in your next call to useEffect hook.
This is because setState is asynchronous. Executing console.log('accountId:', accountId) before the state has updated will still give you the previous state value. If you add an async/await, that should fix the issue.
const setTransactionAccountId = async (e) => {
console.log('Clicked ID:', e.currentTarget.value)
await setAccountId(e.currentTarget.value)
console.log('accountId:', accountId)
}

Hooks useCallback keeps using old values when refreshing list

What is wrong with the useCallback below that I do not get the values below every time the function onRefresh is called ?
How can I make it return the expected values using Hooks?
Example when I call onRefresh 2x
values expected:
true
0
20
false
true
0
20
false
values: received
false
0
0
false
false
20
20
false
initialization of the state variables
const [refreshing, setRefreshing] = useState(false)
const [offsetQuestions, setOffsetQuestions] = useState(0)
Function call with useCallback:
const fetchSomeData = async () => {
await new Promise(resolve => setTimeout(resolve, 3000)) // 3 sec
}
const onRefresh = useCallback( async () => {
setRefreshing(true)
setOffsetQuestions(0)
console.log(refreshing)
console.log(offsetQuestions)
await fetchSomeData()
setOffsetQuestions(20)
setRefreshing(false)
console.log(offsetQuestions)
console.log(refreshing)
}, [refreshing, offsetQuestions])
Where the function is called:
<FlatList
data={questionsLocal}
refreshing={refreshing}
onRefresh={onRefresh}
...
/>
What you are getting is the expected behaviour in hooks. It's all about closures. Each render in react has its own props, state, functions and event handlers and they forever stay the same for that particular render. So whats happening here is that the useCallback is closing over the state variables for that particular render, So the console.log even after a setState will always give you the value of the state for that particular render because of closure.
This situation confuses many developers who start using hooks. You would expect that React will be reactive and will change the value of the state after you called the setX, but if you think about it, you can not expect React to stop the execution flow change the state variable and continue execution.
So, what you are getting is the state variable from the scope of the creation of the useCallback (Read it again and again).
Let's break it down to two parts, the scope, and useCallback, as these are two different things you must understand to be able to use useState and useCallback.
Scope:
Lets say that you have a state called age and a function that write/reads this state (For now, without using useCallback)
const [age, setAge] = useState(42);
const increaseAge = () => {
setAge(age + 1); // this "age" is the same "age" from above
console.log(age); // also this one
}
From this you can see that every render a new age variable is defined (The value if given by React), also a new increaseAge function is defined with access to the scope that holds the age variable.
This is how the scope works, this is why you don't get a new value of age just after calling setAge. To be able to get a new value on age React will have to re-render the component, so a new age variable will be defined on a new scope. And this is exactly what happens after calling setAge, React will trigger a new render.
useCallback
Now, let's say that you don't want to define the increaseAge function again and again, you want to keep the same reference (Can help preventing useless renders). This is where you'd want to use useCallback.
When using useCallback, React provides the same function reference each render, unless one of the dependencies changes.
Without giving the useCallback the dependencies, React will not recreate the function and the function will access to the scope that it was created on (First render), when giving the useCallback the correct dependencies, it will be recreated, and now have access to the scope with the latest state variable.
This should explain why you must provide dependencies, and how state variables are accessed (scope).
You can use UseRef hook to use the current value of state instead of lexical value.
const ref = useRef();
useEffect(() => {
ref.current = value;
});
This how you can set value in a reference and use it wherever you want.
Below is the link where you can understand this problem in detail and with possible solutions
https://dmitripavlutin.com/react-hooks-stale-closures/
Thanks

React Hooks: Adding new fields to an Object state does not get reflected immediately

I am using React Hooks to manage states within a component.
const addNode = () => {
let pform = pForm
let handles = [vForm, yForm, hForm]
let access_info = [virtualForm, management1Form, management2Form, consoleForm]
let newObj = {
...currentForm,
p: pform,
handles: handles,
access_info: access_info,
}
console.log('newObj', newObj)
setCurrentForm(
newRouterObj
)
console.log(currentForm)
let currArr = [...addedNodes]
currArr.push(currentForm)
setAddedNodes(currArr)
intializeForms()
}
The function above is an onClick that I use when I press an Add button. The forms (pForm, vForm, yForm, etc.) are all separate states. I gather them together and put them into a single object newObj and use setCurrentForm to update the currentForm state to newObj.
When I console.log the newObj, everything goes in fine. However, when I check the currentForm after the setCurrentForm, the fields (p, handles, and access_info) are empty.
I know that states in React can have a delay in updates so I might have to use useEffect. However, in my use case, which is to gather different states and put them in as a new field in the currentForm state seems useEffect is not the best way to solve it. Can anyone help please?
You are misunderstanding exactly how useState works. When you call the useState setter function, the state value isn't actually updated immediately, instead it will trigger the component to re-render with the updated value. Even though you call the setter half way through the function, the state value will remain the original value for the entire lifetime of that function call.
You could slightly tweak what you have to be
const addNode = () => {
...
let currArr = [...addedNodes]
// you know that currentForm is supposed to be newObj, so just push that
// see my explanation above to understand why it currentForm isn't what you expect
currArr.push(newObj)
...
}
It's an async action so values will not be assigned/updated instantly. You need to watch for the changes using useEffect hook to log new values and to do anything in case
useEffect(() => {
// Whenever `currentForm` will be updated, this callback will be invoked
console.log('updated currentForm values', currentForm);
},[currentForm]);

Categories

Resources