Search code examples
javascriptreactjsredux

Why does useSelector return initial state while in redux dev tool it shows updated state


I am using redux and am not sure that why do I get initial state when try to access but it shows updated state in redux dev tool.

What I am doing: I have two buttons "Yes", "No" and in reducer, I have initial state so when i click on "yes" button I update the initial state using dispatch method and I am able to see updated state in redux dev tool but when i access state from redux in "No" button's click function it shows initial states:

reducer.js

import { SET_MODAL_DETAILS, CLEAR_MODAL_DETAILS } from "../actions/types";

const initialState = {
  modalInfo: {
    isOpen: false,
    modalName: "",
    title: "",
    content: "",          
  },
};

const modal = (state = initialState, action) => {
  const { type, payload } = action;

  switch (type) {
    case SET_MODAL_DETAILS:
      return { ...state, modalInfo: payload };

    case CLEAR_MODAL_DETAILS:
      return { ...state, modalInfo: null };

    default:
      return state;
  }
};
export default modal;

action.js

import {SET_MODAL_DETAILS, CLEAR_MODAL_DETAILS} from "./types";

export const setModalDetails = (payload) => ({
    type: SET_MODAL_DETAILS,
    payload: payload,
});

export const clearModalDetails = () => ({
    type: CLEAR_MODAL_DETAILS,
});

store.js

import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import rootReducer from "./reducers";
import checkTokenExpiration from "./checkToken";

const middleware = [thunk];

const store = createStore(
  rootReducer,
  process.env.NODE_ENV === "production"
    ? applyMiddleware(...middleware, checkTokenExpiration)
    : composeWithDevTools(applyMiddleware(...middleware, checkTokenExpiration))
);

export default store;

student.js

import { connect } from "react-redux";
import Button from "@mui/material/Button";
import { setModalDetails } from "../../redux/actions/modal";

const Student = ({ setModalDetails, modal: { modalInfo } }) => {
  const handleYesButton = () => {
    setModalDetails({
      ...modalInfo,
      isOpen: true,
      title: "Confirm Modal",
      content: "Do you want to change status?",
      modalName: "confirmModal",
    });
  };

  const handleNoButton = () => {
    console.log(modalInfo); // returns initial state while I have updated via above funtion
  };

  return (
    <>
      <Button onClick={handleYesButton}>Yes</Button>
      <Button onClick={handleNoButton}>No</Button>
    </>
  );
};

const mapStateToProps = (state) => ({
  modal: state.modal,
});

export default connect(mapStateToProps, { setModalDetails })(Student);

Solution

  • If I pass both functions from parent component then it's not working as expected but if I use both function in child component like in student.js then it's working. Could you suggest why?

    It all works in Student (student.js) because Student is directly subscribed to the state.modal state from the store. When the store updates, Student is rerendered with the latest selected modal state value injected as a prop and modal.modalInfo is re-enclosed in the handleNoButton function scope. When the "no" button is clicked, it has the current modalInfo value.

    In the case where the button click handlers aren't working in StudentA (studentA.js) it is because you've closed over a stale copy of the state.modal.modalInfo in some local state that is never updated.

    const Parent = ({ setModalDetails, modal: { modalInfo } }) => {
      const handleYesButton = () => {
        setModalDetails({
          ...modalInfo,
          isOpen: true,
          title: "Confirm Modal",
          content: "Do you want to change status?",
          modalName: "confirmModal"
        });
      };
    
      const handleNoButton = () => {
        console.log("from parent: ", modalInfo);
      };
    
      const [
        confirmOptions,
        setConfirmOptions // <-- never called to update confirmOptions
      ] = useState({
        buttons: [
          {
            label: "Yes",
            onClick: handleYesButton // <-- store state closed over!
          },
          {
            label: "No",
            onClick: handleNoButton  // <-- store state closed over!
          }
        ]
      });
    
      return <StudentA options={confirmOptions} />;
    };
    

    When the redux state is updated and Parent rerenders, setConfirmOptions isn't called to update the confirmOptions value, thus StudentA is passed the current confirmOptions state which now contains stale redux state values.

    If you want to create an options object to be passed down then just compute it directly and pass it down. This way a new confirmOptions object is created with the current selected state value any time Parent renders.

    Example:

    const confirmOptions = {
      buttons: [
        {
          label: "Yes",
          onClick: handleYesButton
        },
        {
          label: "No",
          onClick: handleNoButton
        }
      ]
    };
    
    return <StudentA options={confirmOptions} />;
    

    Edit why-does-useselector-return-initial-state-while-in-redux-dev-tool-it-shows-updat

    If doing this triggers more StudentA renders than is necessary or if it was an expensive computation to create the object, you should use the useMemo hook to memoize and provide a stable options object. Note, however, that handleYesButton and handleNoButton would be external dependencies in creating the confirmOptions object, and so they too would need to be memoized in order to provide stable references.

    Example:

    const handleYesButton = useCallback(() => {
      setModalDetails({
        ...modalInfo,
        isOpen: true,
        title: "Confirm Modal",
        content: "Do you want to change status?",
        modalName: "confirmModal"
      });
    }, [setModalDetails, modalInfo]);
    
    const handleNoButton = useCallback(() => {
      console.log("from parent: ", modalInfo);
    }, [modalInfo]);
    
    const confirmOptions = useMemo(
      () => ({
        buttons: [
          {
            label: "Yes",
            onClick: handleYesButton
          },
          {
            label: "No",
            onClick: handleNoButton
          }
        ]
      }),
      [handleNoButton, handleYesButton]
    );
    
    return <StudentA options={confirmOptions} />;
    

    or because handleYesButton and handleNoButton aren't referenced anywhere else in Parent they can be moved into the useMemo hook callback.

    const confirmOptions = useMemo(
      () => ({
        buttons: [
          {
            label: "Yes",
            onClick: () => {
              setModalDetails({
                ...modalInfo,
                isOpen: true,
                title: "Confirm Modal",
                content: "Do you want to change status?",
                modalName: "confirmModal"
              });
            }
          },
          {
            label: "No",
            onClick: () => {
              console.log("from parent: ", modalInfo);
            }
          }
        ]
      }),
      [modalInfo, setModalDetails]
    );
    
    return <StudentA options={confirmOptions} />;