Search code examples
reactjsexpressauthenticationjwttoken

Out of sync rendering


I have a problem with the following code that is driving me crazy.

In short, what I wanted to do was:

- Save the JWT token saved in the sessionStorage in a state upon successful login.

- Save a boolean isTokenValid in a state.

- Check via GET request to the /api/validateToken endpoint if the token is valid and set isTokenValid accordingly.

- If the token is valid (isTokenValid = true) --> show dashboard

- If the token is invalid (is TokenValid = false) --> show an error page

  • App.jsx

    import './styles/Reset.css';
    import './styles/App.css';
    import 'bootstrap/dist/css/bootstrap.min.css';
    
    import React from 'react';
    import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
    import axios from 'axios';
    
    import Home from './components/Home';
    import Header from './components/Header';
    import Footer from './components/Footer';
    import Login from './components/Login';
    import Signup from './components/Signup';
    import Dashboard from './components/Dashboard';
    import FailedAuth from './components/FailedAuth';
    
    function App() {
    
      const [token, setToken] = React.useState(sessionStorage.getItem('token'));
      const [isTokenValid, setIsTokenValid] = React.useState(false);
      const [loading, setLoading] = React.useState(true);
    
      async function validateToken() {
        try {
          const response = await axios.get('http://localhost:5000/api/validateToken', { 
            headers: { 
              'Authorization': `Bearer ${token}` 
            } 
          });
    
          if (response.status === 200) {
            setIsTokenValid(true);
          } else {
            setIsTokenValid(false);
          }
        } catch (error) {
          setIsTokenValid(false);
        } finally {
          setLoading(false);
        }
      }
    
      React.useEffect(() => {
        async function fetchData() {
          if (token) {
            await validateToken();
          }
        }
    
        fetchData();
      });
    
      if (loading) {
        return (
          <div className="loading">
            <div className="spinner-border text-primary" role="status">
              <span className="visually-hidden">Loading...</span>
            </div>
          </div>
        );
      }
    
      return (
        <Router>
            <Routes>
              <Route path="/login/*" element={
                <>
                  <Header type="auth" />
                  <Login />
                </>
              }
              />
              <Route path="/signup/*" element={
                <>
                  <Header type="auth" />
                  <Signup />
                </>
              }
              />
              <Route path="/*" element={
                <>
                  <Header type="home" />
                  <Home />
                  <Footer />
                </>
                }
              />
              <Route path="/dashboard/*" element={
                <>
                  {isTokenValid ? <Dashboard /> : <FailedAuth />}
                </>
              }
              />
            </Routes>
        </Router>
      );
    
    }
    
    export default App;
    
  • server.js

    app.get('/api/validateToken', passport.authenticate('jwt', { session: false }), (req, res) => {
        try {
            res.status(200).json({ message: 'Token is valid' });
        } catch (error) {
            res.status(500).json({ error: error.message });
        }
    });
    

The server handles authentication perfectly and the JWT token is assigned correctly.

My problem lies precisely in the rendering of the route conditioned by isTokenValid.

When the user logs in, it is rendered to the dashboard and even if the token is valid, it immediately shows <FailedAuth />, but if I refresh the page it shows <Dashboard />, as it should be.

After the token expires, <FailedAuth /> is always shown, so there's no problem with that, it does what it's supposed to do.

I think it's a synchronization problem, but I'm not sure (I'm still new to React, it's my first project).

Could anyone help me? So that I know how to behave in the future when I find myself faced with the same situation.

This is the part of code when i set sessionStorage:

import React, { useState } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';

import TipsAndUpdatesTwoToneIcon from '@mui/icons-material/TipsAndUpdatesTwoTone';
import Alert from '@mui/material/Alert';

function AuthForm(props) {
    const navigate = useNavigate();
    // Stato che contiene i dati del form
    const [formData, setFormData] = useState({
        email: '',
        username: '',
        password: '',
        confirmPassword: '',
    });

    const [error, setError] = useState('');
    const [show, setShow] = useState(true);

    function handleCloseClick() {
      setShow(false);
    }
    
    // Funzione che gestisce l'invio del form
    async function handleSubmit(event) {
        event.preventDefault();
        setTimeout(() => {
          setShow(true);
        }, 1000);
    
        try {
          let response;
    
          if (props.type === 'login') {
            response = await axios.post('http://localhost:5000/api/login', formData);

            sessionStorage.setItem('token', response.data.token);

            navigate('/dashboard');
          } else if (props.type === 'signup') {
            response = await axios.post('http://localhost:5000/api/signup', formData);
          }

          console.log(response.data);
        } catch (error) {
          console.log(error.response.data);
          if (error.response.data.error) {
            setError(error.response.data.error);
          } else {
            setError(error.response.data.message);
          }
        }

        // Resetta il form dopo l'invio
        setFormData({
          email: '',
          username: '',
          password: '',
          confirmPassword: '',
        });
    };
    
    // Funzione che gestisce i cambiamenti dei campi del form
    function handleChange(event) {
        setFormData({ ...formData, [event.target.name]: event.target.value });
    };
    
    // Renderizza il form
    return (
        <>
          {error && show && (
            <Alert onClose={() => {handleCloseClick()}} severity="error" className='mb-4'>{error}</Alert>
          )}
          <div className="d-flex justify-content-center">
            <h1 className='icon-yellow'>
              {props.type === 'login' ? 'Log In' : 'Sign Up'}{' '}
              <TipsAndUpdatesTwoToneIcon className='icon-yellow' style={{ fontSize: '3.5rem' }}/>
            </h1>
          </div>
          <form className="auth-form" onSubmit={handleSubmit}>
            <div className="form-group">
              <label htmlFor="email">Email</label>
              <input className="form-control" type="email" name="email" value={formData.email} onChange={handleChange} />
            </div>
            {props.type === 'signup' && (
              <div className="form-group">
                <label htmlFor="username">Username</label>
                <input className="form-control" type="text" name="username" value={formData.username} onChange={handleChange} />
              </div>
            )}
            <div className="form-group">
              <label htmlFor="password">Password</label>
              <input className="form-control" type="password" name="password" value={formData.password} onChange={handleChange} />
            </div>
            {props.type === 'signup' && (
              <div className="form-group">
                <label htmlFor="confirmPassword">Confirm Password</label>
                <input className="form-control" type="password" name="confirmPassword" value={formData.confirmPassword} onChange={handleChange} />
              </div>
            )}
            <button className="auth-button" type="submit">
              {props.type === 'login' ? 'Login' : 'Sign Up'}
            </button>
          </form>
        </>
    );
}

export default AuthForm;


Solution

  • Issue

    You haven't set the token state using setToken after you set the token in sessionStorage. sessionStorage.getItem('token') is undefined when the App component mounts. You have mistaken it as that it'll fetch a token from sessionStorage after you set sessionStorage, that's not how react state works. On initial mounting only the state is set to the initial value after that you have to use the setter function to mutate it.

    const [token,setToken] = useState(initialValue)

    You were safe without dependency array cause, setIsTokenValid(true), and setLoading(false) setting to the same thing after their state change. That's why react stops re-rendering infinitely. React re-rendering happens if any state changes are noted.

    Without dependency array->

    1. setCount((prev)=>!prev) -> cause infinite re-render
    2. setCount(true/false) -> no infinite re-render.

    Solution

    Remove sessionStorage.getItem('token') from the initial state of the token.

    const [token, setToken] = React.useState(null);
    

    Add [token] to useEffect dependency array

    When the token state changes, it will trigger re-render thus triggering useEffect, token will validate, and isValidToken further set to true.

    useEffect(() => {
      async function fetchData() {
        if (token) {
          await validateToken();
        }
      }
    
      fetchData();
    }, [token]);