Retrieve data server side and save in context with Next.js
Asked Answered
P

4

46

I would like to be able to retrieve data from an API, server-side, and load it into React context, in order to make it available to any component in my app. I have tried various things, but nothing seems to allow me to do exactly what I want. Some things I've tried include:

getServerSideProps - This allows me to retrieve the data, server-side, but only exists for Page components, so if I want this available on every page, and I don't know which page my user will land on, I'll need to add logic for this to every single page.

getInitialProps in _app.js - I can add this to the _app.js component, which will run server-side, and can make it available to all components via a context provider, but the problem is that it runs on every single page, even when navigating client-side. I would like to be able to call the API once and only once, and this doesn't seem to allow that.

getInitialProps in _document.js - I can add this to the _document.js component, which only runs on the server, which seems to address the problem of it being called for every page, but I cannot figure out how to store it in React context from there. In fact, I can't seem to figure out how to access this data anywhere. It looks like getInitialProps in _document.js is called after getInitialProps in _app.js, so I'm not sure if I can use the value I generate from getInitialProps in _document.js when I am in _app.js.

There are a number of ways I can make this work if I call the API on the client, but that won't work for my use case as it will cause a flash of content when the client updates with the data from the API.

Has anyone come up with a way of addressing this use case?

Philomenaphiloo answered 8/2, 2021 at 17:27 Comment(3)
I am so glad for this question.Michaelson
As of Next.js 13, it's worth explicitly differentiating "client context" and "server context". This question assume "client context" given the date it was asked, however I've tried to answer for "server context" too in my answer below.Frisket
dev.to/jdgamble555/…Woozy
S
17

In Next.js, there's no native function to a) retrieve data from an API, b) do it on the server, c) make it available on every page, and d) only query the API on the first page the user visits.

As you've found out, getInitialProps and getServerSideProps will run every time you visit that page.

However, we can get this to work.

If you need the data before the initial load

  1. Use getInitialProps in _app.js to retrieve data from the API
  2. Load the data into React context inside the _app.js file so it persists between pages
  3. When the browser gets the data, create a cookie.
  4. On a subsequent page load, in getInitialProps, check if there's a cookie. If so, don't retrieve the data.

There's a fairly popular library called nookies to help with cookies in a Next.js project.

If you can load the page then fetch the data

There is a performance cost to using getInitialProps in _app.js: you'll never be able to create a fully static page. That's because getInitialProps will have to run on every single page load.

If you can fetch the data after page load, add an API route. Then, in the context provider, use useEffect to fetch the data.

Stung answered 20/2, 2021 at 18:51 Comment(1)
Thanks Nick. It sounds like you are confirming my belief that this isn't possible "out of the box". I had also considered using cookies, but it feels a bit hacky to me. If nobody else comes up with a better solution, I will award you the bounty.Philomenaphiloo
F
46

Next.js 13+ answer

Next.js experimental app folder now covers this use case perfectly well.

Passing values from RSC to client components via React context

  1. Fetch your data in a React Server Component (RSC) layout
  2. In this layout, render a React context with this value
  3. Consume this context in your application as you wish

You can of course set this context in an RSC further down the tree, for example if you need it only on a certain page or component of a page.

In this real-life example, I fetch a survey definition in a page RSC and pass it down to client-code via context. My context provider exposes a typed reusable hook.

Sample code for the React Server Component (here a page):

// /app/referer/page.tsx (Server Component)
import { headers } from 'next/headers'
export default async function Page() {
  const headersList = headers()
  const referer = headersList.get('referer')
  return (
    // we set a CLIENT context,
    // based on the SERVER context that we got via headers()
    <RefererProvider value={referer}>
      <InteractiveReferer />
    </RefererProvider>
  )
}

Sample code for the client context (don't forget the "use client" directive):

// /components/InteractiveReferer.tsx (Client Component)
// use "client"
export const InteractiveReferer = () => {
  // this context has been initialized by a parent RSC
  // but we can access it as expected in a Client Component
  const referer = useRefererContext()
  const [clickedReferer, setClickedReferer] = useState(false)
  return (
    <button
      onCLick={() => {
        setClickedReferer(true)
      }}
    >
      Referer: {referer}
    </button>
  )
}

Passing values from RSC to other RSCs via caching

Another novelty of Next.js app folder, is that now, server-side code is not limited to page-level getServerSideProps anymore. Nested component can also be Server Components and trigger server calls. Therefore, you might sometimes not only want to setup a client-side context, but also a kind of server-side context, scoped to the current request.

I describe this use case extensively in this article. To sum it up, you can use React 18 cache function (undocumented at the time of writing) to achieve this goal.

I've crafted an open source demonstration of this pattern, which can be summarized by this code sample:

import { cache } from "react";

export const getServerContext = cache(() => ({
    // a dummy context for the demonstration
    createdAt: Date.now().toString(),
    calledByLayout: false,
    calledByPage: false,
    calledByNested: false
}))

You can mutate the returned value directly to store new information in this context.

A note on naming: I've be using the term "server context" to designate data stored in cache that acts as a context. It's because it's the server-side equivalent of the "client" context. However, "Request cache" is perhaps more suited, as "server context" may be used for an other purpose in future versions of Next.js and React (sharing data between RSC and client components).

Get/Set and layouts

For RSC, the "cache" function allows to implement "cached getters", you don't really pass the values around but instead you call the function to get the value. Thanks to caching, the value is actually retrieved only once, your database or API won't be overloaded with requests.

In rare situations, this is not possible to use this approach, namely if you want to derive a value from the page props (rather than fetching data from an API/database) and pass it to children without props drilling.

The server-only-context package implements an alterative "get/set" pattern for mimicking a context with RSC. You can set a value in the page, and access it in the component. Be mindful that you can't guarantee that a layout is rendered before a page (because of client-side navigation), so you can't set a value from a layout and get it from a page. Hopefully, this pattern is never strictly needed.

Frisket answered 22/2, 2023 at 13:45 Comment(0)
S
17

In Next.js, there's no native function to a) retrieve data from an API, b) do it on the server, c) make it available on every page, and d) only query the API on the first page the user visits.

As you've found out, getInitialProps and getServerSideProps will run every time you visit that page.

However, we can get this to work.

If you need the data before the initial load

  1. Use getInitialProps in _app.js to retrieve data from the API
  2. Load the data into React context inside the _app.js file so it persists between pages
  3. When the browser gets the data, create a cookie.
  4. On a subsequent page load, in getInitialProps, check if there's a cookie. If so, don't retrieve the data.

There's a fairly popular library called nookies to help with cookies in a Next.js project.

If you can load the page then fetch the data

There is a performance cost to using getInitialProps in _app.js: you'll never be able to create a fully static page. That's because getInitialProps will have to run on every single page load.

If you can fetch the data after page load, add an API route. Then, in the context provider, use useEffect to fetch the data.

Stung answered 20/2, 2021 at 18:51 Comment(1)
Thanks Nick. It sounds like you are confirming my belief that this isn't possible "out of the box". I had also considered using cookies, but it feels a bit hacky to me. If nobody else comes up with a better solution, I will award you the bounty.Philomenaphiloo
B
1

You can try using getInitialProps in the Page Component itself

This way, the data fetching will only happen for that exact page.

Brigid answered 9/2, 2021 at 6:42 Comment(1)
Thanks for the suggestion Edrian, but I need this data on every page, so that would mean adding that code for getInitialProps on every page and that's something I'm trying to avoid. This is why I am hoping to do something with _app.js or _document.js. Also, I want to use getServerSideProps on most of my pages.Philomenaphiloo
L
-1

Maybe Redux could be a solution for your issues. Context api tends to trigger unnecessary re rendering of the consumers.

Lakesha answered 19/2, 2021 at 18:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.