reactjsauthenticationreact-hooksreact-router-domzustand

Authentication in react with react-router-v6 and zustand


AppRoutes:

function AppRoutes(): JSX.Element {
  return (
    <Routes>
      <Route path="/" element={<Auth />} />

      <Route element={<RequiredAuth />}>
        <Route path="/home" element={<Home />} />
      </Route>
    </Routes>
  );
}

private routes checks

import { Navigate, Outlet } from "react-router-dom";
import useAuth from "./hooks/useAuth";

const RequiredAuth = () => {
  const { user } = useAuth();

  console.log("USERAUTH", user);
  return user ? <Outlet /> : <Navigate to="/" replace />;
};
export default RequiredAuth;

useAuth Hook with zustand store

import { create } from "zustand";
import axios, { AxiosResponse } from "axios";
import apiSecure, { api } from "../libs/axios";

type user = {
  id: string;
};

interface useAuthStore {
  user: user | null;
  signIn: (username: string, password: string) => Promise<void>;
  fetchCurrentUser: () => Promise<void>;
}
interface AuthResponse {
  code: number;
  error: boolean;
  message: string;
  data: {
    email: string;
    id: string;
    name: string;
    token: string;
    username: string;
  };
  errors?: [];
}

const useAuth = create<useAuthStore>((set) => ({
  user: null,
  signIn: async (username: string, password: string): Promise<void> => {
    // login process
  },
  fetchCurrentUser: async (): Promise<void> => {
    try {
      const response: AxiosResponse<AuthResponse> = await apiSecure.get(
        `/auth/currentuser`
      );

      const userData = response.data.data;

      set({ user: { id: userData.id } });
      console.log("SETUP", { id: userData.id });
    } catch (error: unknown) {
      // Handle authentication errors
      
    }
  },
}));

export default useAuth;
function App(): JSX.Element {
  const { fetchCurrentUser, user } = useAuth();
  console.log(user);

  useEffect(() => {
    async function auth() {
      await fetchCurrentUser();
    }
    auth();
  }, [fetchCurrentUser]);

  // useEffect(() => {}, [fetchCurrentUser])
  return (
    <>
      <AppRoutes />
    </>
  );
}

export default App;

I'm trying to build authentication with react-router-v6 and have used zustand for state management and verify user using jwt token for which calling fetchcurrentuser (/auth/currentuser api) and updating user state in useAuth with zustand, and protected routes /home with RequiredAuth but it's accessible even if user verified and state updated successfully, or can say updated state with zustand is updated in requiredAuth function.

it's always redirect / what's wrong i am doing?

Please help, I'm relative new to react and this would be much appreciated. Thanks in advance!


Solution

  • The user state starts off already in the "unauthenticated" condition, so on any initial rendering of the component the redirect to "/" will be effected.

    With route protection and authentication schemes like this you effectively have 3 "states":

    It's this third state that is important. You should start from the unknown state and wait until authentication confirmation to switch to either of the other two confirmed states.

    Example:

    interface useAuthStore {
      user?: user | null; // <-- optional to allow undefined
    
      signIn: (username: string, password: string) => Promise<void>;
      fetchCurrentUser: () => Promise<void>;
    }
    
    const useAuth = create<useAuthStore>((set) => ({
      user: undefined, // <-- initially "unknown"
    
      signIn: async (username: string, password: string): Promise<void> => {
        // login process
      },
      fetchCurrentUser: async (): Promise<void> => {
        try {
          const response: AxiosResponse<AuthResponse> = await apiSecure.get(
            `/auth/currentuser`
          );
    
          const { data } = response.data;
    
          if (data.id) {
            set({ user: { id: data.id } });
            console.log("SETUP", { id: data.id });
          } else {
            set({ user: null }); // <-- no id, set null
          }
        } catch (error: unknown) {
          // Handle authentication errors
          set({ user: null }); // <-- auth failed, set null
        }
      },
    }));
    
    const RequiredAuth = () => {
      const { user } = useAuth();
    
      if (user === undefined) {
        return null; // <-- or loading indicator/spinner/etc
      }
    
      return user ? <Outlet /> : <Navigate to="/" replace />;
    };