Understanding Session Management Using React Router V6

Written by antonkalik | Published 2022/09/05
Tech Story Tags: react | react-router | dom | hooks | session-management | context | state-management | sofware-development

TLDRReact does not need any introduction at all, and here I am not going to include a description of what and how it works. I tried to cover in more detail the main aspects of the React application architecture. We have developed exactly the architectural part of building applications using React Router v6. At the end of the article, you can get the GitHub link to the repository. The folder structure contains a bunch of components, constants, and fakes for making API calls. It’s important to understand the state depends on the authorization.via the TL;DR App

React does not need any introduction at all, and here I am not going to include a description of what and how it works. I tried to cover in more detail the main aspects of the React application architecture, which in the future will help you build an easily scalable project very quickly. We have developed exactly the architectural part of building applications using React Router v6.

Also, I am going to use styled-components and at the end of the article, you can get the GitHub link to the repository.

Application Structure

Before starting any programming manipulations, it is better to accurately determine all routes in the application that will be involved. The research is better with some diagrams plan where you can see the whole general picture of your application.

Also, it’s important to be clear about which routes have to be protected. Protection means when the user has access to a specific route only if he has authorization. If you are not authorized, other words are not logged in, then you can’t have that access to that route.

And also a very important part of redirections. For example, if you have the authorization, what has to happen if you will go to the home route? Should you be redirected and where? All of those redirection rules are to be identified at the beginning. Let’s take a look at the diagram with the application routing schema.

Initially, it’s important to divide protected and public routes. Here the green color means public and red is protected. Also, you can find some green boxes with protected paths, which means those protected routes will be with a different logic of redirection.

Redirection has to happen to the first step if you came to the second step of registration without providing dates on the first step.

Composition

Navigation is something common among all routes but depends on the session. The stack of links will be different if the user got authorization. In that way, all routes are better to wrap up to MainLayout where you gonna have the logic of lifecycle and session injection, and, of course, what to show up in header navigation. It’s important to understand the state depends on the authorization.

In our case, all View components except NotFound and Navigation component depends on the session. That means there is going to show Loader before exposing the component in order to be clear on what to render. The folder structure contains a bunch of components, constants, and fakes for making API calls.

application-architecture-boilerplate
    ├── package.json
    └── src
        ├── App.js
        ├── __mocks__
        │         └── db.mock.js
        ├── components
        │         ├── Footer.js
        │         ├── Loading.js
        │         ├── MainLayout.js
        │         ├── Navigation.js
        │         ├── ProtectedRoute.js
        │         └── PublicRoute.js
        ├── constants
        │         └── index.js
        ├── fake
        │         ├── fakeApi.js
        │         └── fakeCache.js
        ├── hooks
        │         └── useSession.js
        └── index.js

Data Request

As you can see that we going to use a fake API for our application. In reality, it can be an Apollo Client or REST API call, but in our case, we going to use a Promise with Timeout for 1s request delay. We have to take care of our authentication for login and logout and request the current session from a fake API. Eventually, it’s a simple class with amount useful methods.

import { db } from 'src/__mocks__/db.mock';
import { fakeCache } from './fakeCache';

class FakeApi {
  static REQUEST_TIME = 1000;
  static KEY_CACHE = 'cache';
  static DB = db;

  context = {};

  constructor() {
    const context = fakeCache.getItem(FakeApi.KEY_CACHE);
    if (context) {
      this.context = context;
    }
  }

  static userById = id => FakeApi.DB.find(user => user.id === id);

  #asyncRequest = callback =>
    new Promise(resolve => {
      setTimeout(() => {
        const result = callback();
        resolve(result);
      }, FakeApi.REQUEST_TIME);
    });

  getSession() {
    return this.#asyncRequest(() => this.context.session);
  }

  login() {
    this.context.session = FakeApi.userById(1);
    fakeCache.setItem(FakeApi.KEY_CACHE, this.context);
    return this.getSession();
  }

  logout() {
    this.context = {};
    fakeCache.clear();
    return this.#asyncRequest(() => null);
  }
}

export const fakeApi = new FakeApi();

You can find out that in the constructor we are using cache. It’s because our request has to have a cache for responses and use the cache as an advance for the next requests to improve performance. This implementation is quite crude and simple, but it’s easy to get the gist of it.

The flow is, that once we call, we have to create a session, and logout clears the session and cache as well. Each asyncRequest should have an REQUEST_TIME as fake delay for our request. But what about the cache?

export const fakeCache = {
  getItem(key) {
    return JSON.parse(localStorage.getItem(key));
  },

  setItem(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  },

  clear() {
    localStorage.clear();
  },
};

For the storing / caching data, we going to use the localStorage. This is just a simple object with methods, nothing else.

Routing

The router part has to care our Private and Public routes. Redirections have to happen from Private when we trying to access /login and when if the user goes to some private route without a session, it has to have the redirection to /login.

"/" // -> Home view depends on session
"/about" // -> About show with session and without
"/login" // -> Login show with without session only
"/signup" // -> Sign Up show without session only
"/forgot-password" // -> Forgot Password show without session only
"/account" // -> User show with session only
"/settings" // -> Settings show with session only
"/posts" // -> Posts and nested routes show with session only
"*" // -> Not Found show without dependencies at all

You can see the comment against each route described the behavior of accessibility. What’s the reason to be in SignUp if you have already a session? I’ve seen many times that issue in other projects. So, in our case, we going to have a redirection and from ProtectedRoute and from PublicRoute. Only NotFoundView should have full access in the end.

<Routes>
  <Route element={<MainLayout />}>
    <Route path="/" element={<HomeView />} />
    <Route path="/about" element={<AboutView />} />
    <Route
      path="/login"
      element={<PublicRoute element={<LoginView />} />}
    />
    <Route
      path="/signup"
      element={<PublicRoute element={<SignUpView />} />}
    />
    <Route
      path="/forgot-password"
      element={<PublicRoute element={<ForgotPasswordView />} />}
    />
    <Route
      path="/account"
      element={
        <ProtectedRoute
          element={<ProtectedRoute element={<UserView />} />}
        />
      }
    />
    <Route
      path="/settings"
      element={<ProtectedRoute element={<SettingsView />} />}
    />
    <Route path="/posts" element={<ProtectedRoute />}>
      <Route index element={<PostsView />} />
      <Route path=":uuid" element={<PostView />} />
    </Route>
  </Route>
  <Route path="*" element={<NotFoundView />} />
</Routes>

As you can see that we added protection for both flows. ProtectedRoute going to navigate to /login in the case when no session and PublicRoute will redirect to /, because  HomeView has to check for authorization.

const HomeView = () => {
  const session = useOutletContext();
  return session.data ? <ListsPostsView /> : <LandingView />;
};

The session possible to get the right form useOutletContext() it’s because MainLayout will provide that context.

Main Layout

Everything is wrapped in MainLayout which going to provide the context of the session and other global-related data. For MainLayout we going to use the common Route and under Outlet will expose all routes. Let’s take a look at the setup.

const MainLayout = ({ navigate }) => {
  const session = useSession();

  return (
    <StyledMainLayout>
      {!session.loading ? (
        <div>
          <Navigation session={session} navigate={navigate} />
          <StyledContainer isLoggedIn={!!session.data}>
            <Outlet context={session} />
          </StyledContainer>
        </div>
      ) : (
        <Loading />
      )}
      <Footer />
    </StyledMainLayout>
  );
};

The Footer has no dependencies of state and we going to render it all the time. But Navigation and all nested routes have to have access the session. Here is the Outlet which render the child route elements and where to pass the contextto all children.

The request which provides us with the session data has a delayed response and in that case, we show the Loading component.

Session

When the application is mounted, we must request the current session. The useSession hook will fire on the mount and get the session either from the cache or from the API.

export const useSession = () => {
  const cache = fakeCache.getItem(SESSION_KEY);
  const [data, setData] = useState(cache);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!cache) {
      setLoading(true);
    }
    fakeApi
      .getSession()
      .then(session => {
        if (session) {
          setData(session);
          fakeCache.setItem(SESSION_KEY, session);
        } else {
          setData(null);
          fakeCache.clear();
        }
      })
      .finally(() => {
        setLoading(false);
      });
  }, []);

  return { data, setData, loading };
};

Every FakeApi request we are storing the response to cache, like cookies stored in real websites. Then it’s time to show depends on session the Navigation with Login / Logout and child components.

Navigation

The state for Navigation is important to enable or disable buttons during the current request. If to hit Logout better to disable all buttons in order to prevent other operations during session clearance.

export const Navigation = ({ navigate, session }) => {
  const [isLoading, setLoading] = useState(false);

  const onLogin = async () => {
    setLoading(true);
    const sessionData = await fakeApi.login();
    session.setData(sessionData);
    fakeCache.setItem(SESSION_KEY, sessionData);
    setLoading(false);
    navigate('/');
  };

  const onLogout = async () => {
    setLoading(true);
    fakeCache.clear();
    await fakeApi.logout();
    session.setData(null);
    setLoading(false);
    navigate('/');
  };

  return (
    <StyledNavigation>
      <Link to="/">Home</Link>
      {session.data ? (
        <div>
          <Link to="/posts">Posts</Link>
          <Link to="/settings">Settings</Link>
          <Link to="/account">My Profile</Link>
          <button disabled={isLoading} onClick={onLogout}>
            {isLoading ? 'loading...' : 'Logout'}
          </button>
        </div>
      ) : (
        <div>
          <Link to="/about">About</Link>
          <Link to="/signup">Sign Up</Link>
          <button disabled={isLoading} onClick={onLogin}>
            {isLoading ? 'loading...' : 'Login'}
          </button>
        </div>
      )}
    </StyledNavigation>
  );
};

Private and Public

It is clear that the routing should be protected depending on the session, but how do get the context in each Route Component? We will still come to the aid of the context useOutletContext() provided by react-router API.

export const ProtectedRoute = ({ element }) => {
  const session = useOutletContext();
  return session.data ? (
    element || <Outlet />
  ) : (
    <Navigate to="/login" replace />
  );
};

For the PublicRoute everything is almost the same but another way around with a different redirection route.

export const PublicRoute = ({ element }) => {
  const session = useOutletContext();
  return session.data ? <Navigate to="/" replace /> : element || <Outlet />;
};

Possibly you can see, that better to have something like a SmartRoute where is better to provide only the route of redirection and prop to identify is it a public or private. I prefer to separate such logic for future scalability. And this is pretty much that’s it.

Conclustion

React Router is the most popular routing solution for React applications and provides the most obvious and clear API for developers today. Since amazing updates in the last version building a routing architecture has become much easier and more convenient. Hope this structure of the application aims to help quickly to build up the body of future React projects. Thank you.

GitHub Repository: architecture-application-boilerplate


Written by antonkalik | Senior Software Engineer at CoverWallet, an Aon company / React and Node JS Developer
Published by HackerNoon on 2022/09/05