DEV Community

Cover image for Session State Management | JS | React
Shubham Tiwari
Shubham Tiwari

Posted on

Session State Management | JS | React

Introduction

Session management is responsible for storing and maintaining user-specific data across multiple requests or interactions with a web application. In web apps, a "session" refers to the period during which a user is actively interacting with the app, starting when a user logs in and ending when they log out or after a certain period of inactivity.

Types of Session Management

  • Cookies: Small pieces of data stored on the user's browser.

  • Local Storage/Session Storage: Web storage methods to store data in the user's browser.

  • JWT (JSON Web Tokens): A stateless way to handle sessions with encoded tokens.

  • Server-side session stores: Persistent session storage on the server that tracks user sessions.

Session Management with JavaScript and React

Cookies

Cookies are stored in the user's browser and can be set to expire after a specific time. They are commonly used for session management because they can be set with server responses and sent automatically with every request to the server.

// Setting a cookie
document.cookie = "sessionId=9808@#45; expires=Fri, 31 Dec 2024 23:59:59 GMT; path=/login";

// Reading a cookie
const cookies = (cookieValue) => {
   return cookieValue.split('; ').reduce((acc, cookie) => {
    const [key, value] = cookie.split('=');
    acc[key] = value;
    return acc;
   }, {});
}

const cookieObj = cookies(cookie)

console.log(cookieObj.sessionId) // 9808@#45
console.log(cookieObj.expires) // Fri, 31 Dec 2024 23:59:59 GMT
console.log(cookieObj.path) // /login
Enter fullscreen mode Exit fullscreen mode

Local and Session Storage

  • localStorage persists data across browser sessions.

  • sessionStorage only persists data for the duration of the page session (until the tab or window is closed).

// React Example
import React from "react";
import { useState } from "react";
import { useEffect } from "react";

const Hero = () => {
  // Accessing theme from localStorage for initial state
  const [theme, setTheme] = useState(
    localStorage.getItem("theme") === "dark" ? "dark" : "light"
  );

  useEffect(() => {
    // Updating local storage if theme state is changed
    localStorage.setItem("theme", theme);
  }, [theme]);

  // Toggling theme between dark and light
  const toggleTheme = () => {
    setTheme(theme === "dark" ? "light" : "dark");
  };

  return (
    <section className={`${theme === "dark" ? "bg-gray-900" : ""}  text-white`}>
      <div className="mx-auto px-4 py-32 lg:flex lg:h-screen lg:items-center lg:justify-between max-w-[1200px]">
        <h1>Theme Change Example</h1>
        <button
          className="fixed top-4 right-4 rounded bg-blue-600 p-2 text-white hover:bg-transparent hover:text-white focus:outline-none focus:ring"
          onClick={toggleTheme}
        >
          {theme === "dark" ? " " : " "}
        </button>
      </div>
    </section>
  );
};

export default Hero;
Enter fullscreen mode Exit fullscreen mode

JWT Authentication

JWT authentication is a token-based stateless authentication mechanism.

// Manually setting tokens for login
const login = async (username, password) => {
  const response = await fetch('https://example.com/api/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ username, password }),
  });

  const data = await response.json();
  localStorage.setItem('token', data.token);
};

// Checking tokens with Authorization headers
const getProtectedData = async () => {
  const token = localStorage.getItem('token');
  const response = await fetch('https://example.com/api/protected', {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`,
    },
  });

  const protectedData = await response.json();
  return protectedData
};
Enter fullscreen mode Exit fullscreen mode
  • On login, the server generates a JWT and sends it to the client.

  • The client stores the JWT in localStorage or sessionStorage.

  • On each request to a protected resource, the client sends the JWT in the headers for authentication.

  • Resource - https://www.npmjs.com/package/react-jwt

Session Management in Next.js

Session management in Next.js can be more robust due to the combination of client and server contexts. Common ways to manage sessions in Next.js include using cookies, JWTs, and third-party authentication libraries. We have already saw Cookies and JWT approach and will implement third party authentication using next-auth, which is a popular authentication library for Next.js that simplifies session management.

  • Session storage: Sessions can be stored in JWTs (for stateless authentication) or server-side databases.

  • Automatic session handling: Next-auth manages session expiration, renewal, and session data automatically.

Installation

npm install next-auth
Enter fullscreen mode Exit fullscreen mode

Adding Github auth provider

import NextAuth from "next-auth"
import GithubProvider from "next-auth/providers/github"

export const authOptions = {
  // Configure one or more authentication providers
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
}

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }
Enter fullscreen mode Exit fullscreen mode

Creating a Session provider for the entire app

"use client";
import { SessionProvider } from "next-auth/react"
import React from "react";

const SessionWrapper = ({children}:{children:React.ReactNode}) => {
    return <SessionProvider>{children}</SessionProvider>
}
export default SessionWrapper
Enter fullscreen mode Exit fullscreen mode

Wrapping the Layout with SessionWrapper

import "./globals.css";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import SessionWrapper from "@/components/SessionWrapper";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <SessionWrapper>
          <Navbar />
            <main>{children}</main>
          <Footer />
        </SessionWrapper>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Accessing the Session data and status

import { useSession } from "next-auth/react"

export default function Component() {
  const { data: session, status } = useSession()

  if (status === "authenticated") {
    return <p>Signed in as {session.user.email}</p>
  }

  return <a href="/api/auth/signin">Sign in</a>
}
Enter fullscreen mode Exit fullscreen mode
  • data: This can be three values: Session / undefined / null.

  • When the session hasn't been fetched yet, data will be undefined

  • In case it failed to retrieve the session, data will be null

  • In case of success, data will be Session.

  • status: enum mapping to three possible session states: "loading" | "authenticated" | "unauthenticated"

State Hydration

  • Hydration is the process where the server-rendered HTML is "reused" by React on the client, and React attaches its JavaScript logic (e.g., event listeners, state management) to the existing HTML.

  • In Next.js, state hydration is essential because when a page is server-rendered, it’s pre-rendered with HTML and some initial state, but once the browser takes over, the client-side React app must take that server-generated HTML and "hydrate" it by attaching event listeners, reinitializing state, and ensuring that the client-side app behaves correctly with the pre-rendered HTML.

  • Hydration could also occur if some process is dependent on an asynchronous state.

  • Here is an example of it

"use client";
import ProfileCard from "./ProfileCard";
import BlogsFetch from "./BlogsFetch";
import { useMarkdownStore } from "@/store/useStore";

const ProfileSection = () => {
  const user = useMarkdownStore((state) => state.user);

  return (
    <div className="flex flex-col lg:flex-row justify-center lg:justify-start gap-5 lg:gap-10 px-5">
      <ProfileCard user={user} />
      <BlogsFetch userId={user.uid as string} />
    </div>
  );
};

export default ProfileSection;
Enter fullscreen mode Exit fullscreen mode
  • The user variable refers to a user state from zustand, which is a state management library. This state is async and is used by the BlogsFetch component which use this user.uid to perform data fetching but initially when the component mounts, user returns null and the data fetching returns an emptry array as the user state is not hydrated immediately.

  • To fix this issue, we need to wait for the user state to get hydrated with some value and then perform the data fetching

const queryInfinite = useInfiniteQuery({
  queryKey: userId ? ["blogs", userId] : ["blogs"],
  queryFn: () =>
    userId
      ? getUserBlogsFromDb(userId, lastDoc)
      : getBlogsFromDb(filters, lastDoc),
  initialPageParam: undefined,
  getPreviousPageParam: (firstPage) => firstPage.firstVisibleDoc,
  getNextPageParam: (lastPage) => lastPage.lastVisibleDoc,
  enabled: userId ? true : false,
});
Enter fullscreen mode Exit fullscreen mode
  • We are using react query to handle the data fetching and mutations, it has 1 property called enabled which fetch the data once the userId state is ready and hydrated. Alternatively, we could use useEffect hook to refetch once the userId is ready to perform the data fetching operation.

Resources

https://next-auth.js.org/getting-started/introduction

https://tanstack.com/query/latest

https://swr.vercel.app/

https://github.com/pmndrs/zustand

That's it for this post
You can contact me on -

Instagram - https://www.instagram.com/supremacism__shubh/
LinkedIn - https://www.linkedin.com/in/shubham-tiwari-b7544b193/
Email - shubhmtiwri00@gmail.com

You can help me with some donation at the link below Thank youπŸ‘‡πŸ‘‡
https://www.buymeacoffee.com/waaduheck

Also check these posts as well

Top comments (0)