iTranslated by AI
[Next.js] Implementing SSR for Asynchronous Data with Ultimate Simplicity
Implementing SSR with Extreme Simplicity
Unnecessary Existing Features
In implementing this approach, getServerSideProps, getInitialProps, and React Server Components are not required. Since no special mechanisms are involved, configuration in next.config.js is unnecessary. There is also no need to write anything in _app.tsx.
Async Data Can Be Easily Output via SSR on Components
This is the minimal sample code for handling asynchronous data. Even though the component returns an asynchronous 'Hello world!', it is properly output in the HTML during the initial rendering. Of course, data fetched via fetch can also be used, which I will introduce later.
src/pages/simple.tsx
Wrap components that require asynchronous data with <SSRProvider>, and call asynchronous processes (such as fetch) via useSSR where the data is needed. There are no constraints like being unable to hold state, as seen in React Server Components. Components output this way can be operated freely on the client side.
import { SSRProvider, useSSR } from "next-ssr";
const Test = () => {
const { data } = useSSR(async () => "Hello world!");
return <div>{data}</div>;
};
const Page = () => {
return (
<SSRProvider>
<Test />
</SSRProvider>
);
};
export default Page;
The following is the output HTML data. You can confirm that <div>Hello world!</div> is included.
<!DOCTYPE html>
<html>
<head>
<style data-next-hide-fouc="true">
body {
display: none;
}
</style>
<noscript data-next-hide-fouc="true">
<style>
body {
display: block;
}
</style>
</noscript>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="next-head-count" content="2" />
<noscript data-n-css=""></noscript>
<script
defer=""
nomodule=""
src="/_next/static/chunks/polyfills.js?ts=1677395377171"
></script>
<script
src="/_next/static/chunks/webpack.js?ts=1677395377171"
defer=""
></script>
<script
src="/_next/static/chunks/main.js?ts=1677395377171"
defer=""
></script>
<script
src="/_next/static/chunks/pages/_app.js?ts=1677395377171"
defer=""
></script>
<script
src="/_next/static/chunks/pages/simple.js?ts=1677395377171"
defer=""
></script>
<script
src="/_next/static/development/_buildManifest.js?ts=1677395377171"
defer=""
></script>
<script
src="/_next/static/development/_ssgManifest.js?ts=1677395377171"
defer=""
></script>
<noscript id="__next_css__DO_NOT_USE__"></noscript>
</head>
<body>
<div id="__next">
<div>Hello world!</div>
<script id="__NEXT_DATA_PROMISE__" type="application/json">
{ ":R2m:": { "data": "Hello world!", "isLoading": false } }
</script>
</div>
<script src="/_next/static/chunks/react-refresh.js?ts=1677395377171"></script>
<script id="__NEXT_DATA__" type="application/json">
{
"props": { "pageProps": {} },
"page": "/simple",
"query": {},
"buildId": "development",
"nextExport": true,
"autoExport": true,
"isFallback": false,
"scriptLoader": []
}
</script>
</body>
</html>
Example of Fetching Weather Forecasts and Using SSR
Next is an example where the code is a bit longer: fetching weather forecasts from the Japan Meteorological Agency (JMA) website and outputting them via SSR. A Reload button is provided, allowing for re-fetching.
src/pages/index.tsx
import { SSRProvider, useSSR } from "next-ssr";
export interface WeatherType {
publishingOffice: string;
reportDatetime: string;
targetArea: string;
headlineText: string;
text: string;
}
/**
* Data obtained from the JMA website.
*/
const fetchWeather = (id: number): Promise<WeatherType> =>
fetch(
`https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${id}.json`
)
.then((r) => r.json())
.then(
// Additional weights (500 ms)
(r) => new Promise((resolve) => setTimeout(() => resolve(r), 500))
);
/**
* Components for displaying weather information
*/
const Weather = ({ code }: { code: number }) => {
const { data, reload, isLoading } = useSSR<WeatherType>(
() => fetchWeather(code),
{ key: code }
);
if (!data) return <div>loading</div>;
const { targetArea, reportDatetime, headlineText, text } = data;
return (
<div
style={
isLoading ? { background: "gray", position: "relative" } : undefined
}
>
{isLoading && (
<div
style={{
position: "absolute",
color: "white",
top: "50%",
left: "50%",
}}
>
loading
</div>
)}
<h1>{targetArea}</h1>
<button onClick={reload}>Reload</button>
<div>
{new Date(reportDatetime).toLocaleString("ja-JP", {
timeZone: "JST",
})}
</div>
<div>{headlineText}</div>
<div style={{ whiteSpace: "pre-wrap" }}>{text}</div>
</div>
);
};
/**
* Page display components
*/
const Page = () => {
return (
<SSRProvider>
<a href="https://github.com/SoraKumo001/next-use-ssr">Source Code</a>
<hr />
{/* Chiba */}
<Weather code={120000} />
{/* Tokyo */}
<Weather code={130000} />
{/* Kanagawa */}
<Weather code={140000} />
</SSRProvider>
);
};
export default Page;
Demo Screen
As shown in the console, the data is already available at the time of the initial HTML. When the Reload button is pressed, the data is re-fetched from the JMA website as a client-side process.

Example of Fetching Hacker News and Using SSR
The process involves retrieving news list numbers first and then fetching individual news data. As a result, the fetches are written in a nested manner, but it works without any particular issues.
src/pages/news.tsx
When nesting useSSR, you must set a key. If the key is omitted, useId is used to determine the cache name; however, in nested cases, the return value of useId can cause inconsistencies between the server and the client, so it must be explicitly specified.
import { Fragment, useState } from "react";
import { NextPage } from "next";
import { SSRProvider, useSSR } from "next-ssr";
const FETCH_WAIT = 50;
const PAGE_SIZE = 30;
type NewsType = {
id: number;
title: string;
time: number;
url: string;
by: String;
score: number;
descendants: number;
kids: number[];
text: string;
};
const newsFetch = async (id: number): Promise<NewsType> => {
return fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`)
.then(
(v) =>
new Promise<Response>((resolve) =>
setTimeout(() => resolve(v), FETCH_WAIT)
)
)
.then((v) => v.json());
};
const News = ({ id }: { id: number }) => {
const { data, reload } = useSSR(
() => newsFetch(id),
// Name of the data to be passed to the client during SSR.
{ key: `news-${id}` }
);
if (!data) return null;
const { title, time, url, by, score, descendants } = data;
return (
<div>
<div>
<button onClick={reload}>Reload</button> <a href={url}>{title}</a>
</div>
<div>
{score} point:{score} by {by}
{new Date(time * 1000).toLocaleString("en-us", {
timeZone: "UTC",
})} | comment:{descendants}
</div>
</div>
);
};
const newsListFetch = (): Promise<number[]> => {
return fetch(`https://hacker-news.firebaseio.com/v0/topstories.json`)
.then(
(v) =>
new Promise<Response>((resolve) =>
setTimeout(() => resolve(v), FETCH_WAIT)
)
)
.then((v) => v.json());
};
const NewsList = () => {
const { data, reload } = useSSR(() => newsListFetch());
const [page, setPage] = useState(1);
if (!data) return null;
const maxPage = Math.floor(data?.length / PAGE_SIZE);
return (
<div>
<div>
<button onClick={reload}>Reload All</button>{" "}
<button onClick={() => setPage(Math.max(page - 1, 1))}>Previous</button>{" "}
{page}/{maxPage}{" "}
<button onClick={() => setPage(Math.min(page + 1, maxPage))}>
Next
</button>
</div>
{data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE).map((id) => (
<Fragment key={id}>
<hr />
<News id={id} />
</Fragment>
))}
</div>
);
};
const Page: NextPage = () => {
return (
<SSRProvider>
<NewsList />
</SSRProvider>
);
};
export default Page;
Various Explanations
The Pain Points of SSR
When performing SSR with asynchronous data in Next.js, you typically use getServerSideProps or getInitialProps. These require you to execute the data fetching process during SSR and pass the data to the components. Additionally, if data re-fetching is needed on the client side, you must also write data fetching logic within the component. When using React Server Components, you can write data fetching logic inside the component, but because they cannot hold state, you cannot re-fetch on the client side; instead, you must re-request the entire component. Overall, implementing SSR is tedious.
How to Wait for Asynchronous Operations During Server-Side Component Rendering
The reason such tedious processing is necessary is due to React's constraints rather than Next.js itself. This is because once component rendering begins, it cannot wait for asynchronous operations and finishes immediately on the server. While it is possible to include asynchronous processes, rendering completes immediately after the data acquisition command is issued, and the data arrives only after the HTML has been sent to the client. When using React Server Components, the component itself can be made asynchronous, allowing it to wait, but you must accept the significant drawback of not being able to hold state.
However, in the program presented at the beginning, ordinary components are waiting for asynchronous processing. React can actually wait for asynchronous operations during component rendering. The only way to achieve this behavior is by using "throw promise". While it is often assumed that this must be used with Suspense, that is not necessary. It can be used when you want to perform an asynchronous wait on the server side, and the component will be re-rendered once the promise is resolved.
By the way, although I've created the sample with Next.js this time, the same thing is possible with React alone by combining it with something like React's renderToPipeableStream. The tests written for the library I created this time use that approach on Jest.
Asynchronous Waiting on the Server Side is Not a Special Method
Asynchronous waiting via throw promise on the server side is a method commonly used in React Server Components. However, as mentioned earlier, using this introduces the constraint of being unable to hold state for the output components, making it difficult to find the right use case. If you want to avoid this constraint, instead of using React Server Components, you can simply enable similar behavior in regular components.
Issues with Using throw promise in Regular Components
In React Server Components, even when handling asynchronous data, the necessary data is automatically embedded in the output HTML, so no additional consideration is required for data handling. On the other hand, in regular components, data disappears when re-mounted on the client side. This is because even if the HTML contains data for display, the component cannot receive it as its own data. In such cases, a re-fetch becomes necessary on the client side, defeating the purpose of SSR.
How to Make Server-Generated throw promise Data Valid on the Client
When you render and output a component, you can pass it to the client in HTML format, but this is merely passing the visual data. It is not shared as data handled by the component for re-rendering. Therefore, you need to convert the asynchronous data into JSON format and pass it in a way that the client can understand. When using getServerSideProps or getInitialProps, Next.js handles this behavior automatically. If you are not using them, you must implement this process manually.
<Suspense> is Not Required
While <Suspense> is often used in tandem with throw promise, as you can see, it hasn't really been used here. If it were used, it would be for simplifying client-side loading or for performing Streaming SSR. The former is a rather crude specification that simply swaps the "thrown" component with a loading indicator, so it is a feature that can only be used when designing relatively simple UIs. The latter became meaningless as it is no longer supported under pages as of Next.js@13.2 (it was available up to the 13.1 series). The intention seems to be that Streaming SSR should be handled in appDir.
Cases Where getInitialProps is Required
Next.js statically optimizes page components by default. If you use useRouter on the server to handle parameters passed to a fetch, please be aware that it won't work correctly unless you add an empty getInitialProps to disable static optimization.
A Library Version of This Series of Processes
The following source code performs the series of processes described above. It handles waiting for asynchronous data and passing the collected data to the client.
import React, {
ReactNode,
useContext,
useId,
useRef,
useCallback,
useSyncExternalStore,
createContext,
} from "react";
const DATA_NAME = "__NEXT_DATA_PROMISE__";
type StateType<T> = {
data?: T;
error?: unknown;
isLoading: boolean;
fetcher: () => Promise<T>;
};
type Render = () => void;
type ContextType = {
values: { [key: string]: StateType<unknown> };
promises: Promise<unknown>[];
finished: boolean;
renderMap: Map<string | number, Set<Render>>;
};
/**
* Context for asynchronous data management
*/
const promiseContext = createContext<ContextType>(undefined as never);
/**
* Rendering event propagation
*/
const render = (
renderMap: Map<string | number, Set<Render>>,
key: string | number
) => renderMap.get(key)?.forEach((render) => render());
/**
* Asynchronous data loading
*/
const loader = <T,>(
key: string | number,
context: ContextType,
fetcher?: () => Promise<T>
) => {
const { promises, values, renderMap } = context;
const _fetcher = fetcher ?? values[key]?.fetcher;
if (!_fetcher) throw new Error("Empty by fetcher");
const value = {
data: values[key]?.data,
error: undefined,
isLoading: true,
fetcher: _fetcher,
};
values[key] = value;
render(renderMap, key);
const promise = _fetcher();
if (typeof window === "undefined") {
promises.push(promise);
}
promise
.then((v) => {
values[key] = {
data: v,
error: undefined,
isLoading: false,
fetcher: _fetcher,
};
render(renderMap, key);
})
.catch((error) => {
values[key] = {
data: undefined,
error,
isLoading: false,
fetcher: _fetcher,
};
render(renderMap, key);
});
return promise;
};
/**
* hook for re-loading
*/
export const useReload = (key: string | number) => {
const context = useContext(promiseContext);
return useCallback(() => {
loader(key, context);
}, [context, key]);
};
/**
* Asynchronous data acquisition hook for SSR
*/
export const useSSR = <T,>(
fetcher: () => Promise<T>,
{ key }: { key?: string | number } = {}
): StateType<T> & { reload: () => void } => {
const context = useContext(promiseContext);
const { values, renderMap } = context;
const id = useId();
const cacheKey = key ?? id;
const value = useSyncExternalStore(
(callback) => {
const renderSet = renderMap.get(cacheKey) ?? new Set<Render>();
renderMap.set(cacheKey, renderSet);
renderSet.add(callback);
return () => renderSet.delete(callback);
},
() => values[cacheKey] as StateType<T>,
() => values[cacheKey] as StateType<T>
);
const reload = useCallback(() => {
return loader(cacheKey, context, fetcher);
}, [cacheKey, context, fetcher]);
if (!value) {
const promise = reload();
if (typeof window === "undefined") {
throw promise;
}
} else if (!value.fetcher) {
value.fetcher = fetcher;
}
return { ...value, reload };
};
/**
* Transfer of SSR data to clients
*/
const DataRender = () => {
const context = useContext(promiseContext);
if (typeof window === "undefined" && !context.finished)
throw Promise.allSettled(context.promises).then((v) => {
context.finished = true;
return v;
});
return (
<script
id={DATA_NAME}
type="application/json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(context.values) }}
/>
);
};
/**
* Context data initialisation hook
*/
const useContextValue = () => {
const refContext = useRef<ContextType>({
values: {},
promises: [],
finished: false,
renderMap: new Map<string, Set<Render>>(),
});
if (typeof window !== "undefined" && !refContext.current.finished) {
const node = document.getElementById(DATA_NAME);
if (node) refContext.current.values = JSON.parse(node.innerHTML);
refContext.current.finished = true;
}
return refContext.current;
};
/**
* Provider for asynchronous data management
*/
export const SSRProvider = ({ children }: { children: ReactNode }) => {
const value = useContextValue();
return (
<promiseContext.Provider value={value}>
{children}
<DataRender />
</promiseContext.Provider>
);
};
Summary
In Next.js SSR, waiting for asynchronous data has become possible through throw promise, simplifying implementation. While this article introduces how to retrieve asynchronous data using fetch, implementations are similarly possible when using libraries that allow throw promise on the server side, such as urql.
On the other hand, I attempted to support Recoil and SWR, but at this time, implementing SSR via throw promise was structurally impossible. @apollo/client has become compatible starting from version 3.8, and while verification code I created for trial purposes succeeded, version 3.8 is still in the alpha stage.
In the future, we can expect SSR implementation to become even easier as more libraries begin to call throw promise internally.
By the way, it seems Vercel wants to push the appDir approach for SSR-related features, but the specification that forces React Server Components from the start seems likely to have quite limited use cases.
- Source Code
https://github.com/SoraKumo001/next-ssr-sample - Live Demo
https://next-use-ssr.vercel.app/
Discussion