Improving User Experience with Next Query Param in React

Improving User Experience with Next Query Param in React

Introduction

Imagine you're trying to access a protected page on a website without logging in. What usually happens? You're redirected to the login page. However, after logging in, you may find yourself on a homepage or some other default page, not the page you originally wanted. This experience can be frustrating for users. Thankfully, with the use of a next query parameter, we can drastically improve this experience by redirecting users to their intended page after they log in.

In this blog post, we will explore how to implement the next query parameter in a React application to make sure users are redirected to the correct page after authentication. We'll walk through code examples, routing setups, and explain the logic behind this implementation.

Note: This blog won't cover the full-fledged implementation of authentication systems, but rather focus on solving the problem of maintaining intended redirection using the next query parameter.

Problem Statement

Users often encounter this situation: they try to access a protected page without being logged in, and instead of being redirected to that page after login, they get sent to a default page (like the homepage). This results in poor user experience because the user has to navigate back manually to the desired page.

Solution: Next Query Param

The next query parameter is a simple yet effective solution. When a user is redirected to the login page, the next parameter is appended to the URL, storing the path of the page the user was trying to access. After the user successfully logs in, they are redirected to the page specified by the next parameter.

Routing Setup

We start by defining the routing setup for our application, which includes both protected and unprotected routes.

import {
  createBrowserRouter,
  Outlet,
  type RouteObject,
} from "react-router-dom";

import AuthPage from "./pages/auth";
import LoginPage from "./pages/auth/LoginPage";
import NotFound from "./pages/NotFoundPage";
import ProtectedPage from "./pages/protected";
import HomePage from "./pages/protected/HomePage";
import ProfilePage from "./pages/protected/ProfilePage";

const authRoutes: RouteObject[] = [
  {
    element: <AuthPage />,
    children: [
      {
        path: "login",
        element: <LoginPage />,
      },
    ],
  },
];

const protectedRoutes: RouteObject[] = [
  {
    element: <ProtectedPage />,
    children: [
      {
        path: "/",
        element: <HomePage />,
      },
      {
        path: "profile",
        element: <ProfilePage />,
      },
    ],
  },
];

const routes: RouteObject[] = [
  {
    element: (
      <>
        <header>
          <h2>Next Param Example</h2>
        </header>
        <main>
          <Outlet />
        </main>
      </>
    ),
    children: [
      ...authRoutes,
      ...protectedRoutes,
      {
        path: "*",
        element: <NotFound />,
      },
    ],
  },
];

const router = createBrowserRouter(routes);

export default router;

Auth Pages

The AuthPage component is a wrapper that handles authentication-related pages like login and signup. It checks whether the user is already logged in and redirects them to the appropriate page based on the next query parameter.

import { useEffect } from "react";
import { Navigate, Outlet } from "react-router-dom";
import useNextParam from "../../hooks/use-next-param";
import { isLoggedin } from "../../lib/utils";

/**
 * AuthPage is a wrapper component for authentication-related pages (like login or signup).
 * It checks if the user is already authenticated, and if so, redirects them to the page specified by the `next` query parameter.
 * If no `next` parameter is found, the user is redirected to the home page.
 *
 * @returns {JSX.Element} - Either the authentication page content or a redirection to the intended page.
 */
export default function AuthPage(): JSX.Element {
  const { nextParam, setNextParam } = useNextParam();
  const loggedin = isLoggedin();

  useEffect(() => {
    if (!nextParam) setNextParam("/");
  }, [nextParam]);

  if (loggedin) return <Navigate to={nextParam!} />;

  return <Outlet />;
}

The LoginPage component is a simple login form that authenticates users and redirects them to the page specified by the next parameter.

import { ChangeEvent, FormEvent, useState } from "react";
import { useNavigate } from "react-router-dom";
import useNextParam from "../../hooks/use-next-param";
import { USER_CREDENTIALS, type UserCredentials } from "../../lib/constants";
import { login } from "../../lib/utils";

/**
 * Login page of the application
 */
export default function LoginPage(): JSX.Element {
  const navigate = useNavigate();
  const { nextParam } = useNextParam();
  const [credentials, setCredentials] = useState<UserCredentials>({
    username: "",
    password: "",
  });

  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { value, name } = e.target;
    setCredentials((credentials) => ({ ...credentials, [name]: value }));
  };

  const loginUser = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const { username, password } = credentials;

    if (!username || !password) return;

    const isUserValid =
      username === USER_CREDENTIALS.username &&
      password === USER_CREDENTIALS.password;

    if (isUserValid) {
      login();
      return navigate(nextParam!);
    }

    alert("Invalid username or password");
  };

  return (
    <form onSubmit={loginUser}>
      <input
        type="text"
        name="username"
        value={credentials.username}
        onChange={handleInputChange}
        placeholder="Enter username"
        required
      />
      <input
        type="password"
        name="password"
        value={credentials.password}
        onChange={handleInputChange}
        placeholder="Enter password"
        required
      />
      <button type="submit">Login</button>
    </form>
  );
}

Protected Pages

The ProtectedPage component ensures that users trying to access protected pages are redirected to the login page if they are not authenticated. The next parameter is used to store the intended page's path, so users are redirected back to that page after logging in.

import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
import { isLoggedin, logout } from "../../lib/utils";
import useNextParam from "../../hooks/useNextParam";

/**
 * ProtectedPage is a wrapper component that guards protected routes in the application.
 * It checks if the user is authenticated, and if not, redirects them to the login page.
 * The login page URL includes a `next` parameter, which stores the path the user was trying to access.
 * Upon successful login, the user is redirected back to the intended page.
 *
 * @returns {JSX.Element} - Either the protected page content or a redirection to the login page.
 */
export default function ProtectedPage(): JSX.Element {
  const { pathname } = useLocation();
  const { nextParam } = useNextParam();
  const navigate = useNavigate();

  if (!isLoggedin()) {
    return <Navigate to={`/login?next=${nextParam ?? pathname}`} replace />;
  }

  const handleLogout = () => {
    logout();
    navigate({ pathname: "/login" });
  };

  return (
    <>
      <Outlet />
      <button onClick={handleLogout}>Logout</button>
    </>
  );
}

Home Page and Profile Page

The home and profile pages are typical protected pages that users can navigate to after logging in. The next parameter ensures that after authentication, users can return to the exact page they intended to visit.

import { Link } from "react-router-dom";

/**
 * Home Page of the application.
 * It includes a heading and a navigation link to the Profile page.
 *
 * @returns {JSX.Element} - The rendered home page with a heading and a link to the Profile page.
 */
export default function HomePage(): JSX.Element {
  return (
    <>
      <h3>Home Page</h3>
      <Link to={"profile"}>Profile</Link>
    </>
  );
}
import { Link } from "react-router-dom";

/**
 * Profile Page of the application.
 * It includes a heading and a navigation link to the Home page.
 *
 * @returns {JSX.Element} - The rendered profile page with a navigation link.
 */
export default function ProfilePage(): JSX.Element {
  return (
    <>
      <h3>Profile Page</h3>
      <Link to={"/"}>Home</Link>
    </>
  );
}

Demonstration

GitHub Repository

You can find the complete code for this project on GitHub: https://github.com/mskp/next-param-example

Conclusion

Using the next query parameter to redirect users to the correct page after login can greatly enhance the user experience, especially in applications with multiple protected pages. While this blog focused on the next parameter, remember to implement a robust authentication system for production use.