8

React useState() doesn't update value of the variable if called just after setting value.

I read about useEffect(), but don't really know how this will be useful for this particular scenario.

Full code (please open the console tab to see the variable status)

UPDATE

// hook
const [ error, setError ] = useState<boolean>();
const handleSubmit = (e: any): void => {
    e.preventDefault();
    if (email.length < 4) {
      setError(true);
    }
    if (password.length < 5) {
      setError(true);
    }
    console.log(error); // <== still false even after setting it to true
    if (!error) { 
      console.log("validation passed, creating token");
      setToken();
    } else {
      console.log("errors");
    }
  };
Tronpora
  • 105
  • 1
  • 1
  • 8
  • Add some code to your question, and explain what you trying to achieve, saying "doesn't update the value of the variable is called just after setting value" is not enough. Refer to [How to ask a good question](https://stackoverflow.com/help/how-to-ask) – Dennis Vash Jun 03 '19 at 20:14
  • Hi Tronpora, just wrote you an answer, let me know if you have any questions. – Chris Ngo Jun 03 '19 at 21:15

2 Answers2

7

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@address.com"
          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;
Chris Ngo
  • 15,460
  • 3
  • 23
  • 46
3

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.

Danny Buonocore
  • 3,731
  • 3
  • 24
  • 46
  • How do I make use of useEffect? and isn't useReducer a bit too complex for this simple scenario? – Tronpora Jun 03 '19 at 20:45
  • I'm not exactly sure what you're trying to accomplish, but like I said you might be better off with just a variable outside the state. If I'm understanding correctly you're trying to read the state right after setting it. So move the code that is reading the state to the arrow function passed to `useEffect`. Then it won't be called until the state has finished updating. – Danny Buonocore Jun 03 '19 at 20:51
  • what I am trying to achieve is, after form submission I make some validation and if the validation doesn't have any error I call the function setToken(), otherwise I just log a message. As you mentioned the solution would be by using useEffect, but I am not really sure how to implement in this scenario. I tried like the following useEffect(()=> {}, [error]); but didn't work. I've updated the question. – Tronpora Jun 03 '19 at 21:08