diff --git a/broadcastQueryClient-experimental/package.json b/broadcastQueryClient-experimental/package.json new file mode 100644 index 0000000000..ca92702779 --- /dev/null +++ b/broadcastQueryClient-experimental/package.json @@ -0,0 +1,6 @@ +{ + "internal": true, + "main": "../lib/broadcastQueryClient-experimental/index.js", + "module": "../es/broadcastQueryClient-experimental/index.js", + "types": "../types/broadcastQueryClient-experimental/index.d.ts" +} diff --git a/docs/src/pages/plugins/broadcastQueryClient.md b/docs/src/pages/plugins/broadcastQueryClient.md new file mode 100644 index 0000000000..7f84f11113 --- /dev/null +++ b/docs/src/pages/plugins/broadcastQueryClient.md @@ -0,0 +1,59 @@ +--- +id: broadcastQueryClient +title: broadcastQueryClient (Experimental) +--- + +> VERY IMPORTANT: This utility is currently in an experimental stage. This means that breaking changes will happen in minor AND patch releases. Use at your own risk. If you choose to rely on this in production in an experimental stage, please lock your version to a patch-level version to avoid unexpected breakages. + +`broadcastQueryClient` is a utility for broadcasting and syncing the state of your queryClient between browser tabs/windows with the same origin. + +## Installation + +This utility comes packaged with `react-query` and is available under the `react-query/broadcastQueryClient-experimental` import. + +## Usage + +Import the `broadcastQueryClient` function, and pass it your `QueryClient` instance, and optionally, set a `broadcastChannel`. + +```ts +import { broadcastQueryClient } from 'react-query/broadcastQueryClient-experimental' + +const queryClient = new QueryClient() + +broadcastQueryClient({ + queryClient, + broadcastChannel: 'my-app', +}) +``` + +## API + +### `broadcastQueryClient` + +Pass this function a `QueryClient` instance and optionally, a `broadcastChannel`. + +```ts +broadcastQueryClient({ queryClient, broadcastChannel }) +``` + +### `Options` + +An object of options: + +```ts +interface broadcastQueryClient { + /** The QueryClient to sync */ + queryClient: QueryClient + /** This is the unique channel name that will be used + * to communicate between tabs and windows */ + broadcastChannel?: string +} +``` + +The default options are: + +```ts +{ + broadcastChannel = 'react-query', +} +``` diff --git a/examples/basic/package.json b/examples/basic/package.json index 487cfbe41b..14809056df 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "axios": "^0.21.1", + "broadcast-channel": "^3.4.1", "react": "^16.8.6", "react-dom": "^16.8.6", "react-query": "^3.5.0", diff --git a/package.json b/package.json index c650e411b0..851b00ea94 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "devtools", "persistQueryClient-experimental", "createLocalStoragePersistor-experimental", + "broadcastQueryClient-experimental", "lib", "react", "scripts", @@ -57,6 +58,7 @@ ], "dependencies": { "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", "match-sorter": "^6.0.2" }, "peerDependencies": { diff --git a/rollup.config.js b/rollup.config.js index 95b16d42a0..7d54f852d3 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -29,6 +29,11 @@ const inputSrcs = [ 'ReactQueryCreateLocalStoragePersistorExperimental', 'createLocalStoragePersistor-experimental', ], + [ + 'src/broadcastQueryClient-experimental/index.ts', + 'ReactQueryBroadcastQueryClientExperimental', + 'broadcastQueryClient-experimental', + ], ] const extensions = ['.js', '.jsx', '.es6', '.es', '.mjs', '.ts', '.tsx'] diff --git a/src/broadcastQueryClient-experimental/index.ts b/src/broadcastQueryClient-experimental/index.ts new file mode 100644 index 0000000000..8bf6184745 --- /dev/null +++ b/src/broadcastQueryClient-experimental/index.ts @@ -0,0 +1,89 @@ +import { BroadcastChannel } from 'broadcast-channel' +import { QueryClient } from '../core' + +interface BroadcastQueryClientOptions { + queryClient: QueryClient + broadcastChannel: string +} + +export function broadcastQueryClient({ + queryClient, + broadcastChannel = 'react-query', +}: BroadcastQueryClientOptions) { + let transaction = false + const tx = (cb: () => void) => { + transaction = true + cb() + transaction = false + } + + const channel = new BroadcastChannel(broadcastChannel, { + webWorkerSupport: false, + }) + + const queryCache = queryClient.getQueryCache() + + queryClient.getQueryCache().subscribe(queryEvent => { + if (transaction || !queryEvent?.query) { + return + } + + const { + query: { queryHash, queryKey, state }, + } = queryEvent + + if ( + queryEvent.type === 'queryUpdated' && + queryEvent.action?.type === 'success' + ) { + channel.postMessage({ + type: 'queryUpdated', + queryHash, + queryKey, + state, + }) + } + + if (queryEvent.type === 'queryRemoved') { + channel.postMessage({ + type: 'queryRemoved', + queryHash, + queryKey, + }) + } + }) + + channel.onmessage = action => { + if (!action?.type) { + return + } + + tx(() => { + const { type, queryHash, queryKey, state } = action + + if (type === 'queryUpdated') { + const query = queryCache.get(queryHash) + + if (query) { + query.setState(state) + return + } + + queryCache.build( + queryClient, + { + queryKey, + queryHash, + }, + state + ) + } else if (type === 'queryRemoved') { + const query = queryCache.get(queryHash) + + if (query) { + queryCache.remove(query) + } + } + }) + } +} diff --git a/src/core/query.ts b/src/core/query.ts index 69be7422a5..a2ca2cded9 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -106,6 +106,7 @@ interface ContinueAction { interface SetStateAction { type: 'setState' state: QueryState + setStateOptions?: SetStateOptions } export type Action = @@ -118,6 +119,10 @@ export type Action = | SetStateAction | SuccessAction +export interface SetStateOptions { + meta?: any +} + // CLASS export class Query< @@ -216,8 +221,11 @@ export class Query< return data } - setState(state: QueryState): void { - this.dispatch({ type: 'setState', state }) + setState( + state: QueryState, + setStateOptions?: SetStateOptions + ): void { + this.dispatch({ type: 'setState', state, setStateOptions }) } cancel(options?: CancelOptions): Promise { diff --git a/tsconfig.types.json b/tsconfig.types.json index 365254c143..8474ccb0b1 100644 --- a/tsconfig.types.json +++ b/tsconfig.types.json @@ -11,7 +11,8 @@ "./src/hydration/index.ts", "./src/devtools/index.ts", "./src/persistQueryClient-experimental/index.ts", - "./src/createLocalStoragePersistor-experimental/index.ts" + "./src/createLocalStoragePersistor-experimental/index.ts", + "./src/broadcastQueryClient-experimental/index.ts" ], "exclude": ["./src/**/*"] } diff --git a/yarn.lock b/yarn.lock index a4455ab9df..284a6690fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1944,6 +1944,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d" + integrity sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.8.4": version "7.10.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" @@ -3088,6 +3095,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +big-integer@^1.6.16: + version "1.6.48" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" + integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -3136,6 +3148,19 @@ braces@^3.0.1: dependencies: fill-range "^7.0.1" +broadcast-channel@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.4.1.tgz#65b63068d0a5216026a19905c9b2d5e9adf0928a" + integrity sha512-VXYivSkuBeQY+pL5hNQQNvBdKKQINBAROm4G8lAbWQfOZ7Yn4TMcgLNlJyEqlkxy5G8JJBsI3VJ1u8FUTOROcg== + dependencies: + "@babel/runtime" "^7.7.2" + detect-node "^2.0.4" + js-sha3 "0.8.0" + microseconds "0.2.0" + nano-time "1.0.0" + rimraf "3.0.2" + unload "2.2.0" + brotli-size@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/brotli-size/-/brotli-size-4.0.0.tgz#a05ee3faad3c0e700a2f2da826ba6b4d76e69e5e" @@ -3652,6 +3677,11 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + diff-sequences@^25.2.6: version "25.2.6" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" @@ -5468,6 +5498,11 @@ jest@^26.0.1: import-local "^3.0.2" jest-cli "^26.0.1" +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -5863,6 +5898,11 @@ micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" +microseconds@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" + integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== + mime-db@1.42.0: version "1.42.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" @@ -5945,6 +5985,13 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== +nano-time@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8= + dependencies: + big-integer "^1.6.16" + nanoid@^3.0.1: version "3.1.3" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.3.tgz#b2bcfcfda4b4d6838bc22a0c8dd3c0a17a204c20" @@ -6823,7 +6870,7 @@ rimraf@2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -7755,6 +7802,14 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +unload@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "^2.0.4" + unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"