KS Kian Seng
KS Kian Seng

Reputation: 319

Redux useSelector not updated, need to be refresh

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?

enter image description here

Upvotes: 0

Views: 10068

Answers (2)

Barnstokkr
Barnstokkr

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

Zachary Haber
Zachary Haber

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

Related Questions