Reputation: 25942
I'm currently using a common Context
pattern I've seen, which allows child components to update a parent's state (i.e. the Provider
) by passing a modifier function down through the shared Context
.
The problem I'm having, is that the modifier functions only reference the original state, and don't reference the latest state...
state.user
) from the modifier function (i.e. modUser
)?Below is the minimum code to reproduce:
Expected output: Nick
... then Bob
... then BOB
.
Actual output: Nick
... then Bob
... then NICK
.
import React, { useState, useContext, useEffect } from "react"
const defaultState = {
user: null,
setUser: () => { },
modUser: () => { }
}
const Context = React.createContext(defaultState)
const Test = () => {
const setUser = user => setState(prevState => ({ ...prevState, user }))
const modUser = () => setUser(state.user.toUpperCase()) // `state.user` never changes to updated value!
// user is 'Nick' at start...
const initState = {
user: 'Nick',
setUser,
modUser
}
// user changes to 'Bob' after 1 second...
useEffect(() => { setTimeout(() => setUser('Bob'), 1000) }, [])
const [state, setState] = useState(initState)
return <Context.Provider value={state}>
<div>Parent user is {state.user}</div>
<TestInner />
</Context.Provider>
}
const TestInner = () => {
const state = useContext(Context)
// user (should) change to uppercase 'BOB' after 2 seconds...
useEffect(() => { setTimeout(() => state.modUser(), 2000) }, [])
return <div>Child user is {state.user}</div>
}
export default Test
Upvotes: 3
Views: 313
Reputation: 39330
You can pass a callback to the state setter. Looking a little closer at your code there is no need to wrap setUser or modUser in a useCallback because after const [state, setState] = useState(initState);
you never change them. The following can work and initializes the state once so mod and setUser don't need to be re created on re render of Test (because they are only used on first render and then ignored.
const { useState, useContext, useEffect } = React;
const Context = React.createContext();
const Test = () => {
//use callback to set state with initial value, after
// first render the callback will not be called again
// until component is unmounted and re mounted
const [state, setState] = useState(() => ({
setUser: user =>
setState(prevState => ({ ...prevState, user })),
modUser: () =>
setState(state => ({
...state,
user: state.user.toUpperCase(),
})),
user: 'Nick',
}));
//get the setUser function to pass it to useEffect as a dependency
const { setUser } = state;
useEffect(() => {
setTimeout(() => setUser('Bob'), 1000);
}, [setUser]);
return (
<Context.Provider value={state}>
<div>Parent user is {state.user}</div>
<TestInner />
</Context.Provider>
);
};
const TestInner = () => {
const { modUser, user } = useContext(Context);
useEffect(() => {
setTimeout(() => modUser(), 2000);
}, [modUser]);
return <div>Child user is {user}</div>;
};
ReactDOM.render(<Test />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Upvotes: 1