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);
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} />;
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} />;