diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index fd66ec6..06f7632 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,5 +1,9 @@ { "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/.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..eda9e73 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 @@ -26,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) diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 7d41886..bb1efc6 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -10,5 +10,10 @@ - [Simple](examples/simple.md) - [Basic](examples/base.md) - [Multi Page](examples/multi-page.md) - - [Suspense](examples/suspense.md) - - [Vue 2.x](examples/vue-2.x.md) + - [Suspense (experimental)](examples/suspense.md) + - [Vue 2.x (experimental)](examples/vue-2.x.md) + - [Nuxt.js (experimental)](examples/nuxt.md) + +- Guides & Concepts + + - [SSR & Nuxt.js (experimental)](guides/ssr.md) diff --git a/docs/examples/nuxt.md b/docs/examples/nuxt.md new file mode 100644 index 0000000..84e2593 --- /dev/null +++ b/docs/examples/nuxt.md @@ -0,0 +1,7 @@ +- [Open in Codesandbox](https://codesandbox.io/s/github/DamianOsipiuk/vue-query/tree/main/examples/nuxt-simple) +- [View Source](https://github.com/DamianOsipiuk/vue-query/tree/main/examples/nuxt-simple) + + diff --git a/docs/guides/ssr.md b/docs/guides/ssr.md new file mode 100644 index 0000000..da87abf --- /dev/null +++ b/docs/guides/ssr.md @@ -0,0 +1,105 @@ +Vue Query supports two ways of prefetching data on the server and passing that to the queryClient. + +- Prefetch the data yourself and pass it in as `initialData` + - Quick to set up for simple cases + - Has some caveats +- Prefetch the query on the server, dehydrate the cache and rehydrate it on the client + - Requires slightly more setup up front + +## Using Nuxt.js + +### Using Hydration + +Vue Query supports prefetching multiple queries on the server in Nuxt.js and then _dehydrating_ those queries to the queryClient. This means the server can prerender markup that is immediately available on page load and as soon as JS is available, Vue Query can upgrade or _hydrate_ those queries with the full functionality of the library. This includes refetching those queries on the client if they have become stale since the time they were rendered on the server. + +To support caching queries on the server and set up hydration: + +- Use `useNuxtQueryProvider` inside setup function of your layout component + +```js +// layouts/default.vue + + + + +``` + +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? 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..c22881c --- /dev/null +++ b/examples/nuxt-simple/README.md @@ -0,0 +1,22 @@ +# 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..5d8384f --- /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..08455dc --- /dev/null +++ b/examples/nuxt-simple/nuxt.config.js @@ -0,0 +1,37 @@ +module.exports = { + /* + ** Headers of the page + */ + head: { + title: "Vue Query Nuxt Example", + 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..9ccfc40 --- /dev/null +++ b/examples/nuxt-simple/package.json @@ -0,0 +1,23 @@ +{ + "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.4", + "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..a78341a --- /dev/null +++ b/examples/nuxt-simple/pages/index.vue @@ -0,0 +1,53 @@ + + + diff --git a/package-lock.json b/package-lock.json index 7f247fe..f8af038 100644 --- a/package-lock.json +++ b/package-lock.json @@ -795,6 +795,66 @@ "fastq": "^1.6.0" } }, + "@nuxtjs/composition-api": { + "version": "0.24.4", + "resolved": "https://registry.npmjs.org/@nuxtjs/composition-api/-/composition-api-0.24.4.tgz", + "integrity": "sha512-WRQGVgE1MbwYVOvFeGfToOjb6w3D2lxWM54N4HMFOPmDJI9+XngJQZgtuQCBUSsEZ3a3dpejBSzSYyEC7FrdvQ==", + "dev": true, + "requires": { + "@vue/composition-api": "1.0.0-rc.11", + "defu": "^5.0.0", + "estree-walker": "^2.0.2", + "fs-extra": "^9.1.0", + "magic-string": "^0.25.7", + "ufo": "^0.7.5", + "upath": "^2.0.1" + }, + "dependencies": { + "@vue/composition-api": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@vue/composition-api/-/composition-api-1.0.0-rc.11.tgz", + "integrity": "sha512-rB5cKeNZlHdjEYre2vBzdid+e09Mf2HmMrg31NCT6c1G6bgX1CJ6V8H7owo9+wK/O2YyN9AEkIVLOczP9s0VTQ==", + "dev": true, + "requires": { + "tslib": "^2.2.0" + } + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "dev": true + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, "@rollup/plugin-node-resolve": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.0.tgz", @@ -1431,6 +1491,12 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -2764,6 +2830,12 @@ } } }, + "defu": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/defu/-/defu-5.0.0.tgz", + "integrity": "sha512-VHg73EDeRXlu7oYWRmmrNp/nl7QkdXUxkQQKig0Zk8daNmm84AbGoC8Be6/VVLJEKxn12hR0UBmz8O+xQiAPKQ==", + "dev": true + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -9986,6 +10058,12 @@ "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", "dev": true }, + "ufo": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-0.7.5.tgz", + "integrity": "sha512-FGG+EgguC1oz5dTE1JptPWCyj6Z9mYpwvZY8PTu9Vh/Aoy+Mj9cpeQ3gg4kyEMDbMrH+lTYiw7bomG58B8X7Kg==", + "dev": true + }, "unbox-primitive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.0.tgz", @@ -10083,6 +10161,12 @@ } } }, + "upath": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "dev": true + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index d2193d9..077724a 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "types": "lib/index.d.ts", "files": [ "/devtools", + "/ssr", "/lib", "/esm" ], @@ -42,14 +43,19 @@ }, "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", + "@nuxtjs/composition-api": "^0.24.4", "vue": "^2.0.0 || >=3.0.0" }, "peerDependenciesMeta": { "@vue/composition-api": { "optional": true + }, + "@nuxtjs/composition-api": { + "optional": true } }, "devDependencies": { + "@nuxtjs/composition-api": "^0.24.4", "@rollup/plugin-node-resolve": "^11.2.0", "@types/jest": "^26.0.21", "@typescript-eslint/eslint-plugin": "^4.16.1", @@ -73,7 +79,7 @@ "ts-node": "^9.1.1", "typescript": "^4.2.3", "vue": "^3.0.7", - "vue2": "npm:vue@2", - "vue-jest": "^5.0.0-alpha.8" + "vue-jest": "^5.0.0-alpha.8", + "vue2": "npm:vue@2" } } 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/ssr/hydration.ts b/src/ssr/hydration.ts new file mode 100644 index 0000000..cd3ebef --- /dev/null +++ b/src/ssr/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/ssr/index.ts b/src/ssr/index.ts new file mode 100644 index 0000000..8d13b46 --- /dev/null +++ b/src/ssr/index.ts @@ -0,0 +1,4 @@ +export { hydrate, dehydrate } from "./hydration"; + +export { useNuxtQueryProvider } from "./useNuxtQueryProvider"; +export { useNuxtDehydrate } from "./useNuxtDehydrate"; diff --git a/src/ssr/useNuxtDehydrate.ts b/src/ssr/useNuxtDehydrate.ts new file mode 100644 index 0000000..85fd5cb --- /dev/null +++ b/src/ssr/useNuxtDehydrate.ts @@ -0,0 +1,17 @@ +import { dehydrate } from "./hydration"; +import type { QueryClient } from "../index"; + +export function useNuxtDehydrate( + ssrContext: { + nuxt: Record; + }, + queryClient: QueryClient +): void { + 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/src/ssr/useNuxtQueryProvider.ts b/src/ssr/useNuxtQueryProvider.ts new file mode 100644 index 0000000..5d999ca --- /dev/null +++ b/src/ssr/useNuxtQueryProvider.ts @@ -0,0 +1,16 @@ +import { useContext } from "@nuxtjs/composition-api"; +import { useQueryClient, useQueryProvider } from "../index"; +import { hydrate } from "./hydration"; + +export function useNuxtQueryProvider(): void { + useQueryProvider(); + + // @ts-expect-error Nuxt.js injected client property + if (process.client) { + const { nuxtState } = useContext(); + if (nuxtState.vueQueryState) { + const queryClient = useQueryClient(); + hydrate(queryClient, nuxtState.vueQueryState); + } + } +} 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" +} 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); + }); +});