The reason why you are seeing the old value of admin
in the click
function is due to closures which cause the stale props and state problem. How do JavaScript closures work?
Every function in JavaScript maintains a reference to its outer
lexical environment. This reference is used to configure the execution
context created when a function is invoked. This reference enables
code inside the function to "see" variables declared outside the
function, regardless of when and where the function is called.
In a React function component, each time the component re-renders, the closures are re-created, so values from one render don't leak into a different render. It's for this same reason why any state you want should be within useState
or useReducer
hooks because they will remain past re-renders.
More information on the react side of it in React's FAQ
In addition to the main answer for the question, you should useState
for the values of user
and pass
in your component. Otherwise the values don't stay between re-renders.
You could otherwise go without using controlled components, but not use a weird mix of both.
Working example below:
const initialstate = {
user: '',
password: '',
};
const adminReducer = (state = initialstate, action) => {
switch (action.type) {
case 'admin':
return (state = {
user: action.user,
password: action.password,
});
default:
return initialstate;
}
};
const adminAction = (user, password) => {
return {
type: 'admin',
user: user,
password: password,
};
};
const store = Redux.createStore(Redux.combineReducers({ admin: adminReducer }));
const App = () => {
const store = ReactRedux.useStore();
const admin = ReactRedux.useSelector((state) => state.admin);
const dispatch = ReactRedux.useDispatch();
const [user, setUser] = React.useState('');
const [pass, setPass] = React.useState('');
const adminChangeUsername = (event) => {
setUser(event.target.value);
};
const adminChangePassword = (event) => {
setPass(event.target.value);
};
const click = (event) => {
event.preventDefault();
dispatch(adminAction(user, pass));
console.log('store has the correct value here', store.getState());
console.log('admin is the previous value of admin due to closures:', admin);
};
return (
<div>
<input
onChange={adminChangeUsername}
value={user}
placeholder="username"
/>
<input
onChange={adminChangePassword}
value={pass}
type="password"
placeholder="password"
/>
<button onClick={click}>submit</button>
<p>This shows the current value of user and password in the store</p>
<p>User: {admin.user}</p>
<p>Pass: {admin.password}</p>
</div>
);
};
ReactDOM.render(
<ReactRedux.Provider store={store}>
<App />
</ReactRedux.Provider>,
document.querySelector('#root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha256-7nQo8jg3+LLQfXy/aqP5D6XtqDQRODTO18xBdHhQow4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js" integrity="sha256-JuJho1zqwIX4ytqII+qIgEoCrGDVSaM3+Ul7AtHv2zY=" crossorigin="anonymous"></script>
<div id="root"/>
Example with original vars instead of useState
. You'll notice in this case by using the vars and keeping the value prop on the inputs, it prevents entry to the password input. The reason why it "works" on the user input is because the user variable started off as undefined rather than an empty string.
const initialstate = {
user: '',
password: '',
};
const adminReducer = (state = initialstate, action) => {
switch (action.type) {
case 'admin':
return (state = {
user: action.user,
password: action.password,
});
default:
return initialstate;
}
};
const adminAction = (user, password) => {
return {
type: 'admin',
user: user,
password: password,
};
};
const store = Redux.createStore(Redux.combineReducers({ admin: adminReducer }));
const App = () => {
const store = ReactRedux.useStore();
const admin = ReactRedux.useSelector((state) => state.admin);
const dispatch = ReactRedux.useDispatch();
var user,pass='';
const adminChangeUsername = (event) => {
user = event.target.value;
};
const adminChangePassword = (event) => {
pass = event.target.value;
};
const click = (event) => {
event.preventDefault();
dispatch(adminAction(user, pass));
console.log('store has the correct value here', store.getState());
console.log('admin is the previous value of admin due to closures:', admin);
};
return (
<div>
<input
onChange={adminChangeUsername}
value={user}
placeholder="username"
/>
<input
onChange={adminChangePassword}
value={pass}
type="password"
placeholder="password"
/>
<button onClick={click}>submit</button>
<p>This shows the current value of user and password in the store</p>
<p>User: {admin.user}</p>
<p>Pass: {admin.password}</p>
</div>
);
};
ReactDOM.render(
<ReactRedux.Provider store={store}>
<App />
</ReactRedux.Provider>,
document.querySelector('#root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha256-7nQo8jg3+LLQfXy/aqP5D6XtqDQRODTO18xBdHhQow4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js" integrity="sha256-JuJho1zqwIX4ytqII+qIgEoCrGDVSaM3+Ul7AtHv2zY=" crossorigin="anonymous"></script>
<div id="root"/>
Example with fully uncontrolled inputs. This doesn't need the vars at all:
const initialstate = {
user: '',
password: '',
};
const adminReducer = (state = initialstate, action) => {
switch (action.type) {
case 'admin':
return (state = {
user: action.user,
password: action.password,
});
default:
return initialstate;
}
};
const adminAction = (user, password) => {
return {
type: 'admin',
user: user,
password: password,
};
};
const store = Redux.createStore(Redux.combineReducers({ admin: adminReducer }));
const App = () => {
const store = ReactRedux.useStore();
const admin = ReactRedux.useSelector((state) => state.admin);
const dispatch = ReactRedux.useDispatch();
const click = (event) => {
event.preventDefault();
const user = event.currentTarget.user.value;
const pass = event.currentTarget.pass.value;
dispatch(adminAction(user, pass));
console.log('store has the correct value here', store.getState());
console.log('admin is the previous value of admin due to closures:', admin);
};
return (
<div>
<form onSubmit={click}>
<input name="user" placeholder="username" />
<input name="pass" type="password" placeholder="password" />
<button type="submit">submit</button>
</form>
<p>This shows the current value of user and password in the store</p>
<p>User: {admin.user}</p>
<p>Pass: {admin.password}</p>
</div>
);
};
ReactDOM.render(
<ReactRedux.Provider store={store}>
<App />
</ReactRedux.Provider>,
document.querySelector('#root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha256-7nQo8jg3+LLQfXy/aqP5D6XtqDQRODTO18xBdHhQow4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js" integrity="sha256-JuJho1zqwIX4ytqII+qIgEoCrGDVSaM3+Ul7AtHv2zY=" crossorigin="anonymous"></script>
<div id="root"/>
Why closures are relevant to the post
Because of the synchronous nature of the original code, it's possible to say that the reason the admin.user
value that's logged isn't the new value is simply because the component hadn't re-rendered yet. However, the point remains that the admin
value for that render is set in stone and will never change due to a closure. Even if React rendered synchronously with the redux state updating, admin
wouldn't change.
It's for reasons like this that it's important to think about the closures and how everything fully works even in the simpler cases so you don't mess up in the more complicated ones.
For example of how incorrect mental models can hinder you, imagine that you assume that the reason the console.log(admin.user)
doesn't show the correct value in the original example is solely because the component hadn't re-rendered yet. You might assume that putting a timeout to wait for the re-render would let you see the new value.
Imagine trying to add an auto-save functionality where you want to save the current value of admin to localstorage or an API every 10 seconds. You might put an effect with an interval of 10 seconds that logs admin
with an empty dependency array because you want it to not reset until the component unmounts. This is clearly incorrect because it doesn't include the admin
value in the dependencies, but it will highlight that no matter how many times you submit the values and change admin
in the redux state, the value in this effect never changes because this effect will always be running from the initial value of admin
on the first render.
useEffect(()=>{
const intervalId = setInterval(()=>{console.log(admin)},10000);
return ()=>clearInterval(intervalId);
},[])
Closures are the overall issue and you can easily get in trouble if you have the wrong mental model for how things work.
More information on closures:
A closure is the combination of a function bundled together (enclosed)
with references to its surrounding state (the lexical environment). In
other words, a closure gives you access to an outer function’s scope
from an inner function. In JavaScript, closures are created every time
a function is created, at function creation time.
As mentioned in the MDN web docs, every time a function is created, the function gains permanent access to the variables in the surrounding code. Essentially, the functions get access to everything within scope for them.
In react function components, each render creates new functions.
In this example, when App is rendered:
const App = ()=>{
const admin = useSelector(state => state.admin);
const dispatch = useDispatch();
var user,pass = '';
const adminChangeUsername = (event) => {
user = event.target.value;
}
const adminChangePassword = (event) => {
pass = event.target.value;
}
const click= (event) => {
event.preventDefault();
dispatch(adminAction(user,pass));
console.log(store.getState())
console.log(admin.user)
}
}
- the admin variable is initialized into the scope of App based on the current state of the redux store (i.e.
{ user: '', password: ''}
).
- The dispatch variable is initialized to be redux's
store.dispatch
- user is initialized to
undefined
- pass is initialized to
''
- the adminChangeUsername and adminChangePassword functions are initialized. They have access to (and use) user and pass from the closure that they create.
- click is initialized. It has access to (and uses) user, pass, store, and admin from it's closure.
click
always only has access to the admin value from the current re-render when it was created because the click function created a closure around it when it was created. Once the click event occurs, and adminAction is dispatched, click
won't change which render it was from, so it will have access to the admin
variable that was initialized when it was. And each render, the admin
variable is re-initialized and is thus a different variable.
Also the main issue here not just the asynchronous nature but the fact
that state values are used by functions based on their current
closures and state updates will reflect in the next re-render by which
the existing closures are not affected but new ones are created. Now
in the current state the values within hooks are obtained by existing
closures and when a re-render happens the closures are updated based
on whether function is recreated again or not
No matter if you put a timeout in the click handler, the admin
value will never be the new value because of that closure. Even if React happens to re-render immediately after calling dispatch.
You can use a useRef
to create a mutatable value who's current
property will remain the same between re-renders, and at that point, the asynchronous nature of React's re-renders actually comes into play because you'd still have the old value for a few more miliseconds before react re-renders the component.
In this example https://codesandbox.io/s/peaceful-neumann-0t9nw?file=/src/App.js, you can see that the admin value logged in the timeout still has the old value, but the adminRef.current value logged in the timeout has the new value. The reason for that is because admin points to the variable initialized in the previous render, while adminRef.current stays the same variable between re-renders. Example of the logs from the code sandbox below:

The only thing that might really surprise you about the order of events in this is that useSelector
is called with the new data before the re-render occurs. The reason for this is that react-redux calls the function whenever the redux store is changed in order to determine if the component needs to be re-rendered due to the change. It would be entirely possible to set the adminRef.current
property within the useSelector
to always have access to the latest value of admin without store.getState()
.
In this case, it's easy enough to just use store.getState()
to get the current redux state if you need access to the latest state outside of the hook's closure. I often tend to use things like that in longer-running effects because store
doesn't trigger re-renders when changed and can help performance in performance critical locations. Though, it should be used carefully for the same reason of it being able to always get you access to the latest state.
I used a timeout in this example because that's the best way to highlight the actual nature of the closure's effect in this. Imagine if you will that the timeout is some asynchronous data call that takes a bit of time to accomplish.