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

Add queryCache de/rehydration #728

Merged
merged 14 commits into from
Aug 29, 2020
Merged

Conversation

Ephem
Copy link
Collaborator

@Ephem Ephem commented Jul 9, 2020

This is a continuation of things described in issue #461. It's a breakout from the closed PR #570 and builds on the merged PRs #584 and #917.

This PR adds a couple of new public APIs:

  • dehydrate(queryCache, dehydrateConfig)
  • hydrate(queryCache, dehydratedQueries)
  • A new hydration/ReactQueryCacheProvider-component with the additional dehydratedState-prop
  • useHydrate(dehydratedQueries) - A hook that does hydrate for you in a React-compatible way

Together, this provides a way to dehydrate a cache, pass its serialized form over the wire or persist it somehow, and later hydrate those queries back into an active cache. Main goals are to improve support for server rendering and help with things like persisting to localstorage or other storage.

An important feature of these new APIs is that the shape of the dehydrated cache is meant to be a private implementation detail that consumers should not rely on, which needs to be emphasized in the docs. This means that the de/rehydrate functionality can more easily be built upon in the future without breaking changes.

SSR Example with Next.js

❗ This example previously looked different, but has been updated for useHydrate

A minimal example:

// _app.jsx
import { ReactQueryCacheProvider } from 'react-query/hydration'

export default function MyApp({ Component, pageProps }) {
  return (
    <ReactQueryCacheProvider initialQueries={pageProps.initialQueries}>
      <Component {...pageProps} />
    </ReactQueryCacheProvider>
  )
}

// pages/posts.jsx
import { makeQueryCache } from 'react-query'
import { dehydrate } from 'react-query/hydration'

export async function getStaticProps() {
  const queryCache = makeQueryCache()

  await queryCache.prefetchQuery('posts', getPosts)

  return {
    props: {
      initialQueries: dehydrate(queryCache)
    }
  }
}

function Posts() {
  // This useQuery could just as well happen in some deeper child to
  // the "Posts"-page, data will be available immediately either way
  const { data } = useQuery('posts', getPosts)

  // This query was not prefetched on the server and will not start
  // fetching until on the client, both patterns are fine to mix
  const { data: otherData } = useQuery('posts-2', getPosts)

  // ...
}

Config options

There are a few config options that you can set when dehydrating and hydrating. Most notably is the shouldDehydrate-function, that can filter the queryCache for queries to dehydrate. Usecase when dehydrating is to filter out only the queries you want to persist to localstorage for example.

With or without shouldDehydrate, only successful queries are dehydrated from the cache

dehydrateConfig

type ShouldDehydrateFunction = <TResult, TError = unknown>(
  query: Query<TResult, TError>
) => boolean

interface DehydrateConfig {
  shouldDehydrate?: ShouldDehydrateFunction
}

Questions

These are a few questions I'm not totally certain about myself and might be worth an extra look when reviewing.

Should hydration be its own entry point?

Solved, is now its own entry point

~~Should dehydrateQuery be included in the official APIs?~~~

Solved, was removed in favour of a shouldDehydrate-function to keep the API lean. Could be exposed publically later on if there is need for it.

How should we handle updatedAt?

Solved, updatedAt is now hydrated and also used for determining how far into the future staleness should be scheduled.

Can naming be improved?

There are a bunch of new public APIs, so let's get the naming juust right!


I'm sure I've missed a bunch of stuff I wanted to highlight and/or ask, but hopefully this is a good start for getting feedback and questions. 😅

@MichaelDeBoey MichaelDeBoey marked this pull request as draft July 10, 2020 12:42
@tannerlinsley
Copy link
Collaborator

I'm loving this. I think something we should consider here is making this its own bundle via rollup, then proxying it with a nested import:

  • Rollup creates similar packages for both the core and the hydration utilities. The existing stuff doesn't need to change, but we can add a new configuration to rollup for hydration and have it place the files in dist/hydration/...
  • Create a file in the root of the repo called hydration.js, which is essentially the same as /index.js, but points the hydration files instead.

This should keep the bundle size down for the default use case.

@tannerlinsley
Copy link
Collaborator

As for updatedAt, technically it is UTC because it uses Date.now(), right? Wouldn't this mean that it would be okay to included in the dehydration? Then really what should happen on hydration is that stale and garbage collection times should be based off of that time (or immediately, if it's too far in the past).

@Ephem
Copy link
Collaborator Author

Ephem commented Jul 24, 2020

Sorry for the radio silence, busy summer. 😄

  • I'll go ahead and separate this out into its own entry point.
  • I'm not sure why I brainfarted on the updatedAt, you are right of course. Requiring server time to be set correctly for staleness to work correctly isn't such a big ask so I think we should definitely base the stale time off of that when hydrating! I'll fix that too.

Another thing I've realized is that the Next-example in the description wont work since setting up the cache per page would blow away the entire cache on page transitions. This might mean we want to separate out the hydration-specific functionality from ReactQueryCacheProvider, possibly to a hook instead. This would also move that to the separate entry point as a bonus.

I'll start working on this now, but might take a little more time figuring out the details (and tests).

What do you think about releasing this as unstable_ or something similar at the start before we have docs, examples etc in place, as well as giving it some time to mature?

@vercel
Copy link

vercel bot commented Jul 25, 2020

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/tannerlinsley/react-query/oaqma3u13
✅ Preview: https://react-query-git-fork-ephem-hydration.tannerlinsley.vercel.app

serverQueryCache.clear({ notify: false })
})

test('should handle global cache case', async () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to handle this case properly. Right now it works on the client, but kind of fails silently on the server (since queries wont actually get added to the cache there). Using the global cache with hydration could be nice in some cases with custom SSR with a separate client entry-point, but is a footgun in Next.

Since v3 will remove the global cache, maybe this behavior should be changed to throw an error if no custom cache has been provided via a ReactQueryCacheProvider instead?

@Ephem
Copy link
Collaborator Author

Ephem commented Jul 26, 2020

Okay, this PR has been updated with a bunch of stuff.

  • Hydration is now its own entry point react-query/hydration
  • initialQueries was removed again from ReactQueryCacheProvider, instead a new useHydrate-hook was introduced
  • updatedAt is now included when de/rehydrating
  • When scheduling staleness on hydration, it is now measured from the point in time the data was fetched on the server (updatedAt), instead of the time of hydration.

I also updated the description above, including the Next.js-example. Except for missing types, docs and examples I hope this is in a pretty good place now and ready for review. I still need to verify a few things so I'll leave the WIP on a while longer.

Two questions:

  • Should we make this an unstable feature at first to make sure it works as we've intended and for all use-cases before we commit to the design fully?
  • Which of types, docs and examples would you like to see in this PR as opposed to in follow-ups?

@@ -0,0 +1,22 @@
import React from 'react'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hook is included in the hydration-bundle which makes the bundle React-specific. I think this makes sense for now to keep down default payload, but that this should probably move to the React-bundle if core and react are split to separate bundles in the future to keep hydration framework agnostic.

@kamranayub
Copy link
Contributor

kamranayub commented Jul 27, 2020

I am not quite sure where to put this (maybe in v3 issue) but the approach we took in our platform was wrapping react-query with our own thin layer that takes care of SSR preloading as well as named query support.

For SSR, I took a pretty naive approach where we use a Map store on the request context (through Next.js) and that store gets serialized to the page on render. I then override initialData in react-query to hydrate from that global storage when available. This lets us achieve most of what we want by using getInitialProps to execute and preload query data and get it hydrated on the client.

Pseudo usage

Homepage.js

import { pageHeading } from 'data/page-heading'

// pageHeading would be a query descriptor
// containing its name, queryFn, default options, etc.

export const Homepage = () => {
  const { data: heading } = pageHeading.useQuery(/* vars, options */);
  
  return (
    <h1>{heading}</h1>
  )
}

Homepage.getInitialProps = (ctx) => {
  await ctx.preloadInitialQuery(pageHeading/*, vars, options */)
  return {};
}

The preloadInitialQuery function inserts the query result into the context request cache:

async function preloadInitialQuery(ctx /* next.js context */, queryDescriptor, vars, options) {
  
  // our executeQuery is a thin wrapper around queryCache.prefetchQuery essentially

  const { data, error } = await executeQuery(queryDescriptor, vars, options);
  if (!error) {

    // set the result for this query key
    // in the per-request context injection store
    ctx.queryStore.set(getQueryKey(queryDescriptor.id, vars), data);
  }
}

On render, the our app server would take that Map and inject it into the page like:

<script>
window.__QUERIES__ = {
  "<QUERY_KEY>": "<QUERY_RESULT>"
};

And our initialData would check for that:

initialData() {
  const queryKey = getQueryKey(name, vars);

  if (window.__QUERIES__[queryKey]) {
    return window.__QUERIES__[queryKey]
  }
  return undefined;
}

By using a Map as the generic store on the request, we were able to use this both within Next.js and outside Next.js (we have two different app servers that use our shared library).

Just wanted to share this approach for future reference/ideas! It works pretty well mainly due to our ability to use named/registered queries so we can always look them up and not have to pass queryFns around.

@Ephem
Copy link
Collaborator Author

Ephem commented Jul 27, 2020

@kamranayub That's a neat solution when using the current React Query, thanks for sharing! What you are doing is essentially implementing a separate cache to use when server rendering, this PR is all about implementing support for this directly in the library so that you don't need to have that separate cache anymore. Besides hopefully reducing complexity for you, this also means:

  • We can transfer some config between server and client (staleTime, cacheTime, updatedAt for now)
  • We can set the query to become stale a certain amount of time after it was fetched on the server
  • We can support Suspense with SSR better in the future (or with third party solutions like react-ssr-prepass)
  • We better support things like persisting the cache to things like localstorage

You probably got this from the description already, just wanted to be clear on the motivations for implementing this as a first class thing. 😄

I'm curious, does it look like this PR would make things easier for you or is it missing something?

Btw, I think the named query registry is still a great idea for other reasons, you probably wanna keep that around either way. 💯

@kamranayub
Copy link
Contributor

Yes, we were looking through this and I think it would help make it easier to integrate with! Since we have two app servers, a Next.js-based one and a custom one, the approach has to end up being able to support a "generic" integration.

@Ephem
Copy link
Collaborator Author

Ephem commented Jul 27, 2020

Glad to hear that! I built this for use in a custom SSR solution myself, so it should absolutely support that.

  • Server
    • Create a cache manually for every request using makeQueryCache
    • Use yourQueryCache.prefetchQuery to prime the cache
    • Place cache on context with ReactQueryCacheProvider
    • Render app
    • Call dehydrate(yourQueryCache)
    • Serialize and embed result in payload like you are doing with window.__QUERIES__
    • Send payload
  • Client
    • Create a cache manually
    • Get dehydrated queries from window.__QUERIES__ (or whatever you called it)
    • Either:
      • Call hydrate(queryCache, dehydratedQueries) and the resulting initializeQueries() directly (low level)
      • Render (with ReactQueryCacheProvider)
    • Or:
      • Render (with ReactQueryCacheProvider)
      • Do useHydrate(dehydratedQueries) in child

Typing out a full example is a bit more verbose with custom SSR of course which is why I left it out of this PR. It will definitely be covered by docs and examples though!

@pseudo-su
Copy link

pseudo-su commented Jul 28, 2020

useHydrate seems like a strange API choice instead of something like

import React from "react";
import {render} from "react-dom";
import {hydrateCache} from "react-query/hydrate";

const hydratedQueryCache = hydrateCache(window.__QUERIES__);

render(
  <ReactQueryCacheProvider cache={hydratedQueryCache} />
    <App />
  </ReactQueryCacheProvider>
)

Is there a reason? Is it to keep things related to hydration out of the main entry point? triggering hydration of the cache using the global data from a child component and effectively passing it up the component tree seems odd.

IMO it would make more sense to provide it at the highest point in the component tree so parent state isn't updated by a child rendering

@Ephem
Copy link
Collaborator Author

Ephem commented Jul 28, 2020

@pseudo-su Great question, this is something I definitely want feedback on so I'm glad you asked.

In Next.js, since we receive the dehydrated queries as props we need to hydrate the queries inside of render. This isn't necessarily great, but I know no other way to solve it currently. Hydrate consists of two parts, first we need to put the queries into the cache and then we need to schedule timeouts for staleness and garbage collection. The first one might not be great to do in render since it could be considered a side effect to add queries to an external cache, but at least it's idempotent, meaning that we can run it several times and get the same outcome, queries already in the cache wont be written over. Scheduling timeouts however is something that should probably happen in an effect instead. (Scheduling timeouts in render is something the other hooks like useQuery do today though, this PR also paves the way to change that if desirable)

When using custom SSR none of this is a problem and you can just do both before even rendering and in fact, the hydrate-function you are suggesting already exists in this PR! The only difference is that in order to separate putting queries into the cache and the scheduling of timeouts, the hydrate in this PR returns a function initializeQueries which you need to call to schedule the timeouts. This feels a bit wonky, and maybe the public API should just be a hydrate-function that does both that you can use in custom SSR, and a useHydrate that you can use inside of render, but if we want to support other frameworks in the future (or just usecases we haven't thought about), it might make sense to provide a low level API as well that lets you separate these. I do worry that it's easy to forget to call the function returned from hydrate though since the API is a bit unusual..

An earlier version of this PR had an extra initialQueries-argument to ReactQueryCacheProvider, but you might want to put hydration and the cache provider in different components. Separating it also means we can move hydration to a separate entry point to avoid increasing the JS-payload for people who don't use hydration. An alternative/complementary API to useHydrate could be to have a ReactQueryHydrator-component or something similar:

function ReactQueryHydrator({ queries, children }) {
  useHydrate(queries)
  return children
}

function App() {
  return (
    <ReactQueryCacheProvider>
      <ReactQueryHydrator queries={dehydratedQueries}>
        <Content />
      </ReactQueryHydrator>
    </ReactQueryCacheProvider>
  )
}

Hydration is purely behaviour though and the rest of React Query is hooks-based, so a hook seemed like a good fit. I'm very open to including such a component as well for conveniency though!

Another difference from your example is that in that, the hydrateCache-function seems to be the one actually creating the cache. This was considered in an earlier version of this PR (#570), but as an argument to makeQueryCache instead, similar to how libraries like Redux does it. I realized that we don't need to constrain ourselves to only hydrating at creation though, it might very well make sense to hydrate several different dehydrated queries and/or at different points in time, for example from both server rendering and localstorage.

To address what seems to be a main concern though, useHydrate isn't actually updating a top components state, only an external cache so the question is, will that update trigger a re-render somewhere? First of all, the queryCache itself is still the same, so ReactQueryCacheProvider wont re-render. Second, if a component is already listening to one of the queries that exists in the dehydratedQueries, this means that query has already been created in the cache (without data) and therefor wont be overwritten by the hydration, so even in that case no re-render should happen. This means useHydrate should only ever affect components further down in the tree which makes sense as you pointed out, you need to hydrate it before you use it.

This became somewhat of a wall of text, but I wanted to try to be as clear as I could about some of the tradeoffs and motivations about the current API design-choices. 😄 Even so, I think there is still room for improvement, so I'm very grateful for all feedback!

@pseudo-su
Copy link

This became somewhat of a wall of text

As someone prone to writing accidental walls of text as well I appreciate the detail 😄. I'll try go through things point by point and then propose/elaborate some thoughts all at once at the end.

In Next.js, since we receive the dehydrated queries as props we need to hydrate the queries inside of render

That does explain why useHydrate is required. My knee-jerk response to that would be why impose hydrating the cache inside of rendering if/when people are doing basic/vanilla SSR without that limitation?

maybe the public API should just be a hydrate-function that does both that you can use in custom SSR, and a useHydrate that you can use inside of render

Wouldn't it be possible to expose the multiple hydration functions and just provide people with the useHydrate and a thing that coordinates them? from what I understand (which might not be accurate) something like:

const queryCache = makeQueryCache();

hydrate(queryCache).initQueries();

but if we want to support other frameworks in the future [...] it might make sense to provide a low level API as well that lets you separate these

I'm not exactly clear on which one of these are being considered as the "low level" vs "high level" API. personally I would consider the "low level" API to be the one that has no knowledge about react and is purely dealing with the queryCache. Which I think I would prefer to use.

An alternative/complementary API to useHydrate could be to have a ReactQueryHydrator component or something similar

If it was necessary to do hydration during the react render I'd personally find it preferable to have the <ReactQueryHydrator /> API (because otherwise everyone doing vanilla SSR has to create it themselves anyway), but I'm not clear on why anyone doing basic SSR would prefer that over just setting up the queryCache outside of react.

Separating it also means we can move hydration to a separate entry point to avoid increasing the JS-payload for people who don't use hydration

Makes sense wanting to keep the entrypoints seperate

Another difference from your example is that in that, the hydrateCache-function seems to be the one actually creating the cache

yeah that was just for terseness (and I wasn't aware of the need to hydrate and also have the seperate initializeQueries), I don't really think it's necessary to have an API that create+hydrates+initializes all in one, but I do think exposing a set of functions to do this would be preferable for anyone doing basic/vanilla SSR.

To address what seems to be a main concern though, useHydrate isn't actually updating a top components state

I did have a quick skim through the code and I saw that it's technically not updating a parent components state, but my first impression was that's what it's function was which made the API feel strange to me. It makes sense that if/when using certain SSR frameworks it's a limitation/requirement to do the hydration as part of the react render. For what it's worth it might just be the name, <ReactQueryHydrator queries={dehydratedQueries}> seemed clearer to me (all in all probably not a big deal tho).

@pseudo-su
Copy link

pseudo-su commented Jul 29, 2020

Is something like this viable?

import React from "react";
import {render} from "react-dom";
import {makeQueryCache} from "react-query";
import {hydrateCache} from "react-query/hydrate";

const queryCache = makeQueryCache();

await hydrate(queryCache, window.__QUERIES__).initQueries();

render(
  <ReactQueryCacheProvider cache={queryCache} />
     <App />
  </ReactQueryCacheProvider>
)

I think there's a real potential benefit of providing an API to use that's totally independent of the react render (not a provider and not a hook) and I think I can provide an example as to why.

Currently I heavily use import() expression syntax in my client entrypoint which allows webpack to do automatic bundle splitting, it looks a bit like this

import "core-js/stable";
import "regenerator-runtime/runtime";

async function loadDependenciesAsync() {
  const loadReact = import(/* webpackPreload: true */ "react").then((m) => ({
    React: m.default,
  }));
  const loadApp = import(/* webpackPreload: true */ "./app");
  const loadLibComponents = import(
    /* webpackPreload: true */ "@mysite/lib-design-system"
  );
  const loadSsrToolbox = import(
    /* webpackPreload: true */ "@mysite/lib-ssr-toolbox/client"
  );

  // Not used directly but still critical-path can good to preload.
  // When making changes to this check the build/webpack.report.html to
  // see how it affects the resulting bundle.
  // prettier-ignore
  {
    void import(/* webpackPreload: true */ "react-dom");
    void import(/* webpackPreload: true */ "react-router");
    void import(/* webpackPreload: true */ "react-router-dom");
  }

  const allLoaded = await Promise.all([
    loadReact,
    loadApp,
    loadLibComponents,
    loadSsrToolbox,
  ]);

  const combined = Object.assign({}, ...allLoaded);

  return combined;
}

void loadDependenciesAsync().then((asyncDeps) => {
  const { React, hydrate, ThemeProvider, App, DefaultTheme } = asyncDeps;
  hydrate({
    render() {
      return (
        <ThemeProvider theme={DefaultTheme}>
          <App />
        </ThemeProvider>
      );
    },
  });
});

If the only API I have is one that runs during the react render then I have to wait for react (and potentially a whole bunch of other libraries) to load before being able to create and hydrate and initialize the queryCache. Alternatively with an API that allowed me to hydrate the cache outside of the react render I could do something like this and initialize the queryCache (potentially) before/while those other JS bundles (react, react-dom etc) are downloading.

async function loadDependenciesAsync() {
  /* etc */
}

async function initQueryCache() {
  const {makeQueryCache} = await import(/* webpackPreload: true */ "react-query");
  const {hydrateCache} = await import(/* webpackPreload: true */ "react-query/hydrate");
  const queryCache = makeQueryCache();

  // maybe this should fail in dev mode but not in prod mode
  await hydrate(queryCache, window.__QUERIES__).initQueries()

  return queryCache;
}

void Promise.all(loadDependenciesAsync(), initQueryCache()).then(([asyncDeps, queryCache]) => {
  const { React, hydrate, ThemeProvider, App, DefaultTheme } = asyncDeps;
  hydrate({
    render() {
      return (
        <ThemeProvider theme={DefaultTheme}>
          <ReactQueryCacheProvider cache={queryCache}>
            <App />
          </ReactQueryCacheProvider>
        </ThemeProvider>
      );
    },
  });
})

@Ephem
Copy link
Collaborator Author

Ephem commented Jul 29, 2020

@pseudo-su ❤️ for the detailed discussion!

That does explain why useHydrate is required. My knee-jerk response to that would be why impose hydrating the cache inside of rendering if/when people are doing basic/vanilla SSR without that limitation?

Just to clear up what seems to be a misunderstanding, we aren't imposing that, the API you are asking for is already included in this PR! It's slightly different (and I'm very open to changing the details), but this works:

import React from "react";
import {render} from "react-dom";
import {makeQueryCache} from "react-query";
import {hydrate} from "react-query/hydrate";

const queryCache = makeQueryCache();

const initQueries = hydrate(queryCache, window.__QUERIES__);
initQueries();

render(
  <ReactQueryCacheProvider cache={queryCache} />
     <App />
  </ReactQueryCacheProvider>
)

An extra detail to note here which is also pretty confusing to communicate is that queries should never be stale or garbage collected before the first render. This would mean different things could render on the server and first render, leading to hydration mismatches.

The example above is safe only because render happens immediately after initQueries, or at least in the same tick. If render would happen in a later tick because of some asynchronous event between them, this could mean trouble.. So await-ing initQueries is actually the opposite of what we want.. So in practice, initQueries should probably be run after the first render:

const initQueries = hydrate(queryCache, window.__QUERIES__);

render(/* ... */);

initQueries();

This is pretty confusing and I don't like this level of detail leaking into the public API, so this is a bit of a sore point currently, I'd love any suggestions to make this less confusing (maybe better naming would help somewhat?). An alternative safer API, but possibly less flexible, could be:

hydrate(queryCache, window.__QUERIES__).then(() => {
  render(/* ... */);
});

This way we could make sure to run the init behind the scenes after calling the thenable? Note that the return wouldn't be a Promise, but rather a synchronous thenable. We could also call it something other than .then to make this clearer. I'm just brainstorming.

Without a doubt we'll support hydrating both inside and outside of render! How we do that and how we best communicate it in docs and examples is very much up for debate though.

Note: render in this entire comment should actually be hydrate when server rendering.. This made me realize calling the API in React Query just hydrate as well is.. not good. hydrateCache makes a lot more sense, I'll change that!

@pseudo-su
Copy link

pseudo-su commented Jul 30, 2020

Just to clear up what seems to be a misunderstanding, we aren't imposing that, the API you are asking for is already included in this PR!

Right 😅. I'm slowly getting onto the same page.

Queries should never be stale or garbage collected before the first render

Ah right yep that makes sense.

I'm curious, what would be the result of never querying initQueries? cache timeouts are never set so cache items hydrated from SSR are never invalidated?

I can see why it would be a good idea to perform the second step of hydration during the render as a side-effect 🤔. It sounds like what I would want then is something that allows me to hydrateCache outside of react and then just initQueries inside of react as a side effect. I'm not quite sure if the following is a good idea/API but I'm going to throw it out there anyway:

import React from "react";
import { hydrate } from "react-dom";
import { makeQueryCache } from "react-query";
import { hydrateCache } from "react-query/hydrate";

const queryCache = makeQueryCache();

// HydratedQueryInvalidator is probably not a great name but it's my
// attempt to think of a name that will communicate to consumers the
// reason they they have to render it into their component tree
// (presuming what I assume in my question above is accurate)
const { HydratedQueryInvalidator } = hydrateCache(queryCache, window.__QUERIES__);
// const { QueryInvalidation } = hydrateCache(queryCache, window.__QUERIES__);
// const { HydrationInvalidator } = hydrateCache(queryCache, window.__QUERIES__);
// const { HydrateFreshness } = hydrateCache(queryCache, window.__QUERIES__);
// const { HydrateStaleness } = hydrateCache(queryCache, window.__QUERIES__);

hydrate(
  <ReactQueryCacheProvider cache={queryCache} />
     <HydratedQueryInvalidator />
     <App />
  </ReactQueryCacheProvider>
)

Maybe hydrateCache could provide multiple options as ways to run the initQueries(); maybe an option to call a function or a component to render into your component tree. (As a side note did find the naming of initQueries slightly confusing, it isn't/wasn't clear to me why/what needs to be initialized).

const { initQueries } = hydrateCache(queryCache, window.__QUERIES__);
// maybe a different name?
initQueries()

// OR

const { HydrationInvalidator } = hydrateCache(queryCache, window.__QUERIES__);;
<HydrationInvalidator />

@tannerlinsley
Copy link
Collaborator

Looks like this is going to need to be updated for the new types :)

@Ephem
Copy link
Collaborator Author

Ephem commented Aug 17, 2020

I've already talked to Tanner about this, but I wanted to update the PR as well for anyone following along. I'm back from vacations and plan to work on this for three full days during next week to hopefully finish it up! That includes refactoring to TS, docs and some extra functionality to better support dehydration/rehydration to/from localstorage.

This should have no breaking changes, so should be able to land it in a minor before v3.

@Ephem
Copy link
Collaborator Author

Ephem commented Aug 26, 2020

Timers

Purely code-wise I still think we should keep the scheduling out of the constructor for different reasons unrelated to this PR, but I do agree we could and should schedule GC immediately on hydrate and let the observer schedule staleness which lets us remove the whole activateTimeoutsManually thing. I've updated the PR with this!

@Ephem
Copy link
Collaborator Author

Ephem commented Aug 26, 2020

Error serialization

I talked to Tanner about this at length yesterday and we discussed very similar APIs to that. It took me a good nights sleep to realize that this isn't even a problem for React Query at all!

I even wrote this in the documentation yesterday:

  • This result is not in serialized form, you need to do that yourself if desired

So since dehydrate returns an un-serialized form, serialization is a user-level concern, not a framework one. So what's left to discuss is if we should include errors or not by default, and how to let the user toggle that.

I still think we should remove errors by default because I think that should be the majority use case, and the alternative requires a bunch of work on the users part. When it comes to how to toggle this we discussed a includeErrors: true-option yesterday, but now I realized that shouldDehydrate already covers this use case!

I've pushed an update that does this:

  • Instead of having a hardcoded query.state.status === 'success' in dehydrate, this has been moved to be a default shouldDehydrate-function
  • If you want to include all queries, provide your own shouldDehydrate: () => true or shouldDehydrate: query => query.state.status !== 'loading' if you want to exclude loading

@Jordan-Gilliam
Copy link

@Ephem This is a killer release, not to mention 0 breaking changes. Thank you much!!

@boschni
Copy link
Collaborator

boschni commented Aug 26, 2020

Getting there!

GC
It might be good to active the GC timer in the constructor though, because if for some reason the timer is not activated, it'll stay in the cache forever. Although preferably the QueryCache would be responsible of GC instead of the Query itself.

Serialization
While I definitely agree the actual (de)serialization should be the responsibility of the user, I do think it would be nice if we would provide an easy and straightforward way for them to do so. This could be added later on though.

Query filtering
Would it be possible to only have one cache on the server? Similar to the example in #728 (comment) ?

useHydrate
Is this hook still needed? The query cache needs to be created somewhere and there it can also be hydrated? Think it would be good to minimize the API surface and keep things similar to other frameworks.

@Ephem
Copy link
Collaborator Author

Ephem commented Aug 26, 2020

GC

Yeah, we need to make sure to always schedule this internally. There is a caveat around prefetching that makes me want to avoid doing it in the constructor that I can explain elsewhere, but in its current form this is all internal implementation details and we can tweak this after the PR. Okay if we move this discussion elsewhere?

Serialization

I agree! We can always add custom JSON.stringify-replacers to the library in the future.

New hydration/ReactQueryCacheProvider

This hopefully addresses both your points about filtering and useHydrate at the same time. 😄 Yes, we do need useHydrate and/or some other way to ergonomically hydrate inside of render because of how Next works. In Next, we only have access to the dehydrated queries via props, which we only have access to in render.

Tanner came up with the idea of adding a new ReactQueryCacheProvider that you can import directly from the hydration entrypoint. This one has two additional props to the one you can import from the core package:

  • initialQueries
  • hydrationConfig

So instead of exposing a <Hydrator />-component, we bake that into CacheProvider instead. This means your custom SSR-example would become:

// Server
const prefetchCache = makeQueryCache()
await prefetchCache.prefetchQuery('key', fn)
const initialQueries = dehydrate(prefetchCache)

const html = ReactDOM.renderToString(
  <ReactQueryCacheProvider initialQueries={initialQueries}>
    <App />
  </ReactQueryCacheProvider>
)
res.send(`
<html>
  <body>
    <div id="app">${html}</div>
    <script>window.__REACT_QUERY_INITIAL_QUERIES__ = ${JSON.stringify(initialQueries)};</script>
  </body>
</html>
`)

// Client
const initialQueries = JSON.parse(window.__REACT_QUERY_INITIAL_QUERIES__)
ReactDOM.hydrate(
  <ReactQueryCacheProvider initialQueries={initialQueries}>
    <App />
  </ReactQueryCacheProvider>
)

(I've also updated the Next example in the PR description)

Hopefully this looks okay? I'm open to adding a more generic queryCache.filter-function in the future that would when used right let you use the same cache on the server, but I don't think that should be the recommended approach mostly because I want to recommend a single way of doing it for Next and custom SSR.

I've pushed the hydration/ReactQueryCacheProvider as well as new docs.

@tannerlinsley This PR should now be up to date with what we talked about yesterday, but with the addition that you can now include errors in dehydration by using shouldDehydrate: () => true 🎉 (see comment above). I've also added the initialData-approach back to the docs.

src/hydration/hydration.ts Outdated Show resolved Hide resolved
src/hydration/hydration.ts Outdated Show resolved Hide resolved
src/hydration/hydration.ts Outdated Show resolved Hide resolved
src/hydration/react.tsx Outdated Show resolved Hide resolved
src/hydration/react.tsx Outdated Show resolved Hide resolved
@Ephem
Copy link
Collaborator Author

Ephem commented Aug 29, 2020

Thanks for the excellent comments, they have really helped improve this PR! From my point of view this is ready now.

@tannerlinsley tannerlinsley merged commit d85f79b into TanStack:master Aug 29, 2020
@tannerlinsley
Copy link
Collaborator

🎉 This PR is included in version 2.13.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

ryands17 added a commit to ryands17/react-query that referenced this pull request Sep 21, 2020
* docs: added more detailed explanation of what the cache timeout does in the detailed walkthough of useQuery (TanStack#928)

Co-authored-by: Clayton Marshall <claytn@claytons-mbp.lan>

* Update index.js

* Add queryCache de/rehydration (TanStack#728)

* chore(hydration): set up separate hydration entry point

* feat(hydration): add support for de/rehydrating queryCaches

- Add dehydrate(queryCache, config)
- Add hydrate(queryCache, dehydratedQueries, config)
- Add useHydrate(dehydratedQueries, config)

* test(hydration): fix broken type in test

* rename scheduleTimeoutsManually to activateTimeoutsManually

* docs(hydration): add API-docs for hydration and update comparison

* docs(ssr): update ssr-docs with new approach based on de/rehydration

* remove activateTimeoutsManually

* add default shouldDehydrate

* add hydration/ReactQueryCacheProvider

* use unknown for initialData in dehydration

* rename initialQueries  and dehydratedQueries to dehydratedState

* include queryKey instead of queryHash in dehydration

* update initialQueries to dehydratedState in ssr guide docs

* remove shouldHydrate-option

* feat: determine staleness locally instead of globally (TanStack#933)

* fix: add hydration.js to npm files

* fix: add hydration.js to npm files

* fix: make sure initial data is used when switching queries (TanStack#944)

* feat: change ReactQueryCacheProvider from hydration to Hydrate (TanStack#943)

* tests: fix setTimeout for ci tests

* fix: always use config from last query execution (TanStack#942)

* fix: make sure queries with only inactive observers are also invalidated (TanStack#949)

* feat: add notifyOnStatusChange flag (TanStack#840)

* docs: update comparison

Closes TanStack#920

* docs: update supporters and comparison

* docs: fix sponsors rendering

* fix: ignore errors from background fetches (TanStack#953)

* feat: add forceFetchOnMount flag (TanStack#954)

* feat: add isPreviousData and isFetchedAfterMount flags (TanStack#961)

* docs(react-native): add solution for fullscreen error (TanStack#958)

* docs: update sponsors

* fix: use hook config on refocus or reconnect (TanStack#964)

* docs: typo in useQuery.test (TanStack#965)

* fix: make sure setQueryData is not considered as initial data (TanStack#966)

* refactor: cleanup to reduce file size and add tests (TanStack#969)

* Update devtools.md (TanStack#973)

Separate the sentences based on contex

* fix: export hydration types (TanStack#974)

* docs: update sponsors

* fix(hydration): overwrite the existing data in the cache if hydrated data is newer (TanStack#976)

* refactor: optimize render path and improve type safety (TanStack#984)

* feat: add reset error boundary component (TanStack#980)

* refactor: remove unused deepEqual function (TanStack#999)

* fix: cancel current fetch when fetching more (TanStack#1000)

* fix: notify query cache on stale (TanStack#1001)

* test: add previous data test (TanStack#1003)

* feat: add support for tree shaking (TanStack#994)

* fix: useInfinityQuery fetchMore should not throw (TanStack#1004)

* docs: update api docs (TanStack#1005)

* refactor: remove query status bools (TanStack#1009)

* fix: make sure initial data always uses initial stale (TanStack#1010)

* feat: add always option to refetch options (TanStack#1011)

* feat: export QueryCache and remove global query cache from docs and examples (TanStack#1017)

* docs: remove shared config (TanStack#1021)

* feat: add remove method and deprecate clear (TanStack#1022)

* fix: should be able to invalidate queries (TanStack#1006)

* fix: should throw error when using useErrorBoundary (TanStack#1016)

* docs: Add graphql docs

* docs: add graphql-request example

* docs: update graphql docs

* docs: add graphql example

* feat: implement batch rendering (TanStack#989)

* docs: Update comparison.md

* docs: Update essentials banner

* docs: reorder homepage

* fix: accept any promise in useMutation callbacks (TanStack#1033)

* docs: prefer default config of QueryCache (TanStack#1034)

* fix: include config callbacks in batch render (TanStack#1036)

* docs: update example deps

* docs: fix comparison 3rd party website links (TanStack#1040)

* Remove storing the return value of queryCache.removeQueries (TanStack#1038)

Removed storing the return value of `queryCache.removeQueries` as it doesn't return anything.

* feat: add QueryCache.fetchQuery method (TanStack#1041)

* docs: add refetch documentation (TanStack#1043)

* docs: fix graphql example link

Closes TanStack#1044

* docs: remove trailing quotes from supporters links (TanStack#1045)

The quotes were breaking the links.

* docs: fix typo in queries page (TanStack#1046)

* fix: prevent bundlers from removing side effects (TanStack#1048)

* test: add invalidate query tests (TanStack#1052)

* fix: query should try and throw again after error boundary reset (TanStack#1054)

* docs: fix `user.id` access in case user is null (TanStack#1056)

* docs: update comparison

* docs: update sponsors

* feat: add QueryCache.watchQuery (TanStack#1058)

* docs: Update invalidations-from-mutations.md (TanStack#1057)

Remove unnecessary parenthesis

Co-authored-by: Clayton Marshall <c.marshall@salesforce.com>
Co-authored-by: Clayton Marshall <claytn@claytons-mbp.lan>
Co-authored-by: Tanner Linsley <tannerlinsley@gmail.com>
Co-authored-by: Fredrik Höglund <fredrik.hoglund@gmail.com>
Co-authored-by: Niek Bosch <just.niek@gmail.com>
Co-authored-by: Dragoș Străinu <str.dr4605@gmail.com>
Co-authored-by: Alex Marshall <alex.k.marshall83@gmail.com>
Co-authored-by: Rudzainy Rahman <rudzainy@gmail.com>
Co-authored-by: Corentin Leruth <corentin.leruth@gmail.com>
Co-authored-by: Evgeniy Boreyko <boreykojenya@yandex.ru>
Co-authored-by: Pierre Mdawar <pierre@mdawar.dev>
Co-authored-by: Juliano Farias <thefrontendwizard@gmail.com>
Co-authored-by: Twinkle <saintwinkle@gmail.com>
Co-authored-by: Julius-Rapp <61518032+Julius-Rapp@users.noreply.github.com>
Co-authored-by: cheddar <chad@cmfolio.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants