with-next-promise-tree-walker
v1.1.2
Published
SSR data fetching using on component level without having to (pre)fetch in getInitialProps
Downloads
19
Maintainers
Readme
with-next-promise-tree-walker
SSR data fetching using on component level without having to (pre)fetch in getInitialProps
What is it?
This package consists of a getPromiseDataFromTree
method, a PromiseCache
class and a usePromise
hook.
With this package you can achieve SSR data fetching on component level without having to (pre)fetch in getInitialProps.
This function walks down the entire React tree and executes every required promise it encounters (including nested promises).
When the Promise resolves, you're ready to render your React tree and return it, along with the current state of the cache.
Sidenote! This library walks down the entire React tree on SSR. It'll run renderToStaticMarkup from ReactDOMServer. Note that this is the React SSR API and means that it does a full server-render of the whole React tree.
Note that renderToStaticMarkup is a synchronous run to completion method, meaning that it can't await promises as of right now (Suspense might solve this).
In practice though you have usePromise components deeply nested in the React tree. React can't await those as said, so this is worked around by throwing a promise every time a query is found.
When the promise is thrown that is awaited and then the rendering starts again, from the beginning of the tree.
This means that if you have nested queries you cause a lot of full server-renders.
This solution might cause you a performance overhead. But try it out and see if it is a bottleneck for you!
Usage
See the example
folder for a full fletched example on how to use this.
Start of by creating a custom Higher Order Component that uses PromiseCache and the getPromiseDataFromTree method:
import { NextPage, NextPageContext } from 'next';
import App, { AppContext, AppInitialProps } from 'next/app';
import Head from 'next/head';
import React, { useMemo } from 'react';
import { InitialCache, PromiseCache } from 'with-next-promise-tree-walker';
type WithPromisesContext = AppContext & NextPageContext;
export interface IWithPromiseCacheSSRProps {
promises?: PromiseCache
initialCache?: InitialCache
}
function getDisplayName(Component: React.ComponentType<any>) {
return Component.displayName || Component.name || 'Unknown';
}
export default function WithPromiseCacheSSR<T>(PageComponent: NextPage<any> | typeof App) {
const PromiseCacheContext = PromiseCache.getContext();
function WithPromises({ initialCache, promises, ...props }: IWithPromiseCacheSSRProps) {
const _promises = useMemo<PromiseCache>(() => promises || new PromiseCache({ isSSR: false }), [promises]);
if (initialCache && Object.keys(initialCache).length) {
_promises.setInitialCacheResult(initialCache);
}
return (
<PromiseCacheContext.Provider value={_promises}>
<PageComponent {...props} />
</PromiseCacheContext.Provider>
);
}
if (process.env.NODE_ENV === 'development') {
WithPromises.displayName = `WithPromises(${getDisplayName(PageComponent)})`;
}
WithPromises.getInitialProps = async (ctx: WithPromisesContext) => {
const { AppTree } = ctx;
const isInAppContext = Boolean(ctx.ctx);
let pageProps = {};
if (PageComponent.getInitialProps) {
pageProps = { ...pageProps, ...(await PageComponent.getInitialProps(ctx)) };
}
if (typeof window !== 'undefined') {
return pageProps;
}
if (ctx.res && (ctx.res.headersSent || ctx.res.writableEnded)) {
return pageProps;
}
const promises = new PromiseCache({ isSSR: true });
try {
const { getPromiseDataFromTree } = await import('with-next-promise-tree-walker');
// Since AppComponents and PageComponents have different context types
// we need to modify their props a little.
let props;
if (isInAppContext) {
props = { ...pageProps, promises };
} else {
props = { pageProps: { ...pageProps, promises } };
}
await getPromiseDataFromTree(<AppTree {...props as AppInitialProps} />, { promises });
} catch (error) {
promises.seal();
console.error('Error while running `getPromiseDataFromTree`', error);
}
// Head side effect therefore need to be cleared manually
Head.rewind();
return {
...pageProps,
initialCache: promises.getInitialCacheResult(),
};
};
return WithPromises;
}
In your Custom _app.tsx add the HOC around your app:
import { WithPromiseCacheSSR } from '../hocs'
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default WithPromiseCacheSSR(MyApp)
In your component/page you need to use the usePromise()
hook. This example uses Typescript.
usePromise() also exports a run() function. If you turn skip to true you can manually fetch the data by calling the exported run() function.
import { usePromise } from 'with-next-promise-tree-walker'
interface VercelRepo {
name: string
description: string
subscribers_count: number
stargazers_count: number
forks_count: number
}
const fetcher = (url: string) => fetch(url).then(res => res.json());
const SomePage: React.FC = () => {
const { isLoading, data, error } = usePromise<VercelRepo>('repos/vercel/swr', () => fetcher('https://api.github.com/repos/vercel/swr'), { ssr: true, skip: false });
if (error) return <div>An error has occurred</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return <div>No results found.</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{" "}
<strong>✨ {data.stargazers_count}</strong>{" "}
<strong>🍴 {data.forks_count}</strong>
</div>
);
};
Authors
Made by Daphne Smit
Prior Art
The approach of doing an initial "data fetching pass" is inspired by:
Production Build
Run npm run build
to build a file for production and emit the types
Development Build
Run npm run dev
to build a file for development
Contributing
You are free to contribute to this project! Please use a conventional commit and make pull requests to the develop branch (pre-release branch).