From 7b3ab2aa3f1fd875b17fe40fea3ebdb6dec85f24 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Tue, 8 Jun 2021 01:22:32 +0200 Subject: [PATCH 01/11] feat: expose hydration methods --- src/hydration.ts | 168 +++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 2 + 2 files changed, 170 insertions(+) create mode 100644 src/hydration.ts diff --git a/src/hydration.ts b/src/hydration.ts new file mode 100644 index 0000000..cd3ebef --- /dev/null +++ b/src/hydration.ts @@ -0,0 +1,168 @@ +// This is exact copy of react-query implementation. +// Original implementation cannot be used due to dependency on react + +import type { + QueryClient, + Query, + MutationKey, + MutationOptions, + QueryKey, + QueryOptions, +} from "react-query/core"; +import type { QueryState } from "react-query/types/core/query"; +import type { Mutation, MutationState } from "react-query/types/core/mutation"; + +// TYPES + +export interface DehydrateOptions { + dehydrateMutations?: boolean; + dehydrateQueries?: boolean; + shouldDehydrateMutation?: ShouldDehydrateMutationFunction; + shouldDehydrateQuery?: ShouldDehydrateQueryFunction; +} + +export interface HydrateOptions { + defaultOptions?: { + queries?: QueryOptions; + mutations?: MutationOptions; + }; +} + +interface DehydratedMutation { + mutationKey?: MutationKey; + state: MutationState; +} + +interface DehydratedQuery { + queryHash: string; + queryKey: QueryKey; + state: QueryState; +} + +export interface DehydratedState { + mutations: DehydratedMutation[]; + queries: DehydratedQuery[]; +} + +export type ShouldDehydrateQueryFunction = (query: Query) => boolean; + +export type ShouldDehydrateMutationFunction = (mutation: Mutation) => boolean; + +// FUNCTIONS + +function dehydrateMutation(mutation: Mutation): DehydratedMutation { + return { + mutationKey: mutation.options.mutationKey, + state: mutation.state, + }; +} + +// Most config is not dehydrated but instead meant to configure again when +// consuming the de/rehydrated data, typically with useQuery on the client. +// Sometimes it might make sense to prefetch data on the server and include +// in the html-payload, but not consume it on the initial render. +function dehydrateQuery(query: Query): DehydratedQuery { + return { + state: query.state, + queryKey: query.queryKey, + queryHash: query.queryHash, + }; +} + +function defaultShouldDehydrateMutation(mutation: Mutation) { + return mutation.state.isPaused; +} + +function defaultShouldDehydrateQuery(query: Query) { + return query.state.status === "success"; +} + +export function dehydrate( + client: QueryClient, + options?: DehydrateOptions +): DehydratedState { + options = options || {}; + + const mutations: DehydratedMutation[] = []; + const queries: DehydratedQuery[] = []; + + if (options?.dehydrateMutations !== false) { + const shouldDehydrateMutation = + options.shouldDehydrateMutation || defaultShouldDehydrateMutation; + + client + .getMutationCache() + .getAll() + .forEach((mutation) => { + if (shouldDehydrateMutation(mutation)) { + mutations.push(dehydrateMutation(mutation)); + } + }); + } + + if (options?.dehydrateQueries !== false) { + const shouldDehydrateQuery = + options.shouldDehydrateQuery || defaultShouldDehydrateQuery; + + client + .getQueryCache() + .getAll() + .forEach((query) => { + if (shouldDehydrateQuery(query)) { + queries.push(dehydrateQuery(query)); + } + }); + } + + return { mutations, queries }; +} + +export function hydrate( + client: QueryClient, + dehydratedState: unknown, + options?: HydrateOptions +): void { + if (typeof dehydratedState !== "object" || dehydratedState === null) { + return; + } + + const mutationCache = client.getMutationCache(); + const queryCache = client.getQueryCache(); + + const mutations = (dehydratedState as DehydratedState).mutations || []; + const queries = (dehydratedState as DehydratedState).queries || []; + + mutations.forEach((dehydratedMutation) => { + mutationCache.build( + client, + { + ...options?.defaultOptions?.mutations, + mutationKey: dehydratedMutation.mutationKey, + }, + dehydratedMutation.state + ); + }); + + queries.forEach((dehydratedQuery) => { + const query = queryCache.get(dehydratedQuery.queryHash); + + // Do not hydrate if an existing query exists with newer data + if (query) { + if (query.state.dataUpdatedAt < dehydratedQuery.state.dataUpdatedAt) { + query.setState(dehydratedQuery.state); + } + return; + } + + // Restore query + queryCache.build( + client, + { + ...options?.defaultOptions?.queries, + queryKey: dehydratedQuery.queryKey, + queryHash: dehydratedQuery.queryHash, + }, + dehydratedQuery.state + ); + }); +} diff --git a/src/index.ts b/src/index.ts index 87e3c58..19d66c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,3 +17,5 @@ export { useInfiniteQuery } from "./useInfiniteQuery"; export { useMutation } from "./useMutation"; export { useIsFetching } from "./useIsFetching"; export { useIsMutating } from "./useIsMutating"; + +export { hydrate, dehydrate } from "./hydration"; From 517c82000c4c0291dcb8154a8e86f831729e7cb0 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Tue, 8 Jun 2021 18:35:05 +0200 Subject: [PATCH 02/11] docs: nuxt example --- examples/nuxt-simple/.eslintrc.js | 13 +++++ examples/nuxt-simple/.gitignore | 5 ++ examples/nuxt-simple/README.md | 23 +++++++++ examples/nuxt-simple/layouts/default.vue | 19 ++++++++ examples/nuxt-simple/nuxt.config.js | 37 ++++++++++++++ examples/nuxt-simple/package.json | 27 +++++++++++ examples/nuxt-simple/pages/about.vue | 21 ++++++++ examples/nuxt-simple/pages/index.vue | 62 ++++++++++++++++++++++++ 8 files changed, 207 insertions(+) create mode 100644 examples/nuxt-simple/.eslintrc.js create mode 100644 examples/nuxt-simple/.gitignore create mode 100644 examples/nuxt-simple/README.md create mode 100644 examples/nuxt-simple/layouts/default.vue create mode 100644 examples/nuxt-simple/nuxt.config.js create mode 100644 examples/nuxt-simple/package.json create mode 100644 examples/nuxt-simple/pages/about.vue create mode 100644 examples/nuxt-simple/pages/index.vue diff --git a/examples/nuxt-simple/.eslintrc.js b/examples/nuxt-simple/.eslintrc.js new file mode 100644 index 0000000..1113d87 --- /dev/null +++ b/examples/nuxt-simple/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + root: true, + env: { + browser: true, + node: true, + }, + parserOptions: { + parser: "babel-eslint", + }, + extends: ["plugin:vue/essential"], + plugins: ["vue"], + rules: {}, +}; diff --git a/examples/nuxt-simple/.gitignore b/examples/nuxt-simple/.gitignore new file mode 100644 index 0000000..2769875 --- /dev/null +++ b/examples/nuxt-simple/.gitignore @@ -0,0 +1,5 @@ +node_modules +npm-debug.log +.nuxt +dist +yarn.lock diff --git a/examples/nuxt-simple/README.md b/examples/nuxt-simple/README.md new file mode 100644 index 0000000..3cbec41 --- /dev/null +++ b/examples/nuxt-simple/README.md @@ -0,0 +1,23 @@ +# my-nuxt-project + +> Nuxt.js project + +## Build Setup + +``` bash +# install dependencies +$ npm install # Or yarn install + +# serve with hot reload at localhost:3000 +$ npm run dev + +# build for production and launch server +$ npm run build +$ npm start + +# generate static project +$ npm run generate +``` + +For detailed explanation on how things work, checkout the [Nuxt.js docs](https://github.com/nuxt/nuxt.js). + diff --git a/examples/nuxt-simple/layouts/default.vue b/examples/nuxt-simple/layouts/default.vue new file mode 100644 index 0000000..7515f31 --- /dev/null +++ b/examples/nuxt-simple/layouts/default.vue @@ -0,0 +1,19 @@ + + + diff --git a/examples/nuxt-simple/nuxt.config.js b/examples/nuxt-simple/nuxt.config.js new file mode 100644 index 0000000..f8d6984 --- /dev/null +++ b/examples/nuxt-simple/nuxt.config.js @@ -0,0 +1,37 @@ +module.exports = { + /* + ** Headers of the page + */ + head: { + title: "my-nuxt-project", + meta: [ + { charset: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + { hid: "description", name: "description", content: "Nuxt.js project" }, + ], + link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }], + }, + /* + ** Customize the progress bar color + */ + loading: { color: "#3B8070" }, + /* + ** Build configuration + */ + build: { + /* + ** Run ESLint on save + */ + extend(config, { isDev, isClient }) { + if (isDev && isClient) { + config.module.rules.push({ + enforce: "pre", + test: /\.(js|vue)$/, + loader: "eslint-loader", + exclude: /(node_modules)/, + }); + } + }, + }, + buildModules: ["@nuxtjs/composition-api/module"], +}; diff --git a/examples/nuxt-simple/package.json b/examples/nuxt-simple/package.json new file mode 100644 index 0000000..b0985d5 --- /dev/null +++ b/examples/nuxt-simple/package.json @@ -0,0 +1,27 @@ +{ + "name": "my-nuxt-project", + "version": "1.0.0", + "description": "Nuxt.js project", + "author": "Damian Osipiuk ", + "private": true, + "scripts": { + "dev": "nuxt", + "build": "nuxt build", + "start": "nuxt start", + "generate": "nuxt generate", + "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", + "precommit": "npm run lint" + }, + "dependencies": { + "@nuxtjs/composition-api": "^0.24.2", + "nuxt": "^2.0.0", + "vue-query": "^1.5.1" + }, + "devDependencies": { + "babel-eslint": "^10.0.1", + "eslint": "^4.19.1", + "eslint-friendly-formatter": "^4.0.1", + "eslint-loader": "^2.1.1", + "eslint-plugin-vue": "^4.0.0" + } +} diff --git a/examples/nuxt-simple/pages/about.vue b/examples/nuxt-simple/pages/about.vue new file mode 100644 index 0000000..cc7f772 --- /dev/null +++ b/examples/nuxt-simple/pages/about.vue @@ -0,0 +1,21 @@ + + + diff --git a/examples/nuxt-simple/pages/index.vue b/examples/nuxt-simple/pages/index.vue new file mode 100644 index 0000000..7e92704 --- /dev/null +++ b/examples/nuxt-simple/pages/index.vue @@ -0,0 +1,62 @@ + + + From 45cc2eb67d2ab58bb96ca17f25f854c03c33ea4c Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sat, 12 Jun 2021 14:09:29 +0200 Subject: [PATCH 03/11] feat: expose hydration utilities as a separate entry --- .codesandbox/ci.json | 2 +- examples/nuxt-simple/pages/index.vue | 9 ++------- package.json | 1 + rollup.config.ts | 18 ++++++++++++++++++ src/index.ts | 2 -- src/{ => ssr}/hydration.ts | 0 src/ssr/index.ts | 1 + ssr/package.json | 6 ++++++ 8 files changed, 29 insertions(+), 10 deletions(-) rename src/{ => ssr}/hydration.ts (100%) create mode 100644 src/ssr/index.ts create mode 100644 ssr/package.json diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index fd66ec6..14e222a 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,5 +1,5 @@ { "packages": ["./"], - "sandboxes": ["/examples/base", "/examples/basic-vue-2.x"], + "sandboxes": ["/examples/base", "/examples/basic-vue-2.x", "/examples/nuxt-simple"], "node": "14" } diff --git a/examples/nuxt-simple/pages/index.vue b/examples/nuxt-simple/pages/index.vue index 7e92704..7014f54 100644 --- a/examples/nuxt-simple/pages/index.vue +++ b/examples/nuxt-simple/pages/index.vue @@ -15,13 +15,8 @@ import { onServerPrefetch, onBeforeMount, } from "@nuxtjs/composition-api"; -import { - hydrate, - dehydrate, - QueryClient, - useQuery, - useQueryClient, -} from "vue-query"; +import { QueryClient, useQuery, useQueryClient } from "vue-query"; +import { hydrate, dehydrate } from "vue-query/ssr"; const fetcher = async () => await fetch("https://jsonplaceholder.typicode.com/todos").then((response) => diff --git a/package.json b/package.json index d2193d9..7a93307 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "types": "lib/index.d.ts", "files": [ "/devtools", + "/ssr", "/lib", "/esm" ], diff --git a/rollup.config.ts b/rollup.config.ts index 9f79104..ead4ca4 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -30,6 +30,15 @@ export default [ }, ...common, }, + { + input: "src/ssr/index.ts", + output: { + file: "lib/ssr.js", + format: "cjs", + sourcemap: true, + }, + ...common, + }, { input: "src/index.ts", output: { @@ -48,4 +57,13 @@ export default [ }, ...common, }, + { + input: "src/ssr/index.ts", + output: { + file: "esm/ssr.js", + format: "esm", + sourcemap: true, + }, + ...common, + }, ]; diff --git a/src/index.ts b/src/index.ts index 19d66c0..87e3c58 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,5 +17,3 @@ export { useInfiniteQuery } from "./useInfiniteQuery"; export { useMutation } from "./useMutation"; export { useIsFetching } from "./useIsFetching"; export { useIsMutating } from "./useIsMutating"; - -export { hydrate, dehydrate } from "./hydration"; diff --git a/src/hydration.ts b/src/ssr/hydration.ts similarity index 100% rename from src/hydration.ts rename to src/ssr/hydration.ts diff --git a/src/ssr/index.ts b/src/ssr/index.ts new file mode 100644 index 0000000..aff450f --- /dev/null +++ b/src/ssr/index.ts @@ -0,0 +1 @@ +export { hydrate, dehydrate } from "./hydration"; diff --git a/ssr/package.json b/ssr/package.json new file mode 100644 index 0000000..12848fd --- /dev/null +++ b/ssr/package.json @@ -0,0 +1,6 @@ +{ + "internal": true, + "main": "../lib/ssr", + "module": "../esm/ssr", + "types": "../lib/ssr/index.d.ts" +} From 1ebb56f7f11855e05fff2770b56add599121e41d Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sat, 12 Jun 2021 20:52:14 +0200 Subject: [PATCH 04/11] feat: nuxt hooks --- examples/nuxt-simple/layouts/default.vue | 4 +- examples/nuxt-simple/nuxt.config.js | 2 +- examples/nuxt-simple/package.json | 2 +- examples/nuxt-simple/pages/index.vue | 25 +++---- package-lock.json | 84 ++++++++++++++++++++++++ package.json | 9 ++- src/ssr/index.ts | 3 + src/ssr/useNuxtDehydrate.ts | 11 ++++ src/ssr/useNuxtQueryProvider.ts | 16 +++++ 9 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 src/ssr/useNuxtDehydrate.ts create mode 100644 src/ssr/useNuxtQueryProvider.ts diff --git a/examples/nuxt-simple/layouts/default.vue b/examples/nuxt-simple/layouts/default.vue index 7515f31..5d8384f 100644 --- a/examples/nuxt-simple/layouts/default.vue +++ b/examples/nuxt-simple/layouts/default.vue @@ -7,13 +7,13 @@ diff --git a/examples/nuxt-simple/nuxt.config.js b/examples/nuxt-simple/nuxt.config.js index f8d6984..08455dc 100644 --- a/examples/nuxt-simple/nuxt.config.js +++ b/examples/nuxt-simple/nuxt.config.js @@ -3,7 +3,7 @@ module.exports = { ** Headers of the page */ head: { - title: "my-nuxt-project", + title: "Vue Query Nuxt Example", meta: [ { charset: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, diff --git a/examples/nuxt-simple/package.json b/examples/nuxt-simple/package.json index b0985d5..e326b7a 100644 --- a/examples/nuxt-simple/package.json +++ b/examples/nuxt-simple/package.json @@ -13,7 +13,7 @@ "precommit": "npm run lint" }, "dependencies": { - "@nuxtjs/composition-api": "^0.24.2", + "@nuxtjs/composition-api": "^0.24.4", "nuxt": "^2.0.0", "vue-query": "^1.5.1" }, diff --git a/examples/nuxt-simple/pages/index.vue b/examples/nuxt-simple/pages/index.vue index 7014f54..bea4b5d 100644 --- a/examples/nuxt-simple/pages/index.vue +++ b/examples/nuxt-simple/pages/index.vue @@ -7,16 +7,15 @@ - + +``` + +Now you are ready to prefetch some data in your pages with `onServerPrefetch`. + +- Use `useQueryClient` to get server-side instance of queryClient +- Use `useContext` to get nuxt context +- Prefetch all the queries that you need with `prefetchQuery` +- Use `useNuxtDehydrate` to dehydrate the query cache and pass it to the client-side via nuxt context. + +```js +// pages/todos.vue + + + +``` + +As demonstrated, it's fine to prefetch some queries and let others fetch on the queryClient. This means you can control what content server renders or not by adding or removing `prefetchQuery` for a specific query. + +## Tips, Tricks and Caveats + +### Only successful queries are included in dehydration + +Any query with an error is automatically excluded from dehydration. This means that the default behaviour is to pretend these queries were never loaded on the server, usually showing a loading state instead, and retrying the queries on the queryClient. This happens regardless of error. + +Sometimes this behavior is not desirable, maybe you want to render an error page with a correct status code instead on certain errors or queries. In those cases, use `fetchQuery` and catch any errors to handle those manually. + +### Staleness is measured from when the query was fetched on the server + +A query is considered stale depending on when it was `dataUpdatedAt`. A caveat here is that the server needs to have the correct time for this to work properly, but UTC time is used, so timezones do not factor into this. + +Because `staleTime` defaults to `0`, queries will be refetched in the background on page load by default. You might want to use a higher `staleTime` to avoid this double fetching, especially if you don't cache your markup. + +This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the `staleTime` of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day? From 65a05b348bcd5d16b7ea0d43001603ad6e5db434 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sat, 12 Jun 2021 23:37:03 +0200 Subject: [PATCH 08/11] refactor: clean nuxt package.json --- examples/nuxt-simple/package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/nuxt-simple/package.json b/examples/nuxt-simple/package.json index e326b7a..9ccfc40 100644 --- a/examples/nuxt-simple/package.json +++ b/examples/nuxt-simple/package.json @@ -1,8 +1,4 @@ { - "name": "my-nuxt-project", - "version": "1.0.0", - "description": "Nuxt.js project", - "author": "Damian Osipiuk ", "private": true, "scripts": { "dev": "nuxt", From 0e8ad0dc6371947b78a44581c9501183a85c7ff1 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sat, 12 Jun 2021 23:41:59 +0200 Subject: [PATCH 09/11] style: prettier --- .codesandbox/ci.json | 6 +++++- .eslintrc.json | 2 +- CONTRIBUTING.md | 1 + README.md | 1 + examples/nuxt-simple/README.md | 3 +-- src/ssr/useNuxtQueryProvider.ts | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 14e222a..06f7632 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,5 +1,9 @@ { "packages": ["./"], - "sandboxes": ["/examples/base", "/examples/basic-vue-2.x", "/examples/nuxt-simple"], + "sandboxes": [ + "/examples/base", + "/examples/basic-vue-2.x", + "/examples/nuxt-simple" + ], "node": "14" } diff --git a/.eslintrc.json b/.eslintrc.json index 003fd33..e0c4971 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -17,4 +17,4 @@ "no-console": "warn", "no-debugger": "warn" } -} \ No newline at end of file +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c90e8f..045bb40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,7 @@ If you want to suggest a feature, [create an issue](https://github.com/DamianOsi ## Development While contributing, make sure to follow the guidelines: + - run `npm run verify` before opening a PR - write tests for any new piece of code that you are adding to the repository when applicable diff --git a/README.md b/README.md index f53bfe1..aeca9e8 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Support for Vue 2.x via [vue-demi](https://github.com/vueuse/vue-demi) Based on [react-query](https://github.com/tannerlinsley/react-query) # Documentation + Visit https://vue-query.vercel.app # Quick Features diff --git a/examples/nuxt-simple/README.md b/examples/nuxt-simple/README.md index 3cbec41..c22881c 100644 --- a/examples/nuxt-simple/README.md +++ b/examples/nuxt-simple/README.md @@ -4,7 +4,7 @@ ## Build Setup -``` bash +```bash # install dependencies $ npm install # Or yarn install @@ -20,4 +20,3 @@ $ npm run generate ``` For detailed explanation on how things work, checkout the [Nuxt.js docs](https://github.com/nuxt/nuxt.js). - diff --git a/src/ssr/useNuxtQueryProvider.ts b/src/ssr/useNuxtQueryProvider.ts index 1c101f2..5d999ca 100644 --- a/src/ssr/useNuxtQueryProvider.ts +++ b/src/ssr/useNuxtQueryProvider.ts @@ -13,4 +13,4 @@ export function useNuxtQueryProvider(): void { hydrate(queryClient, nuxtState.vueQueryState); } } -} \ No newline at end of file +} From 02ec64e5af9b357485b43315efc1e33b5fc9109c Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sat, 12 Jun 2021 23:43:24 +0200 Subject: [PATCH 10/11] docs: mark experimental features in readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aeca9e8..eda9e73 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ Visit https://vue-query.vercel.app - Paginated + Cursor-based Queries - Load-More + Infinite Scroll Queries w/ Scroll Recovery - Request Cancellation -- [Suspense](https://v3.vuejs.org/guide/migration/suspense.html#introduction) + Fetch-As-You-Render Query Prefetching +- (experimental) [Suspense](https://v3.vuejs.org/guide/migration/suspense.html#introduction) + Fetch-As-You-Render Query Prefetching +- (experimental) SSR support - Dedicated Devtools - [![npm bundle size](https://img.shields.io/bundlephobia/minzip/vue-query)](https://bundlephobia.com/result?p=vue-query) (depending on features imported) From dc04a32e7018cde889385c5fc2b069713d2b7727 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sun, 13 Jun 2021 00:45:57 +0200 Subject: [PATCH 11/11] test: nuxt hooks tests --- src/ssr/useNuxtDehydrate.ts | 8 ++- tests/ssr/useNuxtDehydrate.test.ts | 34 +++++++++++++ tests/ssr/useNuxtQueryProvider.test.ts | 70 ++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 tests/ssr/useNuxtDehydrate.test.ts create mode 100644 tests/ssr/useNuxtQueryProvider.test.ts diff --git a/src/ssr/useNuxtDehydrate.ts b/src/ssr/useNuxtDehydrate.ts index 9cc3fa3..85fd5cb 100644 --- a/src/ssr/useNuxtDehydrate.ts +++ b/src/ssr/useNuxtDehydrate.ts @@ -7,5 +7,11 @@ export function useNuxtDehydrate( }, queryClient: QueryClient ): void { - ssrContext.nuxt.vueQueryState = dehydrate(queryClient); + if (!ssrContext || !ssrContext.nuxt) { + throw new Error( + "Please provide `ssrContext` from nuxt `useContext` hook as a first parameter to `useNuxtDehydrate`" + ); + } else { + ssrContext.nuxt.vueQueryState = dehydrate(queryClient); + } } diff --git a/tests/ssr/useNuxtDehydrate.test.ts b/tests/ssr/useNuxtDehydrate.test.ts new file mode 100644 index 0000000..a213cfb --- /dev/null +++ b/tests/ssr/useNuxtDehydrate.test.ts @@ -0,0 +1,34 @@ +import { useNuxtDehydrate } from "../../src/ssr/useNuxtDehydrate"; + +jest.mock("../../src/ssr/hydration", () => ({ + dehydrate: jest.fn(() => "dehydrated"), +})); + +const nothing = {} as never; + +describe("useNuxtDehydrate", () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + test("should inject dehydrated queryClient state to nuxt context", () => { + const context = { nuxt: { vueQueryState: undefined } }; + + useNuxtDehydrate(context, nothing); + + expect(context.nuxt.vueQueryState).toStrictEqual("dehydrated"); + }); + + test("should throw an error when ssrContext is not provided", () => { + expect(useNuxtDehydrate).toThrowError(); + }); + + test("should throw an error when ssrContext is not a valid object", () => { + const shouldThrow = () => { + useNuxtDehydrate(nothing, nothing); + }; + + expect(shouldThrow).toThrowError(); + }); +}); diff --git a/tests/ssr/useNuxtQueryProvider.test.ts b/tests/ssr/useNuxtQueryProvider.test.ts new file mode 100644 index 0000000..6cbbc6c --- /dev/null +++ b/tests/ssr/useNuxtQueryProvider.test.ts @@ -0,0 +1,70 @@ +import { useContext } from "@nuxtjs/composition-api"; +import { useNuxtQueryProvider } from "../../src/ssr/useNuxtQueryProvider"; +import { useQueryClient, useQueryProvider } from "../../src/index"; +import { hydrate } from "../../src/ssr/hydration"; + +jest.mock("@nuxtjs/composition-api", () => ({ + useContext: jest.fn(), +})); + +jest.mock("../../src/index", () => ({ + useQueryClient: jest.fn(), + useQueryProvider: jest.fn(), +})); + +jest.mock("../../src/ssr/hydration", () => ({ + hydrate: jest.fn(), +})); + +const withVueQueryState = { nuxtState: { vueQueryState: {} } }; +const withoutVueQueryState = { nuxtState: {} }; + +describe("useNuxtQueryProvider", () => { + const useContextSpy = useContext as jest.Mock; + const useQueryProviderSpy = useQueryProvider as jest.Mock; + const useQueryClientSpy = useQueryClient as jest.Mock; + const hydrateSpy = hydrate as jest.Mock; + + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + // @ts-expect-error Nuxt.js injected client property + process.client = true; + }); + + test("should call queryProvider", () => { + // @ts-expect-error Nuxt.js injected client property + process.client = false; + useNuxtQueryProvider(); + + expect(useQueryProviderSpy).toHaveBeenCalledTimes(1); + }); + + test("should call useQueryClient when vueQueryState is present", () => { + useContextSpy.mockReturnValueOnce(withVueQueryState); + useNuxtQueryProvider(); + + expect(useQueryClientSpy).toHaveBeenCalledTimes(1); + }); + + test("should NOT call useQueryClient when vueQueryState is NOT present", () => { + useContextSpy.mockReturnValueOnce(withoutVueQueryState); + useNuxtQueryProvider(); + + expect(useQueryClientSpy).toHaveBeenCalledTimes(0); + }); + + test("should call hydrate when vueQueryState is present", () => { + useContextSpy.mockReturnValueOnce(withVueQueryState); + useNuxtQueryProvider(); + + expect(hydrateSpy).toHaveBeenCalledTimes(1); + }); + + test("should NOT call hydrate when vueQueryState is NOT present", () => { + useContextSpy.mockReturnValueOnce(withoutVueQueryState); + useNuxtQueryProvider(); + + expect(hydrateSpy).toHaveBeenCalledTimes(0); + }); +});