A very hacky hack for SSR cache merges

Ok! I think I got it! It's very very nastly tho lmao! But this will merge in the new SSR-provided data before the new page can render, instead of having it sometimes make redundant network requests & show loading spinners in the meantime for data that Next.js already fulfilled for it.

Nasty nasty lil trick. But it seems to be working! Let's see how it does lmao
This commit is contained in:
Emi Matchu 2022-09-15 04:45:44 -07:00
parent 0176792cb9
commit 193e993095

View file

@ -41,15 +41,6 @@ export default function DTIApp({ Component, pageProps }: AppPropsWithLayout) {
const renderWithLayout = const renderWithLayout =
Component.renderWithLayout ?? renderWithDefaultLayout; Component.renderWithLayout ?? renderWithDefaultLayout;
// Store the *first* value of initialCacheState we get into our cache,
// because we don't want to rebuild our client and flush out everything else
// when the page changes. We set it in state here then never touch it again!
// TODO: Is there a clever way to *add* to our cache each time? The Apollo
// API suggests not really, but maybe there's a clever option...
const [initialCacheState, unusedSetInitialCacheState] = React.useState(
pageProps.graphqlState ?? {}
);
React.useEffect(() => setupLogging(), []); React.useEffect(() => setupLogging(), []);
return ( return (
@ -70,12 +61,12 @@ export default function DTIApp({ Component, pageProps }: AppPropsWithLayout) {
audience="https://impress-2020.openneo.net/api" audience="https://impress-2020.openneo.net/api"
scope="" scope=""
> >
<ApolloProviderWithAuth0 initialCacheState={initialCacheState}> <DTIApolloProvider additionalCacheState={pageProps.graphqlState ?? {}}>
<ChakraProvider theme={theme}> <ChakraProvider theme={theme}>
<CSSReset /> <CSSReset />
{renderWithLayout(<Component {...pageProps} />)} {renderWithLayout(<Component {...pageProps} />)}
</ChakraProvider> </ChakraProvider>
</ApolloProviderWithAuth0> </DTIApolloProvider>
</Auth0Provider> </Auth0Provider>
</> </>
); );
@ -85,12 +76,12 @@ function renderWithDefaultLayout(children: JSX.Element) {
return <PageLayout>{children}</PageLayout>; return <PageLayout>{children}</PageLayout>;
} }
function ApolloProviderWithAuth0({ function DTIApolloProvider({
children, children,
initialCacheState, additionalCacheState,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
initialCacheState: NormalizedCacheObject; additionalCacheState: NormalizedCacheObject;
}) { }) {
const auth0 = useAuth0(); const auth0 = useAuth0();
const auth0Ref = React.useRef(auth0); const auth0Ref = React.useRef(auth0);
@ -99,6 +90,12 @@ function ApolloProviderWithAuth0({
auth0Ref.current = auth0; auth0Ref.current = auth0;
}, [auth0]); }, [auth0]);
// Save the first `additionalCacheState` we get as our `initialCacheState`,
// which we'll use to initialize the client without having to wait a tick.
const [initialCacheState, unusedSetInitialCacheState] = React.useState(
additionalCacheState
);
const client = React.useMemo( const client = React.useMemo(
() => () =>
buildApolloClient({ buildApolloClient({
@ -107,6 +104,42 @@ function ApolloProviderWithAuth0({
}), }),
[initialCacheState] [initialCacheState]
); );
// When we get a new `additionalCacheState` object, merge it into the cache:
// copy the previous cache state, merge the new cache state's entries in,
// and "restore" the new merged cache state.
//
// HACK: Using `useMemo` for this is a dastardly trick!! What we want is the
// semantics of `useEffect` kinda, but we need to ensure it happens
// *before* all the children below get rendered, so they don't fire off
// unnecessary network requests. Using `useMemo` but throwing away the
// result kinda does that. It's evil! It's nasty! It's... perfect?
// (This operation is safe to run multiple times too, in case memo
// re-runs it. It's just, y'know, a performance loss. Maybe it's
// actually kinda perfect lol)
//
// I feel like there's probably a better way to do this... like, I want
// the semantic of replacing this client with an updated client - but I
// don't want to actually replace the client, because that'll break
// other kinds of state, like requests loading in the shared layout.
// Idk! I'll see how it goes!
React.useMemo(() => {
const previousCacheState = client.cache.extract();
const mergedCacheState = { ...previousCacheState };
for (const key of Object.keys(additionalCacheState)) {
mergedCacheState[key] = {
...mergedCacheState[key],
...additionalCacheState[key],
};
}
console.debug(
"Merging Apollo cache:",
additionalCacheState,
mergedCacheState
);
client.cache.restore(mergedCacheState);
}, [client, additionalCacheState]);
return <ApolloProvider client={client}>{children}</ApolloProvider>; return <ApolloProvider client={client}>{children}</ApolloProvider>;
} }