skip to Main Content
const AUTH_DISABLED = 0;
const AUTH_LOGIN = 1;
const AUTH_LOGOUT = 2;

function App() {
  const [showLoginPopup, setShowLoginPopup] = useState(false);
  const [authButtonState, setAuthButtonState] = useState(AUTH_DISABLED);

  const handleClick = () => {
    if ( authButtonState == AUTH_LOGOUT ) {
      authGlobal.logout().then( () => { setAuthButtonState(AUTH_LOGIN) } );
    } else {
      setShowLoginPopup(true)
    }
  };

    if (authButtonState === AUTH_DISABLED) {
      authGlobal.tryAuthentication().then((loggedIn) => {
        setAuthButtonState(loggedIn ? AUTH_LOGOUT : AUTH_LOGIN);
      });
    } 

  return (
    <div className="main-container">
      <div> Display </div>
      <div className='main-controls'>
        <button type='button' onClick={handleClick} disabled = {authButtonState == AUTH_DISABLED}>Log {authButtonState == AUTH_LOGOUT ? "Out" : "In"} </button>
        <LoginPopup isVisible={showLoginPopup}/>
      </div>
    </div>
  )
}

In code above tryAuthentication is async and has 3 paths: credentials are in local storage, credentials are in URL (just logged in with external app) or there is nothing to log in with.

My problem is that, in second case, button doesn’t change to "Log Out". State is never updated but then() calls setAuthButtonState() correctly.

Putting that branch in useEffect() helps but I don’t know why.
Shouldn’t then() queue another rerender with new state without UseEffect()?

3

Answers


  1. I’ve refactor your component for better handling of the asynchronous authentication flow.

    import React, { useState, useEffect } from 'react';
    
    const AUTH_DISABLED = 0;
    const AUTH_LOGIN = 1;
    const AUTH_LOGOUT = 2;
    
    function App() {
      const [showLoginPopup, setShowLoginPopup] = useState(false);
      const [authButtonState, setAuthButtonState] = useState(AUTH_DISABLED);
    
      useEffect(() => {
        // This effect runs only once after the component mounts
        authGlobal.tryAuthentication().then((loggedIn) => {
          setAuthButtonState(loggedIn ? AUTH_LOGOUT : AUTH_LOGIN);
        });
      }, []); // Empty dependency array means it runs once after mount
    
      const handleClick = () => {
        if (authButtonState === AUTH_LOGOUT) {
          authGlobal.logout().then(() => {
            setAuthButtonState(AUTH_LOGIN);
            setShowLoginPopup(false); // Hide popup on logout
          });
        } else {
          setShowLoginPopup(true);
        }
      };
    
      return (
        <div className="main-container">
          <div>Display</div>
          <div className="main-controls">
            <button 
              type="button" 
              onClick={handleClick} 
              disabled={authButtonState === AUTH_DISABLED}
            >
              Log {authButtonState === AUTH_LOGOUT ? "Out" : "In"}
            </button>
            {showLoginPopup && <LoginPopup />}
          </div>
        </div>
      );
    }
    
    

    So, in summary, while it might seem like the then() should queue another re-render with the new state without useEffect, in practice, the asynchronous nature of your tryAuthentication function and the way React batches state updates can lead to the issue you observed. Using useEffect provides a more controlled and predictable way to handle side effects and asynchronous operations in functional components.

    Login or Signup to reply.
  2. In React, state updates are asynchronous, and the component may not re-render immediately after calling setAuthButtonState. The then callback in your tryAuthentication function is likely resolving outside of the React rendering cycle, which means React doesn’t immediately recognize the state change.

    Here’s a modified version of your code

    const AUTH_DISABLED = 0;
    const AUTH_LOGIN = 1;
    const AUTH_LOGOUT = 2;
    
    function App() {
      const [showLoginPopup, setShowLoginPopup] = useState(false);
      const [authButtonState, setAuthButtonState] = useState(AUTH_DISABLED);
    
      const handleClick = () => {
        if (authButtonState === AUTH_LOGOUT) {
          authGlobal.logout().then(() => {
            setAuthButtonState(AUTH_LOGIN);
          });
        } else {
          setShowLoginPopup(true);
        }
      };
    
      useEffect(() => {
        if (authButtonState === AUTH_DISABLED) {
          authGlobal.tryAuthentication().then((loggedIn) => {
            setAuthButtonState(loggedIn ? AUTH_LOGOUT : AUTH_LOGIN);
          });
        }
      }, [authButtonState]); // Run the effect whenever authButtonState changes
    
      return (
        <div className="main-container">
          <div> Display </div>
          <div className="main-controls">
            <button type="button" onClick={handleClick} disabled={authButtonState === AUTH_DISABLED}>
              Log {authButtonState === AUTH_LOGOUT ? "Out" : "In"}
            </button>
            <LoginPopup isVisible={showLoginPopup} />
          </div>
        </div>
      );
    }
    
    export default App;
    
    Login or Signup to reply.
  3. You are triggering a side effect on render. Side effects on render need to be done inside a useEffect hook: https://dev.to/hellonehha/what-is-side-effect-in-reactjs-and-how-to-handle-it-39j8

    See the official documentation about this too.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search