React useState() doesn't update value synchronously
Let's assume the user does not have valid credentials. The problem is here:
if (email.length < 4) { // <== this gets executed
setError(true);
}
if (password.length < 5) { // <== this gets executed
setError(true);
}
console.log(error); // <== still false even after setting it to true
if (!error) { // <== this check runs before setError(true) is complete. error is still false.
console.log("validation passed, creating token");
setToken();
} else {
console.log("errors");
}
You are using multiple if-checks that all run independently, instead of using a single one. Your code executes all if-checks. In one check, you call setError(true)
when one of the conditions is passed, but setError()
is asynchronous. The action does not complete before the next if-check is called, which is why it gives the appearance that your value was never saved.
You can do this more cleanly with a combination of if-else and useEffect instead: https://codesandbox.io/s/dazzling-pascal-78gqp
import * as React from "react";
const Login: React.FC = (props: any) => {
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [error, setError] = React.useState(null);
const handleEmailChange = (e: any): void => {
const { value } = e.target;
setEmail(value);
};
const handlePasswordChange = (e: any): void => {
const { value } = e.target;
setPassword(value);
};
const handleSubmit = (e: any): void => {
e.preventDefault();
if (email.length < 4 || password.length < 5) {
setError(true);
} else {
setError(false);
}
};
const setToken = () => {
//token logic goes here
console.log("setting token");
};
React.useEffect(() => {
if (error === false) {
setToken();
}
}, [error]); // <== will run when error value is changed.
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="[email protected]"
onChange={handleEmailChange}
/>
<br />
<input
type="password"
placeholder="password"
onChange={handlePasswordChange}
/>
<br />
<input type="submit" value="submit" />
</form>
{error ? <h1>error true</h1> : <h1>error false</h1>}
</div>
);
};
export default Login;
Just like setState
, useState
is asynchronous and tends to batch updates together in an attempt to be more performant. You're on the right track with useEffect
, which would allow you to effectively perform a callback after the state is updated.
Example from the docs:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Although it is also recommended that if you need the updated value as soon as an update to the state has been requested, you're likely better off with just a variable in the component.
More on using state synchronously
And if you're familiar with Redux's reducers, you could use useReducer
as another alternative. From the docs:
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.