Search code examples
javascriptreactjsreduxredux-toolkit

How to persist current page during relaod


I have a React application with a home or landing page and a dashboard, I want the dashboard components to persist after the page reloads, but whenever I refresh, it navigates back to my home page

AuthSlice

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import authService from './authService';
import { jwtDecode } from 'jwt-decode';
 
const initialState = {
  isAuthenticated: false,
  data:{
    user: {} 
  } ,
  loading: false,
  isError: false,
  isSuccess: false,
  isLoading: false,
  message: '',
  getcpd: null,
  employeeCode: { portalCode: '' },
  errors: null,
};

console.log('initial-redux state : ',initialState.user)

// Async thunk to initialize authentication state during application startup
export const initializeAuthState = createAsyncThunk(
  'auth/initializeAuthState',
  async () => {
    console.log('checking local storage...');
    const storedToken = localStorage.getItem('jwtToken');

    if (storedToken) {
      console.log('token present in local storage!');
      const decoded = jwtDecode(storedToken);

      try {
        const response = await authService.retrieveUserData(decoded.id);
        console.log('userData retrieved during initialization', response);

        // Return the payload to be used in extraReducers
        return {
          decoded,
          userData: response.data,
        };
      } catch (error) {
        console.error('Error during authentication state initialization:', error);
        throw error; // Propagate the error
      }
    } else {
      console.log('No token in local storage!');
      return null; // Return null if no token is present
    }
  }
);
 
export const register = createAsyncThunk(
  'auth/register',
  async (userData, { rejectWithValue }) => {
    try {
      await authService.register(userData);
      return { success: true };
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Login user
export const login = createAsyncThunk(
  'auth/login',
  async (user, thunkAPI) => {
    try {
      const response = await authService.login(user);
      console.log('login response from service', response);

      if (response.token) {
        // Use jwtDecode directly
        const decoded = jwtDecode(response.token);
        console.log('decoded:', decoded);

        localStorage.setItem('jwtToken', response.token);
        authService.setAuthToken(response.token);

        // Dispatch the setCurrentUser action using thunkAPI
        thunkAPI.dispatch(setCurrentUser({
          ...decoded, // Include the decoded token properties
          ...response.data, // Include additional user information from response.data
        }));

        // Return the response data
        return response;
      } else {
        console.log('Token is not available in the response data.');
        // Handle the absence of token, you may want to throw an error or handle it as needed
        throw new Error('Token is not available in the response data.');
      }
    } catch (error) {
      // Handle the error and return a rejected value using rejectWithValue
      const message =
        (error.response && error.response.data && error.response.data.message) ||
        error.message ||
        error.toString();

      return thunkAPI.rejectWithValue(message);
    }
  }
); 

export const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    setCurrentUser: (state, action) => {
      console.log('setcurrentUser payload:', action.payload)
      state.isAuthenticated = !!Object.keys(action.payload).length;
      state.user = action.payload;
    },
    setUserLoading: (state) => {
      state.loading = true;
    },
    resetErrors: (state) => {
      state.errors = null;
    },
  },
  extraReducers: (builder) => {
    builder
    .addCase(initializeAuthState.fulfilled, (state, action) => {
      state.isAuthenticated = true;
      state.data.user = {
        ...action.payload.decoded,
        ...action.payload.userData,
      };
    })
    .addCase(initializeAuthState.rejected, (state, action) => {
      console.error('Error during authentication state initialization:', action.error);
    })
    .addCase(login.fulfilled, (state) => {
      state.loading = false;
    })
    .addCase(login.rejected, (state, action) => {
      state.loading = false;
      state.errors = action.payload;
    })
  },
});

export const { setCurrentUser, setUserLoading, resetErrors } =
  authSlice.actions;
export default authSlice.reducer;

Then I am importing initializeAuthState in app.jsx

export default function App() {
  const dispatch = useDispatch();

  //Call initializeAuthState when the app starts
  useEffect(() => {
    dispatch(initializeAuthState());
  });

  useScrollToTop();

  return (
    <ThemeProvider>
      <ErrorBoundary>
        <Router />
      </ErrorBoundary>
    </ThemeProvider>
  );
}

My handleSubmit function for login

const handleSubmitLogin = async () => {
  // Dispatch the login action
  const actionResult = await dispatch(login(userLogin));
  
  // Check if the login action was successful
  if (login.fulfilled.match(actionResult)) {
    console.log('isAuthenticated:', isAuthenticated); // Add this line
    console.log('user:', user); // Add this line
  
    // If already authenticated, stay on the current page
    if (isAuthenticated) {
      console.log('Already authenticated, not navigating.'); // Add this line
      return;
    }
  
    // If not authenticated, navigate to the home page
    router.push('/home');
  }
};

and lastly my Route file handler

export default function Router() {
  // const { isSuccess } = useSelector((state) => state.auth);
  const storedIsSuccess = localStorage.getItem('isSuccess') === 'true';
  const { isAuthenticated  } = useSelector((state) => state.auth);
  const { user  } = useSelector((state) => state.auth );
  const location = useLocation();
  const isReload = location.state && location.state.isReload;

  useEffect(() => {
    if (isAuthenticated && isReload) {
      // Prevent navigation away from the current page during refresh
      return;
    }
  }, [isAuthenticated, isReload]);

  console.log(isAuthenticated && 'user authenticated');
  console.log('user before login starts',user);

  return (
    <Routes>
      <Route
        path="/"
        element={
          (user && isAuthenticated) ? (
            <DashboardLayout>
           
            <DashboardLayout>
              <Suspense>
                <Outlet />
              </Suspense>
            </DashboardLayout>
          ) : (
            <HomePage />
          )
        }
      >

I want user to be able to refresh a page and it doesn't return to home page.


Solution

  • Issue

    There are no navigation actions occurring in the app. The initial isAuthenticated state is false so when the page loads and the app mounts, Router conditionally renders the HomePage on the only route that exists, "/" on the initial render cycle.

    Solution

    Start from an undefined initial isAuthenticated state value and conditionally null or some loading indicator while the initializeAuthState action processes and sets the isAuthenticated state based on the stored JWT token.

    const initialState = {
      isAuthenticated: undefined, // <-- we don't initially know auth state 🤷🏻‍♂️
      data: {
        user: {} 
      } ,
      loading: false,
      isError: false,
      isSuccess: false,
      isLoading: false,
      message: '',
      getcpd: null,
      employeeCode: { portalCode: '' },
      errors: null,
    };
    

    Update the initializeAuthState action to catch thrown errors and rejected Promises and return a rejected value from the Thunk.

    export const initializeAuthState = createAsyncThunk(
      'auth/initializeAuthState',
      async (_, thunkApi) => {
        try {
          const storedToken = localStorage.getItem('jwtToken');
          const decoded = jwtDecode(storedToken);
          const { data } = await authService.retrieveUserData(decoded.id);
    
          return {
            decoded,
            userData: data,
          };
        } catch (error) {
          return thunkApi.rejectWithValue(error);
        }
      }
    );
    

    Update the auth slice reducers to handle both token check success/failure.

    ...
    .addCase(initializeAuthState.fulfilled, (state, action) => {
      state.isAuthenticated = true;
      state.data.user = action.payload;
      state.isError = false;
      state.errors = null;
    })
    .addCase(initializeAuthState.rejected, (state, action) => {
      state.isAuthenticated = false;
      state.data.user = {};
      state.isError = true;
      state.errors = action.payload;
    })
    ...
    

    I suggest creating home route layout route component to abstract the auth check and conditional rendering of page content.

    const Home = () => {
      const { isAuthenticated  } = useSelector((state) => state.auth);
    
      if (isAuthenticated === undefined) {
        return null; // <-- or loading indicator/spinner/etc
      }
    
      return isAuthenticated
        ? (
          <DashboardLayout>
            <Suspense>
              <Outlet />
            </Suspense>
          </DashboardLayout>
        ) : <HomePage />;
    };
    
    export default function Router() {
      ....
    
      return (
        <Routes>
          <Route path="/" element={<Home />}>
            ....
          </Route>
        </Routes>
      );
    }