Dynamic Data in Authenticated Next.js Apps

Next.js offers several data-fetching patterns, each suited to different application needs: static-site generation (SSG), server-side rendering (SSR), client-side rendering (CSR), incremental static regeneration (ISR), and dynamic routing. This article examines SSG and dynamic routing through the getStaticProps and getStaticPaths methods, particularly when authentication is required.

Dynamic data fetching means rendering user-specific information on a dedicated page. For example, clicking a user's name from a list should take you to a unique page showing that user's details. getStaticPaths enables this by generating paths for each item in an array of objects, typically keyed by a unique field like id or _id.

export async function getStaticPaths() {
  return {
    paths: {
      [{
        params: {
          uniqueId: id.toString()
        }
      }],
      fallback: false
    }
  }
}

The unique identifier from getStaticPaths becomes available through the context parameter of getStaticProps. These two methods work in tandem—you extract the id from the generated path and pass it to getStaticProps for the actual data fetch.

export async function getStaticProps(context) {
  return {
    props: {
      userData: data,
    },
  }
}

Limitations of Native Methods for Protected Routes

Public APIs that don't require authentication work well with getStaticProps and getStaticPaths. The flow is straightforward:

// getStaticPaths
export async function getStaticPaths() {
  const response = fetch("https://jsonplaceholder.typicode.com/users")
  const userData = await response.json()

 // Getting the unique key of the user from the response
 // with the map method of JavaScript.
  const uniqueId = userData.map((data) => {
    return data.id
  })

  return {
    paths: {
      [{
        params: {
          uniqueId: uniqueId.toString()
        }
      }],
      fallback: false
    }
  }
}

Here, the unique id is derived from JavaScript's map method and passed via the context parameter.

export async function getStaticProps(context) {
  // Obtain the user’s unique ID.
  const userId = context.params.uniqueId

  // Append the ID as a parameter to the API endpoint.
  const response = fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
  const userData = await response.json()
  return {
    props: {
      userData,
    },
  }
}

In that snippet, userId is initialized from context and appended to the API's base URL. Remember, these methods must be exported only from files within the pages folder.

Authenticated applications, however, introduce a different flow. Consider a user who logs in and visits their profile to view or edit their own data. This requires verifying the user's identity—most commonly handled through JSON Web Tokens (JWTs). When a user signs up, their details are stored and a JWT is assigned. On login, the credentials are checked and an authentication state is established so the front end can access details like the JWT.

There are several approaches to preserving the auth-state: Redux, React Composition, or React's Context API. Storing the JWT in localStorage is common for initial development, but from a security standpoint, an httpOnly cookie is safer to mitigate cross-site request forgery (CSRF) and cross-site scripting (XSS) attacks. This approach relies on the back-end API having the proper cookie middleware in place. Alternatively, NextAuth.js offers an open-source solution for authentication.

With the token in localStorage, API calls requiring authorization can proceed without a 501 (unauthorized) error.

headers: {
  "x-auth-token": localStorage.getItem("token")
}

Using useRouter for Client-Side Dynamic Fetching

The native getStaticProps and getStaticPaths methods cause issues in authenticated applications because they run on the server, where localStorage doesn't exist—resulting in a referenceError: "localStorage is undefined". Since localStorage is a browser-side object, the server cannot access it.

Next.js's router API offers a workaround. Using the useRouter hook, you can fetch user-specific data based on the unique ID from the URL.

// pages/index.js

import React from "react";
import axios from "axios";
import { userEndpoints } from "../../../routes/endpoints";
import Link from "next/link";

const Users = () => {
  const [data, setData] = React.useState([])
  const [loading, setLoading] = React.useState(false)

  const getAllUsers = async () => {
    try {
      setLoading(true);
      const response = await axios({
        method: "GET",
        url: userEndpoints.getUsers,
        headers: {
          "x-auth-token": localStorage.getItem("token"),
          "Content-Type": "application/json",
        },
      });
      const { data } = response.data;
      setData(data);
    } catch (error) {
      setLoading(false);
      console.log(error);
    }
  };

  return (
    <React.Fragment>
      <p>Users list</p>
      {data.map((user) => {
          return (
            <Link href={`/${user._id}`} key={user._id}>
              <div className="user">
                <p className="fullname">{user.name}</p>
                <p className="position">{user.role}</p>
              </div>  
            </Link>
          );
        })}
    </React.Fragment>
  );
};

export default Users;

In that code, the useEffect hook triggers the data fetch once the page renders, and the JWT is set in the request header under x-auth-token. Clicking a user routes to a page built from their unique ID.

The useRouter hook gives access to the pathname, allowing you to extract the query parameter—the unique id—from the route's URL.

// [id].js

import React from "react";
import Head from "next/head";
import axios from "axios";
import { userEndpoints } from "../../../routes/endpoints";
import { useRouter } from "next/router";

const UniqueUser = () => {
  const [user, setUser] = React.useState({
    fullName: "",
    email: "",
    role: "",
  });
  const [loading, setLoading] = React.useState(false);
  const { query } = useRouter();

  // Obtaining the user’s unique ID with Next.js'
  // useRouter hook.
  const currentUserId = query.id;

  const getUniqueUser = async () => {
    try {
      setLoading(true);
      const response = await axios({
        method: "GET",
        url: `${userEndpoints.getUsers}/${currentUserId}`,
        headers: {
          "Content-Type": "application/json",
          "x-auth-token": localStorage.getItem("token"),
        },
      });
      const { data } = response.data;
      setUser(data);
    } catch (error) {
      setLoading(false);
      console.log(error);
    }
  };

  React.useEffect(() => {
    getUniqueUser();
  }, []);

  return (
    <React.Fragment>
      <Head>
        <title>
          {`${user.fullName}'s Profile | "Profile" `}
        </title>
      </Head>
        <div>
          <div className="user-info">
            <div className="user-details">
              <p className="fullname">{user.fullName}</p>
              <p className="role">{user.role}</p>
              <p className="email">{user.email}</p>
            </div>
          </div>
        </div>
      )}
    </React.Fragment>
  );
};
export default UniqueUser;

As shown, the query object from useRouter is destructured to get the user's unique ID, which is then appended to the API endpoint.

const {query} = useRouter()
const userId = query.id

Once the unique ID is in the endpoint, the user's data renders when the page loads. This client-side approach bypasses the server-side limitations of the native SSG methods when working with authenticated data.