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.