-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Conversation
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:
This should keep the bundle size down for the default use case. |
As for updatedAt, technically it is UTC because it uses |
Sorry for the radio silence, busy summer. 😄
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 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 |
This pull request is being automatically deployed with Vercel (learn more). 🔍 Inspect: https://vercel.com/tannerlinsley/react-query/oaqma3u13 |
serverQueryCache.clear({ notify: false }) | ||
}) | ||
|
||
test('should handle global cache case', async () => { |
There was a problem hiding this comment.
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?
Okay, this PR has been updated with a bunch of stuff.
I also updated the description above, including the Two questions:
|
src/hydration/useHydrate.js
Outdated
@@ -0,0 +1,22 @@ | |||
import React from 'react' |
There was a problem hiding this comment.
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.
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 Pseudo usageHomepage.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 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 <script>
window.__QUERIES__ = {
"<QUERY_KEY>": "<QUERY_RESULT>"
}; And our initialData() {
const queryKey = getQueryKey(name, vars);
if (window.__QUERIES__[queryKey]) {
return window.__QUERIES__[queryKey]
}
return undefined;
} By using a 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 |
@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:
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. 💯 |
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. |
Glad to hear that! I built this for use in a custom SSR solution myself, so it should absolutely support that.
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! |
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 |
@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 When using custom SSR none of this is a problem and you can just do both before even rendering and in fact, the An earlier version of this PR had an extra 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 To address what seems to be a main concern though, 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! |
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.
That does explain why
Wouldn't it be possible to expose the multiple hydration functions and just provide people with the
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
If it was necessary to do hydration during the react render I'd personally find it preferable to have the
Makes sense wanting to keep the entrypoints seperate
yeah that was just for terseness (and I wasn't aware of the need to hydrate and also have the seperate
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, |
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 Currently I heavily use 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 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>
);
},
});
}) |
@pseudo-su ❤️ for the detailed discussion!
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 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 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.
|
Right 😅. I'm slowly getting onto the same page.
Ah right yep that makes sense. I'm curious, what would be the result of never querying 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 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 const { initQueries } = hydrateCache(queryCache, window.__QUERIES__);
// maybe a different name?
initQueries()
// OR
const { HydrationInvalidator } = hydrateCache(queryCache, window.__QUERIES__);;
<HydrationInvalidator /> |
Looks like this is going to need to be updated for the new types :) |
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. |
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 |
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:
So since 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 I've pushed an update that does this:
|
@Ephem This is a killer release, not to mention 0 breaking changes. Thank you much!! |
Getting there! GC Serialization Query filtering useHydrate |
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 New This hopefully addresses both your points about filtering and Tanner came up with the idea of adding a new
So instead of exposing a // 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 I've pushed the @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 |
Thanks for the excellent comments, they have really helped improve this PR! From my point of view this is ready now. |
🎉 This PR is included in version 2.13.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
* 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>
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)
hydration/ReactQueryCacheProvider
-component with the additionaldehydratedState
-propuseHydrate(dehydratedQueries)
- A hook that doeshydrate
for you in a React-compatible wayTogether, 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
A minimal example:
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
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?~~Should
dehydrateQuery
be included in the official APIs?~~~How should we handleupdatedAt
?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. 😅