Reputation: 319
i face this issue. when i have a Axios call, which promise to dispatch a action to update the redux and execute the callback. but when callback is executed, the redux state seem to be stale.
i got a sandbox code for demo here
if you click on the getNewDate Button, the console will show the difference in the redux state. the state will be correct when redux cause a re-render.
How do i get the correct redux state during callback?
Upvotes: 0
Views: 10068
Reputation: 3129
The use previous answer of Zachary Haber's useLayoutEffect
is the correct answer.
But here are two subpar solutions that I can share, both with their own issues.
Solution 1
Use a key
on the button to inform React that it should renew the scope of the button object (i.e. re-mount the component) because some of it's dependencies have updated.
import React from "react";
import { useResponse } from "../hooks/useResponse";
import { IResponse } from "../utils/apiDef";
const MyPages = () => {
const { response, getNewDate } = useResponse();
const callbackSuccessful = (data: IResponse) => {
console.log("response is: " + response.newDate)
console.log("callback data is: " + data.newDate)
}
const callbackFail = (data: any) => {
}
const handleButton = () => {
getNewDate(callbackSuccessful, callbackFail)
}
const buttonKey = response.newDate
return <div>
hello world {response.newDate}
<br/>
<button key={buttonKey} type="button" onClick={handleButton}>getNewDate</button>
</div>;
};
export default MyPages;
A slight problem: This will display the previous result, i.e. response.newDate
will contain the value it had when the key
was changed.
Solution 2
Use a global variable e.g. state
import React from "react";
import { useResponse } from "../hooks/useResponse";
import { IResponse } from "../utils/apiDef";
const state = {
response: undefined
}
const MyPages = () => {
const { response, getNewDate } = useResponse();
state.response = response;
const callbackSuccessful = (data: IResponse) => {
console.log("state response is: " + state.response.newDate)
console.log("callback data is: " + data.newDate)
}
const callbackFail = (data: any) => {
}
const handleButton = () => {
getNewDate(callbackSuccessful, callbackFail)
}
return <div>
hello world {response.newDate}
<br/>
<button type="button" onClick={handleButton}>getNewDate</button>
</div>;
};
export default MyPages;
In the call back the state.response.newDate
is equal to data.newDate
.
A slight problem: It's not a reusable component anymore. This solution will only work if you ever have one MyPages
object in your app. All instances will point to the same static
global variable and this will introduce a contention of whoever writes last wins.
You should not do:
<MyPages />
<MyPages />
Another issue I have with this solution: React's optimization; I don't know how this will affect it.
I hope this helps.
Upvotes: 1
Reputation: 11027
The response will always be stale, that's how React hooks work. They apply a closure over all the variables in each individual render when they are created. If you absolutely need the value to not be stale in a callback function (or effect), set up a ref for it.
const { response, getNewDate } = useResponse();
const responseRef = useRef(response);
useLayoutEffect(() => {
responseRef.current = response;
}, [response]);
const callbackSuccessful = (data: IResponse) => {
console.log("response is not Stale: " + responseRef.current.newDate);
console.log("should be: " + data.newDate);
};
Once you set up a ref, you'll clearly see that the response is in-fact changing and the responseRef.current shows the same value as data.newDate.
You have to useLayoutEffect here because the order in which the effect runs is wrong for the callback. Since useEffect runs after the component re-renders while useLayoutEffect runs while the component re-renders.
Another way you could see that the useSelector is working fine and updating and that your MyPages.tsx is seeing that update is useEffect to log the change whenever it changes.
useEffect(() => {
console.log(response.newDate)
}, [response]);
If you want access to the latest redux store state in a callback without any timing issues at all, useStore is helpful, and it doesn't cause re-rendering at all.
const store = useStore();
const callbackSuccessful = (data: IResponse) => {
console.log("should be: " + data.newDate);
console.log("Redux store: " + store.getState().apiResponse.newDate);
};
https://codesandbox.io/s/react-typescript-demo-pek65?file=/src/pages/MyPages.tsx
Upvotes: 5