Persist user state in NextJS by using a cookie and a context provider

Use a combination of application context and browser cookies to save and retrieve state in a NextJS app.

Required packages

Setting up the context provider

Using react createContext and useContext.

// FILENAME: ./src/contexts/location.js

import { createContext, useContext, useState } from "react";

import { SetLocation } from "../utils/location";

const Context = createContext();

export function LocationProvider({ children, cookie }) {
  const [location, setLocation] = useState(cookie);

  return (
    <Context.Provider value={[location, setLocation]}>
      {children}
    </Context.Provider>
  );
}

export function useLocationContext() {
  SetLocation(Context._currentValue[0]);

  return useContext(Context);
}Code language: JavaScript (javascript)

Add the context provider to the root app.

Note: Feel free to remove Apollo, NProgress, and Router events.

//FILENAME: ./pages/_app.js

import { ApolloProvider } from "@apollo/client";
import Router from "next/router";
import NProgress from "nprogress";
import "nprogress/nprogress.css";

import "../src/styles/index.scss";
import client from "../src/apollo/client";
import { LocationProvider } from "../src/contexts/location";

NProgress.configure({ minimum: 0.1 });
NProgress.configure({ trickleRate: 0.1, trickleSpeed: 400 });
Router.events.on("routeChangeStart", () => NProgress.start());
Router.events.on("routeChangeComplete", () => NProgress.done());
Router.events.on("routeChangeError", () => NProgress.done());

function MyApp({ Component, pageProps }) {
  return (
    <ApolloProvider client={client}>
      <LocationProvider>
        <Component {...pageProps} />
      </LocationProvider>
    </ApolloProvider>
  );
}

export default MyApp;
Code language: JavaScript (javascript)

Getting and setting the cookies

The way that I get and set cookies, and more on getting them than I actually use, look out for the note about this.

// FILENAME: ./pages/api/get-location.js

import { getLocation } from "../../src/utils/cookies";

export default async function location(req, res) {
  res.status(200).json({ success: getLocation(req) });
}Code language: JavaScript (javascript)
// FILENAME: ./pages/api/get-location.js

import cookie from "cookie";

export default async function location(req, res) {
  const { location } = req?.body ?? {};

  let maxAge = 60 * 60 * 24 * 30; // 1 month

  // Delete the cookie
  if (location === "" || location === "undefined" || location === undefined) {
    maxAge = -1;
  }

  /**
   * Note when you run 'npm run start' locally, cookies won't be set, because locally process.env.NODE_ENV = 'production'
   * so secure will be true, but it will still be http and not https , when tested locally.
   * So when testing locally both in dev and prod, set the value of 'secure' to be false.
   */
  res.setHeader(
    "Set-Cookie",
    cookie.serialize("location", location, {
      httpOnly: true,
      secure: "development" !== process.env.NODE_ENV,
      path: "/",
      maxAge: maxAge,
    })
  );

  // Only sending a message that successful, because we dont want to send JWT to client.
  res.status(200).json({ success: location });
}Code language: JavaScript (javascript)
// FILENAME: ./src/utils/location

import axios from "axios";

export const GetLocation = async () => {
  return axios({
    data: {},
    method: "post",
    url: "/api/get-location",
  })
    .then((data) => {
      const { success } = data?.data ?? {};

      if (success === "undefined" || success === "null" || success === "")
        return undefined;
      return success;
    })
    .catch(() => {
      return false;
    });
};

export const SetLocation = async (l) => {
  return axios({
    data: {
      location: l,
    },
    method: "post",
    url: "/api/set-location",
  })
    .then((data) => {
      const { success } = data?.data ?? {};
      return true;
    })
    .catch(() => {
      return false;
    });
};Code language: JavaScript (javascript)
// FILENAME: ./src/utils/cookies.js

import cookie from "cookie";

export function parseCookies(req) {
  return cookie.parse(req ? "" + req.headers.cookie : "");
}

export function getLocation(req) {
  const cookies = parseCookies(req);
  return cookies.location || "";
}Code language: JavaScript (javascript)
Note: the following are only present for completeness but are infact unused:
  • `./src/utils/cookies.js` – I don’t use this in this instance because can read the cookie using NextJS ‘getInitialProps’ (given later in this article).
  • `./src/utils/location.js > GetLocation` – I don’t use this for the same reason.

The way I get the cookie

Replace the code for the root application. Note the addition of the cookies property and its passing to the location provider. The cookie is read using NextJS ‘getInitialProps’.

Note: Feel free to remove Apollo, NProgress, and Router events.

//FILENAME: ./pages/_app.js

import { ApolloProvider } from "@apollo/client";
import Router from "next/router";
import NProgress from "nprogress";
import "nprogress/nprogress.css";

import "../src/styles/index.scss";
import client from "../src/apollo/client";
import { LocationProvider } from "../src/contexts/location";

NProgress.configure({ minimum: 0.1 });
NProgress.configure({ trickleRate: 0.1, trickleSpeed: 400 });
Router.events.on("routeChangeStart", () => NProgress.start());
Router.events.on("routeChangeComplete", () => NProgress.done());
Router.events.on("routeChangeError", () => NProgress.done());

function MyApp({ Component, pageProps, cookies }) {
  return (
    <ApolloProvider client={client}>
      <LocationProvider cookie={cookies.location}>
        <Component {...pageProps} />
      </LocationProvider>
    </ApolloProvider>
  );
}

export default MyApp;

MyApp.getInitialProps = async (props) => {
  const { ctx } = props;

  return {
    cookies: ctx.req.cookies,
  };
};Code language: JavaScript (javascript)

Bringing it all together

At this point everything is in place to save the application state to the context and a cookie and when the application starts the cookies are read and loaded into the context.

Reading and writing the state

Now that we have the location context provider which exposes a react useState we can read and write it in much the same way as useState. When setting the location the context is updated and a cookie is also set.

// FILENAME: ./pages/index.js

...

import { useLocationContext } from "../src/contexts/location";

export default function Index({ data }) {
  ...

  const [location, setLocation] = useLocationContext();

  ...

  const _setLocation = (l) => {
    setLocation(l);
  };

  return (
    ...
    <p>Current location is: {location}</p>
    <button onClick={() => SetLocation("x")}>Set location to x</button>
    ...
  )
}Code language: PHP (php)
I quickly wrote out this article, but if it’s helpful please comment. If people are finding this type of thing helpful I’ll rewrite with a lot more detail and explanation.

See also: Add a universal page load bar and/or spinner to NextJS if you’re wondering what the nprogress code in the _app.js file is for.

References:

Method 3 in this article provides more explanation about the provider.

https://blog.bitsrc.io/3-effective-ways-to-implement-state-management-in-next-js-66d1b09f0b8b

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.