I see three options to achieve SSR-only data fetching once for things that won't ever change between page transitions:
1. getInitialProps() in _app.ts
You can just use getInitialProps() in _app.tsx. This runs on the server first and you can just cache the response value in a variable. Next time getInitialProps() is executed, it will just serve the cached value instead of firing another request. To make this work client-side, you have to rehydrate the cache variable in an useEffect:
// pages/_app.tsx
let navigationPropsCache
function MyApp({ Component, pageProps, navigationProps }) {
useEffect(
()=>{
navigationPropsCache = navigationProps
},
[]
)
return <>
<Navigation items={navigationProps}/>
<Component {...pageProps} />
</>
}
MyApp.getInitialProps = async () => {
if(navigationPropsCache) {
return {navigationProps: navigationPropsCache}
}
const res = await fetch("http://localhost:3000/api/navigation")
const navigationProps = await res.json()
navigationPropsCache = navigationProps
return {navigationProps}
}
Note that getInitialProps() is a deprecated feature since next 9.3. Not sure how long this will be supported in the the future. See: https://nextjs.org/docs/api-reference/data-fetching/getInitialProps
See https://github.com/breytex/firat500/tree/trivial-getInitialProps for full code example.
2. Use a custom next server implementation
This solution is based on two ideas:
- Use a custom server.ts to intercept the nextjs SSR feature. Fetch all the data you need, render the navbar and footer serverside, inject the component HTML into the SSR result.
- Rehydrate the DOM based on stringified versions of the fetched data you also attached to the DOM as a
<script>.
// server/index.ts
server.all("*", async (req, res) => {
const html = await app.renderToHTML(req, res, req.path, req.query);
const navigationProps = await getNavigationProps()
const navigationHtml = renderToString(React.createElement(Navigation, {items: navigationProps}))
const finalHtml = html
.replace("</body>", `<script>window.navigationProps = ${JSON.stringify(navigationProps)};</script></body>`)
.replace("{{navigation-placeholder}}", navigationHtml)
return res.send(finalHtml);
});
// components/Navigation.tsx
export const Navigation: React.FC<Props> = ({items})=>{
const [finalItems, setFinalItems] = useState(items ?? [])
useEffect(
()=>{
setFinalItems((window as any).navigationProps)
},
[]
)
if(!Array.isArray(finalItems) || finalItems.length === 0) return <div>{"{{navigation-placeholder}}"}</div>
return (
<div style={{display:"flex", maxWidth: "500px", justifyContent: "space-between", marginTop: "100px"}}>
{finalItems.map(item => <NavigationItem {...item}/>)}
</div>
)
}
I'd consider this a pretty dirty example for now, but you could build something powerful based on this.
See full code here: https://github.com/breytex/firat500/tree/next-link-navigation
3. Use react-ssr-prepass to exec all data fetching server side
- This uses a custom made fetch wrapper which has some kind of cache
- The React component tree is traversed server side, and all data fetching functions are executed. This populates the cache.
- The state of the cache is sent to the client and rehydrates the client side cache
- On DOM rehydration all data is served from that cache, so no request is sent a second time
This example is a little bit longer and based on the outstanding work of the urql project: https://github.com/FormidableLabs/next-urql/blob/master/src/with-urql-client.tsx
See full example here: https://github.com/breytex/firat500/tree/prepass
Conclusion:
I'd personally would go with option #1 as long as its feasible.
#3 looks like an approach with a good developer experience, suitable for bigger teams. #2 needs some love to actually be useful :D