javascriptreactjsreact-router

Conditional rendering not working in route component


I'm trying to route to a component based on a condition and it does it, but only if I refresh the page. I've had to add forceUpdate() to get it to work, but that is killing performance. Maybe this is something that would stand out easily for you guys, but for me it is escaping me.

The following route is the one in question:

<Route element={<RequireAuth />}>
  <Route
    path="/"
    element={userrole === "Browser" ? <ViewOnly /> : <InvoiceList2 />}
  />
</Route>

Full code:

import React, { useEffect, useState, useReducer } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import {
  useFetchVendorsQuery,
  useFetchInvoicesQuery,
  useFetchTrackersQuery,
} from "../src/features/Invoices/InvoiceSlice";

const ROLES = {
  User: "user",
  Admin: "Admin",
};

const Wildcard = () => {
  <Navigate to="/login" />;
};

const App = () => {
  // const auth = useAuth();
  // let { user, role, userid } = auth.auth;

  const GetVendors = useFetchVendorsQuery();
  const [vendors, setVendors] = useState([]);

  const GetTrackers = useFetchTrackersQuery();
  const [trackers, setTrackers] = useState([]);

  const GetInvoices = useFetchInvoicesQuery();
  const [invoices, setInvoices] = useState([]);

  const [userrole, setUserRole] = useState("");

  const User = JSON.parse(localStorage.getItem("user"));

  const [ignored, forceUpdate] = useReducer((x) => x + 1, 0);

  const handleOnload = () => {
    forceUpdate();
  };

  useEffect(() => {
    // setVendors(GetVendors);
    // setInvoices(GetInvoices);
    // setTrackers(GetTrackers);
    setUserRole(User?.role);
    forceUpdate();
  }, [userrole, ignored]);

  return (
    <div className="content">
      <BrowserRouter>
        {/* <Header /> */}
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/register" element={<Registration />} />
          <Route element={<RequireAuth />}>
            {/* <Route path="/" element={<InvoiceList2 />} /> */}
            <Route
              path="/"
              element={userrole === "Browser"
                ? <ViewOnly />
                : <InvoiceList2 />
              }
            />
          </Route>
          <Route element={<RequireAuth />}>
            <Route path="/admin" element={<Admin />} />
          </Route>
          <Route element={<RequireAuth />}>
            <Route path="/manual" element={<ManualEntry />} />
          </Route>
          <Route path="/*" element={Wildcard} />
        </Routes>
        <Footer1 />
      </BrowserRouter>
    </div>
  );
};

export default App;

Solution

  • Issue

    I suspect the issue is that users are authenticating and this only updates the "user" value in localStorage and nothing else. LocalStorage is non-reactive.

    The issues is basically these lines of code:

    const [userrole, setUserRole] = useState("");
    
    const User = JSON.parse(localStorage.getItem("user"));
    
    useEffect(() => {
      // setVendors(GetVendors);
      // setInvoices(GetInvoices);
      // setTrackers(GetTrackers);
      setUserRole(User?.role);
      forceUpdate();
    }, [userrole, ignored]);
    

    The App component mounts and:

    1. Initializes userrole to ""
    2. Reads "user" value from localStorage and declares a User variable
    3. The useEffect hook runs and updates the userRole state to the current User?.role value, likely null since no user is authenticated

    I suspect the sequence of events goes something along the lines of:

    1. App mounts and the 3 steps above occur
    2. User authenticates and a "user" value is set in localStorage
    3. .... Nothing triggers App to rerender and "see" the updated "user" value in localStorage
    4. You manually reload the page or manually trigger a component rerender to pick up the localStorage value stored into User and the effects runs to update the userrole state

    Solution Suggestion

    Pass the user state updater function down as props to your Login component so that when a user authenticates you update the React state instead of localStorage. If you need to persist that state, then do that in App via a side-effect.

    Example:

    const App = () => {
      ...
    
      // Lazy initialize user state from localStorage
      const [user, setUser] = useState(() => {
        return localStorage.getItem("user")
          ? JSON.parse(localStorage.getItem("user"))
          : null;
      });
    
      // Persist user state changes to localStorage
      useEffect(() => {
        localStorage.setItem("user", JSON.stringify(user));
      }, [user]);
    
      return (
        <div className="content">
          <BrowserRouter>
            ...
            <Routes>
              <Route
                path="/login"
                element={<Login setUser={setUser} />}
              />
              <Route
                path="/register"
                element={<Registration setUser={setUser} />}
              />
              <Route element={<RequireAuth />}>
                <Route
                  path="/"
                  element={user.role === "Browser"
                    ? <ViewOnly />
                    : <InvoiceList2 />
                  }
                />
                <Route path="/admin" element={<Admin />} />
                <Route path="/manual" element={<ManualEntry />} />
              </Route>
              <Route path="*" element={<Navigate to="/login" />} />
            </Routes>
            ...
          </BrowserRouter>
        </div>
      );
    };
    

    The Login and Registration receive setUser as a prop so that when users log in or register you can update the user state in the parent component and trigger a component rerender.