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

SSR and cache-and-network fetch policy #2119

Closed
mxstbr opened this issue Sep 5, 2017 · 36 comments · Fixed by #3372
Closed

SSR and cache-and-network fetch policy #2119

mxstbr opened this issue Sep 5, 2017 · 36 comments · Fixed by #3372
Assignees
Labels

Comments

@mxstbr
Copy link
Contributor

mxstbr commented Sep 5, 2017

Intended outcome:

I server-side render my React application. I used renderToStringWithData on the backend to get all the data, then serialized the state with Yahoos serialize-javascript into the generated HTML and then added it to my custom Redux store as the initial state via window.__SERVER_STATE__.

I expected the client bundle to load in and not fetch anything.

Actual outcome:

The client bundle loads in and React rehydrates, then Apollo fetches /api even though it already has all the data from the initial state in the HTML.

This seems backwards, since even with cache-and-network after the SSR'd first result I wouldn't expect apollo client to go to the network again on first render.

How to reproduce the issue:

  • Have an SSR'd React app
  • Use a fetch policy of cache-and-network
  • Check your network requests

Version

  • apollo-client@1.9.1

/cc @peterpme who alerted me of this issue

@mhuggins
Copy link

I just started using Apollo and am seeing this as well. Not sure if there's an existing solution or if this is a bug.

@mhuggins
Copy link

As a workaround, I'm now using the ssr: false option to prevent loading data on the server, instead just sending the unloaded/loading state back to the client. http://dev.apollodata.com/react/server-side-rendering.html#skip-for-ssr

@stale
Copy link

stale bot commented Oct 7, 2017

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions to Apollo Client!

@mhuggins
Copy link

mhuggins commented Oct 7, 2017

This is still an issue.

@stale stale bot removed the no-recent-activity label Oct 7, 2017
@mxstbr
Copy link
Contributor Author

mxstbr commented Oct 7, 2017

Definitely still an issue.

@leoasis
Copy link
Contributor

leoasis commented Oct 7, 2017

Isn't this expected behavior with cache-and-network? According to the docs here http://dev.apollodata.com/react/api-queries.html#graphql-config-options-fetchPolicy, that policy will always execute a network request even if the data is present in the cache (in which case that data is also returned). Maybe the cache-first policy would have the behavior you'd expect?

@mxstbr
Copy link
Contributor Author

mxstbr commented Oct 7, 2017

No. I want my query to run again when a user visits a page again to get the freshest data, but I also want it to show the stale data for a faster render and nicer UX.

It's just now rendered with the fresh data on the server, why would I want it to stress my server by unnecessarily fetching the exact same queries that were just loaded literally milliseconds ago?

@leoasis
Copy link
Contributor

leoasis commented Oct 7, 2017

Yeah I understand that, just wanted to say that it was kind of the expected behavior IMO, because the component is returning the data from the cache and also firing a network request, as it is documented. The problem though is that in this case, the cache was populated by that same component on the server side, but that fact is not transferred to the client (since it's not part of the store), only the data in the store.

Maybe there could be a way to set the fetchPolicy to cache-first and as soon as the component is filled with the data, it could be changed back to cache-and-network?

To make this work "transparently" the client would have to also save its internal state for the queries being watched, and have some way to rehydrate those based on the components that mount. This looks like it would be very hard to implement.

Another solution would be to ignore the fetch policy when hydrating from the server, and just populate the data from the cache. Something similar to ssrForceFetchDelay.

@jbaxleyiii
Copy link
Contributor

@mxstbr so you are saying the initial data from the SSR isn't present and it resets and makes a network request?

Looking at the intended outcome from the issue, you don't want it to make the request again but that is what that policy is designed to do? I'm not sure I understand what is wrong?

@mxstbr
Copy link
Contributor Author

mxstbr commented Oct 10, 2017

No, the data is there, that's the whole point.

  1. User requests /something
  2. We render that page on the server with renderToStringWithData and send down the full HTML with all the data
  3. The user gets the full HTML and downloads and executes the JS bundle
  4. Now React rehydrates and Apollo rehydrates too
  5. At this point, the user has the full app and the JS bundle has rehydrated
  6. Apollo goes and fetches the data we already have from the SSR initial state again

That sixth step doesn't make any sense: the server just loaded the data and sent it down, why would Apollo go back to the server to get the exact same data again?!

@jbaxleyiii
Copy link
Contributor

@mxstbr that totally shouldn't be happening! Does that only happen with certain fetch policies? Can you share your initialization code and SSR code where you inline the state? I've got a few SSR apps and they don't double request so I'm looking to narrow down the issue!

@mxstbr
Copy link
Contributor Author

mxstbr commented Oct 10, 2017

It only happens with cache-and-network, so it seems to be semi-intended behaviour but to me that's wrong behaviour since it doesn't make any sense to give my server double the load. 😅

You can replicate this in your own browser by opening https://spectrum.chat/styled-components in a private browser window (make sure to clear your browser cache each time since we cache the app shell locally with service workers) and looking at the network tab. You'll get the SSR'd HTML, but then after the JS bundle loads you'll see a request to /api that loads the entire data again:

screen shot 2017-10-10 at 8 45 18 pm

Note: Sorry for the german browser interface, Firefox Nightly for some reason doesn't use the OS language but is based on location or something 🤷‍♂️

Client initialisation:

const store = initStore(window.__SERVER_STATE__ || initialState, {
  middleware: [client.middleware()],
  reducers: {
    apollo: client.reducer(),
  },
});

function render() {
  return ReactDOM.render(
    <ApolloProvider store={store} client={client}>
      <Router history={history}>
        <Routes
          maintenanceMode={process.env.REACT_APP_MAINTENANCE_MODE === 'enabled'}
        />
      </Router>
    </ApolloProvider>,
    document.querySelector('#root')
  );
}

Client creation:

const networkInterface = createBatchingNetworkInterface({
  uri: API_URI,
  batchInterval: 10,
  opts: {
    credentials: 'include',
  },
});

const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
  networkInterface,
  wsClient
);

const fragmentMatcher = new IntrospectionFragmentMatcher({
  introspectionQueryResultData,
});

export const client = new ApolloClient({
  networkInterface: networkInterfaceWithSubscriptions,
  fragmentMatcher,
  initialState: window.__SERVER_STATE__ && {
    apollo: window.__SERVER_STATE__.apollo,
  },
  ssrForceFetchDelay: 100,
  ...getSharedApolloClientOptions(),
});

Server (express.js) route:

const renderer = (req, res) => {
  // apollo
  const client = new ApolloClient({
    ssrMode: true,
    networkInterface: createLocalInterface(graphql, schema, {
      context: {
        loaders: createLoaders(),
        user: req.user,
      },
    }),
    ...getSharedApolloClientOptions(),
  });
  // redux
  const initialReduxState = {
    users: {
      currentUser: req.user,
    },
  };

  const store = initStore(initialReduxState, {
    // Inject the server-side client's middleware and reducer
    middleware: [client.middleware()],
    reducers: {
      apollo: client.reducer(),
    },
  });
  const context = {};

  // Frontend
  const frontend = (
    <ApolloProvider store={store} client={client}>
      <StaticRouter location={req.url} context={context}>
        <Routes maintenanceMode={IN_MAINTENANCE_MODE} />
      </StaticRouter>
    </ApolloProvider>
  );
  // styled-components
  const sheet = new ServerStyleSheet();
  renderToStringWithData(sheet.collectStyles(frontend))
    .then(content => {
      if (context.url) {
        res.redirect(301, context.url);
        return;
      }
      const state = store.getState();
      // react-helmet for meta tags
      const helmet = Helmet.renderStatic();
      res.status(200);
      // Compile the HTML and send it down
      res.send(
        constructHTML({
          content,
          state,
          styleTags: sheet.getStyleTags(),
          metaTags:
            helmet.title.toString() +
            helmet.meta.toString() +
            helmet.link.toString(),
        })
      );
      res.end();
    })
    .catch(err => {
      console.log(err);
      res.status(500);
      res.end();
      throw err;
    });
};

@stale
Copy link

stale bot commented Oct 31, 2017

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions to Apollo Client!

@mxstbr
Copy link
Contributor Author

mxstbr commented Oct 31, 2017

Argg, not stale. Thank god I can label it myself now...

@stale
Copy link

stale bot commented Nov 21, 2017

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions to Apollo Client!

@mxstbr
Copy link
Contributor Author

mxstbr commented Nov 21, 2017

Wait that still doesn't work, wat

@mhuggins
Copy link

Still not stale.

@particle4dev
Copy link

@mxstbr What version of apollo you use ? I have same issue with apollo-client@2.1.0 then I realized that I did not migrated code from 1.0 correctly.
Change

     return new ApolloClient({
      cache,
      initialState,

to

     return new ApolloClient({
      cache: cache.restore(initialState),

solved my problem. Hope this help :)

@mxstbr
Copy link
Contributor Author

mxstbr commented Dec 3, 2017

I'm on 1.0

@benjie
Copy link
Contributor

benjie commented Jan 10, 2018

This issue persists with Apollo 2.0 if you use cache-and-network fetch policy, e.g.:

const client = new ApolloClient({
  link,
  cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
  defaultOptions: {
    query: {
      fetchPolicy: "cache-and-network",
    },
  },
  ssrForceFetchDelay: 1000,
});

It would be nice to be able to use "cache-first" as the network policy for the first second after SSR render, then switch to "cache-and-network" after - an option like ssrCacheFirstFetchPolicyDuration: 1000 might help?

@benjie
Copy link
Contributor

benjie commented Jan 10, 2018

It seems like the issue could be solved around here:

if (this.disableNetworkFetches && options.fetchPolicy === 'network-only') {
options = { ...options, fetchPolicy: 'cache-first' } as WatchQueryOptions;
}

Maybe something like this would work:

    if (this.disableNetworkFetches && (
      options.fetchPolicy === 'network-only' || options.fetchPolicy === 'cache-and-network'
    )) {
      options = { ...options, fetchPolicy: 'cache-first' } as WatchQueryOptions;
    }

Would a PR performing this change be considered?

@dmitriy-baltak
Copy link

Seems like an easy fix for a long open issue

@benjie
Copy link
Contributor

benjie commented Feb 9, 2018

I'd be more than happy to fix this in a PR if I can get a collaborator to just give me the okay.

@jbaxleyiii
Copy link
Contributor

@benjie go for it!

@alexandcote
Copy link

@benjie any update on this ?

@benjie
Copy link
Contributor

benjie commented Mar 26, 2018

Sorry I’ve not had time :( A potential fix is detailed above though, feel free to give it a go yourself.

@ciscorn
Copy link

ciscorn commented May 28, 2018

#3372 should be reviewed and merged.

@hwillson
Copy link
Member

@ciscorn It will be reviewed shortly (it's on my radar for hopefully later today). Thanks!

@aronddadi
Copy link

Any news?

@vincentbello
Copy link

Was this ever fixed? I'm still getting the exact same issue @mxstbr was reporting, where cache-and-network queries refetch once the client bundle loads in.
Is there a way to ignore the fetch policy when hydrating from the server and populate data from the cache?

@6axter82
Copy link

Is it still open or merged?

@sgup
Copy link

sgup commented Sep 20, 2019

Having this issue too.

My use case was: View page -> Edit -> Save -> Back to View. It was still returning the cached version.

Ended up using a query search param:

let fetchPolicy = "cache-first";
if (this.props.location.search === "?updated") {
  fetchPolicy = "network-only";
}
<Query
  // ...
  fetchPolicy={fetchPolicy}
>
// ....

@geraldstrobl
Copy link

Has anyone a workaround for this issue?

@tomraithel
Copy link

The solution to this problem is setting an appropriate ssrForceFetchDelay:

If you are using fetchPolicy: network-only or fetchPolicy: cache-and-network on some of the initial queries, you can pass the ssrForceFetchDelay option to skip force fetching during initialization, so that even those queries run using the cache:

As stated on the Apollo SSR docu.

@Moniv9
Copy link

Moniv9 commented Jul 22, 2020

Is this resolved? As the same is still reproducible.

@darrylmabini
Copy link

Boom! using ssrForceFetchDelay do the trick.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.