Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract Apollo cache for SSR #22

Closed
tcastelly opened this issue May 5, 2021 · 14 comments
Closed

Extract Apollo cache for SSR #22

tcastelly opened this issue May 5, 2021 · 14 comments

Comments

@tcastelly
Copy link

tcastelly commented May 5, 2021

Hello,

I'm trying to use Apollo Client with SSR and React.
My idea was to create a InMemoryCache and extract data from the store. It seams that viteSSR does not wait the resolution of a query. If I add a setTimeout to the hook of viteSSR, I cant print the good content of the store.
According the Apollo Documentation, I have to use renderToStringWithData. But I have no idea how I can manage with vite-ssr plugin. Is it possible?

Thank you very much.

./server/index.js

// This is the server renderer we just built
const main = import('../dist/server/main.js');

server.get('*', async (req, res) => {
  const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
  const renderPage = (await main).default.default;

  const apolloCache = new InMemoryCache();

  const { html } = await renderPage(url, {
    manifest,
    apolloCache,
    preload: true,
  });

  res.setHeader('Cache-Control', 'max-age=0');
  res.end(html);
});

./src/App.jsx

const App = ({
  isClient,
  apolloCache = new InMemoryCache(),
}) => {
  const client = new ApolloClient({
    link: createHttpLink({
      uri: 'http://localhost:8080/graph',
      credentials: 'same-origin',
    }),
    ssrMode: !isClient,
    cache: apolloCache,
    credentials: 'same-origin',
  });
...

./src/main.jsx

export default viteSSR(App, { routes }, ({
  initialState,
  apolloCache,
}) => {
  if (import.meta.env.SSR) {
    setTimeout(() => {
      console.log(apolloCache.extract()) // not empty
    }, 1000)
    initialState.apolloCache = apolloCache.extract();
  }
  // Custom initialization hook
})
@frandiox
Copy link
Owner

frandiox commented May 6, 2021

Hi @tcastelly ! Tell me if something like this works in your project:

// This is the server renderer we just built
const main = import('../dist/server/main.js');

server.get('*', async (req, res) => {
  const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
  const renderPage = (await main).default.default;

  const apolloCache = new InMemoryCache();

  const { html } = await renderPage(url, {
    manifest,
-   apolloCache,
+   initialState: { apolloCache },
    preload: true,
  });

  res.setHeader('Cache-Control', 'max-age=0');
  res.end(html);
});
const App = ({
  isClient,
- apolloCache = new InMemoryCache(),
+ initialState,
}) => {
  const client = new ApolloClient({
    link: createHttpLink({
      uri: 'http://localhost:8080/graph',
      credentials: 'same-origin',
    }),
    ssrMode: !isClient,
-   cache: apolloCache,
+   cache: initialState.apolloCache,
    credentials: 'same-origin',
  });
export default viteSSR(App, {
  routes,
  transformState(state) {
    if (import.meta.env.SSR) {
      // Serialize
      state.apolloCache = state.apolloCache.extract()
      return JSON.stringify(JSON.stringify(state))
    } else {
      // Deserialize
      state = JSON.parse(state)
      state.apolloCache = new InMemoryCache().restore(JSON.parse(state.apolloCache))
      return state
    }
  }
}, (ctx) => {
  // Custom initialization hook
})

If this works, I will add a second parameter defaultTransformer to the transformState hook so you don't need to care about stringify/parse and escaping dangerous characters for XSS.

@tcastelly
Copy link
Author

tcastelly commented May 6, 2021

Thank you for your help.

In the main.jsx, the state is always null.
I created a project it can be easier to test:

https://github.com/shenron/vite-ssr-plugin-react-demo

@frandiox
Copy link
Owner

frandiox commented May 9, 2021

@tcastelly Ah right, I forgot about the development server. This fixes it and is a bit simpler:

export default viteSSR(App, {
  routes,
  transformState(state) {
    if (import.meta.env.SSR) {
      // Serialize
      state.apolloCache = state.apolloCache.extract();
      return JSON.stringify(JSON.stringify(state));
    }

    // Deserialize
    return JSON.parse(state);
  },
}, ({ initialState }) => {
  // Custom initialization hook
  if (import.meta.env.SSR) {
    initialState.apolloCache = new InMemoryCache();
  } else {
    initialState.apolloCache = new InMemoryCache().restore(initialState.apolloCache);
  }
});

You can stop passing initialState: { apolloCache } from your production server since it's already done in the main hook.

@frandiox
Copy link
Owner

frandiox commented May 9, 2021

@tcastelly I've added defaultTransformer in 0.8.1. You can simplify the transformState like this (keep the code in the main hook as in the previous comment):

transformState(state, defaultTransformer) {
  if (import.meta.env.SSR) {
    state.apolloCache = state.apolloCache.extract();
  }

  return defaultTransformer(state);
}

@tcastelly
Copy link
Author

tcastelly commented May 10, 2021

Thank you for your help. I updated the plugin and files like this:

./back/index.mjs

const { html } = await renderPage(url, {
  manifest,
  initialState: { },
  preload: true,
});

./client/main.jsx

export default viteSSR(App, {
  routes,
  transformState(state, defaultTransformer) {
    if (import.meta.env.SSR) {
      state.apolloCache = state.apolloCache.extract();
    }

    return defaultTransformer(state);
  },
}, ({ initialState }) => {
  // Custom initialization hook
  if (import.meta.env.SSR) {
    initialState.apolloCache = new InMemoryCache();
  } else {
    initialState.apolloCache = new InMemoryCache().restore(initialState.apolloCache);
  }
});

Maybe I missed something, but The behavior is the same. The SSR does not wait the end of the Graphql query.
I have "Loading ..." in my DOM and the cache is empty:

...
<script>window.__INITIAL_STATE__="{\"apolloCache\":{}}"</script>
...

I created an other project without the ssr plugin:
https://github.com/shenron/vite-ssr-react-demo/

The only way that I found to fix my issue is to use:

import { getDataFromTree } from '@apollo/client/react/ssr';

https://github.com/shenron/vite-ssr-react-demo/blob/es6/src/entry-server.jsx#L22

@frandiox
Copy link
Owner

Ah I see, so the state now works but it cannot await for queries. vite-ssr relies on Suspense viareact-ssr-prepass. Looks like some updates in Apollo 3 have removed Suspense support so it's tricky to make it work:

I wasn't aware that Apollo is not compatible with Suspense. I'll have a deeper look at it later this week and see if there's anything that we can do here.

@frandiox
Copy link
Owner

@tcastelly I've been playing with Apollo internals for a while and I think I got it working. The simplest solution is probably using getDataFromTree instead of React's renderToString until Apollo supports Suspense. I'll try to figure out a way to provide it in vite-ssr API soon 👍

@tcastelly
Copy link
Author

tcastelly commented May 12, 2021

Thank you for your help.

I have issue with getDataFromTree during the "hydration", this article helped me:
https://lihautan.com/hydrating-text-content/

So instead I use renderToStringWithData.

@frandiox
Copy link
Owner

@tcastelly Can you check v0.9.0? It should use renderToStringWithData automatically if it detects you have @apollo/client and @vitejs/plugin-react-refresh installed.

I tried it in your repo and it does await for the loading variable to become false so it doesn't render Loading... anymore. However, the GraphQL query returns undefined for some reason but that might be related to the API 🤔

@tcastelly
Copy link
Author

It works better thank you :)
But I have a strange behavior.

In this demo project, each page resolve props once and the home page resolve a QraphQL query.

In the ./client/api.jsx I print the state. In ssr mode I see only the apolloCache. Else I see the result of the default query.

// from ssr: { apolloCache: InMemoryCache2 }
// else {body: {…}}
console.log(route.meta.state);
return <Page {...route.meta.state} />;

To reproduce:

npm run dev

Go the the About page from Home. And try to load directly this About page.

Thank you very much for your help.

@frandiox
Copy link
Owner

@tcastelly I think that's expected 🤔 . Only the first route gets the initialState in its route.meta.state after rehydration. Further navigation (SPA mode) to other routes don't get anything in their respective route.meta.state.

I'm not sure what you are trying to accomplish. If you need the whole initialState in every page, you can pass it from your App.jsx as props I guess.

Also, are you going to mix Apollo queries and page-props?

@frandiox
Copy link
Owner

I've added a simple example for manual testing based on your repo here: https://github.com/frandiox/vite-ssr/tree/master/examples/react-apollo

@tcastelly
Copy link
Author

tcastelly commented May 18, 2021

I'm agree, only the first route gets the initialState.

I created this project from your react example and I added an Apollo Query to try. So I mixed Apollo queries and page props. Maybe not a good idea because there is no more props in pages during the first launch.

Anyway, vite-ssr plugin with Apollo works now!
Thank you very much for your time.

Edit: Thank you for the new example with only react apollo

@frandiox
Copy link
Owner

@tcastelly Thanks for all the reproduction and information you provided here, glad it's now working!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants