iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🙆

Making SSR in Next.js as Simple as Possible

に公開2

1. The Demise of getInitialProps

1.1. In Next.js, it is assumed that SSR with getInitialProps is over

Since Next.js 9.3, with the arrival of getStaticProps and getServerSideProps, there is a prevailing view that SSR using getInitialProps has reached its end. However, getStaticProps and getServerSideProps actually possess significant drawbacks.

getStaticProps is perfectly fine if used on the premise of SSG. However, it does not play well with systems that have a certain update frequency or real-time editing features. Even when used with ISR, there is a specification where old data is displayed on the first visit after expiration; thus, if the timing is bad, you might end up serving old data to someone who has finally visited. It is powerful depending on where it is used, but its applications are limited.

getServerSideProps fetches data every time you navigate to that page, so it is excellent for real-time responsiveness. Or so one would normally think, but it won't fetch data unless you navigate. Since it does not update unless routing occurs, to reload data within a page, you need to trigger routing to the same page. And the annoying part is that once that routing occurs, there is no way to stop the data update. Even in cases where you want it not to update right now because you are caching data on the client, it will always go to fetch the data. There is no way to stop an over-enthusiastic getServerSideProps.

And then there is getInitialProps, which is thought to have met its end. The decisive difference from the previous two is that while getStaticProps and getServerSideProps are associated with page components, getInitialProps is used in the App component common to all pages. It is a specification for centrally managing features intended for all pages in one place. Furthermore, while the previous two are server-side exclusive features, getInitialProps is called on both the client and the server, making it very difficult to separate functionality. Also, it is called on the server side only once when the page is reloaded; even if routing occurs, it is never called on the server again. getInitialProps processes everything in one place, and the server-side processing happens exactly once. On top of that, there are calls from the client. Just hearing this, you probably won't see any advantages at all.

1.2. Now, it's time for getInitialProps

Actually, if you want to control data exchange at any timing based on the premise of SSR, the only choice left is getInitialProps. This is because getStaticProps does not update data when you want it to, and getServerSideProps stays active even when you don't need it. The optimal approach is to perform SSR using getInitialProps at the time of page refresh, and for the remaining update processes, build a cache structure on the client side and reload at the necessary timing.

2. Realizing SSR with getInitialProps

2.1. The hardship of writing double fetches for server and client

This is what makes SSR tedious. If it's a system where you just reload the page and that's it, server-side processing alone is enough. However, if you are to control data requests on the client side, you must create fetches for both. Furthermore, if you try to incorporate a feature like authentication, it's a nightmare to look at.

2.2. Let's unify it

First, we will avoid writing the code twice. This is realized by Apollo, which is used with GraphQL. However, to perform SSR properly while introducing Apollo, you have to overcome a certain mental hurdle. Let's just borrow the operating principle.

How does Apollo realize SSR without duplicating code? The answer is "empty rendering." It performs a React rendering once on the server to extract only the data and passes that to the props of the App component. Then, it performs the actual rendering for SSR on the server and passes the generated HTML data and the created props to the client. As long as the client side receives the same props that the server side used to generate the HTML, no differences occur, and thus no unnecessary Node changes are triggered.

In other words, if you do the same thing, you can unify the fetch process. It's a very simple story, right?

3. Just perform empty rendering and pass data—it's that simple

So, let's try it out.
First, we'll fetch data from the Japan Meteorological Agency, which is easy for anyone to use.

3.1. Sample Source

Source code: https://github.com/SoraKumo001/next-weather
Demo: https://next-weather-opal.vercel.app/

src/pages/_app.tsx

For SSR, getDataFromTree is executed within getInitialProps, and the data generated through empty rendering is sent to the App component.
The specific processing logic is available in the source code at @react-libraries/use-ssr.

import {
  CachesType,
  createCache,
  getDataFromTree,
} from "@react-libraries/use-ssr";

import { AppContext, AppProps } from "next/app";

const App = (props: AppProps & { cache: CachesType }) => {
  const { Component, cache } = props;
  createCache(cache);
  return <Component />;
};
App.getInitialProps = async ({ Component, router, AppTree }: AppContext) => {
  const cache = await getDataFromTree(
    <AppTree Component={Component} pageProps={{}} router={router} />
  );
  return { cache };
};
export default App;

src/pages/index.tsx

This is a sample for retrieving the list of regions.

By using useSSR and performing setState on the data to be subjected to SSR, the data is sent during getDataFromTree.
Since the completion of the asynchronous function within useSSR is regarded as the completion of the SSR process, it is possible to wait until the fetch is finished on the server.

On the client side, the target data is cached even after the component is unmounted, unless the state is explicitly cleared.
Even when routing occurs, it does not fetch data unnecessarily.
In the event when the button is pressed, setState(undefined) is executed; this clears the cache and triggers a re-fetch on the client side.

import React from "react";
import Link from "next/link";
import { useSSR } from "@react-libraries/use-ssr";

interface Center {
  name: string;
  enName: string;
  officeName?: string;
  children?: string[];
  parent?: string;
  kana?: string;
}
interface Centers {
  [key: string]: Center;
}
interface Area {
  centers: Centers;
  offices: Centers;
  class10s: Centers;
  class15s: Centers;
  class20s: Centers;
}

const Page = () => {
  const [state, setState] = useSSR<Area | null>(
    "area",
    async (state, setState) => {
      // Do nothing if data already exists
      if (state !== undefined) return;
      // Set a flag indicating processing
      setState(null);
      const result = await fetch(
        `https://www.jma.go.jp/bosai/common/const/area.json`
      )
        .then((r) => r.json())
        .catch(() => null);
      // Save the results
      setState(result);
    }
  );
  return (
    <div>
      <button onClick={() => setState(undefined)}>Reload</button>
      {state &&
        Object.entries(state.offices).map(([code, { name }]) => (
          <div key={code}>
            <Link href={`/weather/${code}`}>
              <a>{name}</a>
            </Link>
          </div>
        ))}
    </div>
  );
};
export default Page;

src/pages/weather/[id].tsx

This is a sample for displaying the weather for a region code.

In useSSR, it is necessary to set a key for the stored data. To avoid conflicts with region codes, the key is set using an array like ["weather", String(id)].

import { useSSR } from "@react-libraries/use-ssr";
import { useRouter } from "next/dist/client/router";
import Link from "next/link";
import React from "react";

export interface Weather {
  publishingOffice: string;
  reportDatetime: Date;
  targetArea: string;
  headlineText: string;
  text: string;
}

const Page = () => {
  const router = useRouter();
  const id = router.query["id"];
  const [state, setState] = useSSR<Weather | null>(
    ["weather", String(id)] /*CacheKeyName*/,
    async (state, setState) => {
      if (state !== undefined) return;
      setState(null);
      const result = await fetch(
        `https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${id}.json`
      )
        .then((r) => r.json())
        .catch(() => null);
      setState(result);
    }
  );
  return (
    <div>
      <button onClick={() => setState(undefined)}>Reload</button>
      {state && (
        <>
          <h1>{state.targetArea}</h1>
          <div>{new Date(state.reportDatetime).toLocaleString()}</div>
          <div>{state.headlineText}</div>
          <pre>{state.text}</pre>
        </>
      )}
      <div>
        <Link href="/">
          <a>戻る</a>
        </Link>
      </div>
    </div>
  );
};
export default Page;

3.2. Operation Results

3.2.1. Operating Status

Region List

Details

3.2.2. Generated HTML

You can see that it has been server-side rendered.

4. Conclusion

In the process of "SSR" + "Updating freely on the client," it is now possible to write compact descriptions while preventing code duplication. It's quite simple, isn't it?
By the way, as long as the data can be fetched, you can use other libraries or mechanisms for the fetch part.

As for future plans, I have a vision of modifying getDataFromTree to create a mechanism for caching a certain amount of data on the server side to perform something like pseudo-ISR.

GitHubで編集を提案

Discussion

snakasnaka

また、サーバ側で呼び出されるのはページをリロードしたタイミング一度きりで、ルーティングが発生しても、サーバ上では二度と呼ばれることはありません。

記事の主題とはズレるのですが、上記の箇所について手元の Next.JS 12 で確認していますが、条件によってはこのとおりの動作とはならないようでした。
( 記事を書かれた時点から Next.JS の実装もかなり変化しているので当然かもしれませんがメモとして... :pray: )

確認した条件:

  • Next.JS : next@12.3.4
  • 対象 Page が SSG 対象となっている ( 対象 Page で getStaticProps が実装されている )
  • _app.tsx に getInitialProps を実装している

上記の条件では

  • 対象 Page をリロードした場合、Linkなどで対象 Page へ遷移した場合、いずれの場合でも Server-side で getInitialProps が動作しているように見えました