├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierignore ├── .release-it.json ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── README.md ├── art └── invalidate-queries.png ├── example ├── .gitignore ├── App.tsx ├── PokemonList.tsx ├── app.json ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── package.json ├── queryClient.ts ├── reactotron.ts ├── tsconfig.json └── yarn.lock ├── package-lock.json ├── package.json ├── reactotron.png ├── src ├── index.ts └── lib │ ├── invalidate-react-query-command.spec.ts │ ├── invalidate-react-query-command.ts │ ├── query-cache-notify-event.interface.ts │ ├── query-client-manager-options.interface.ts │ ├── query-client-manager.spec.ts │ ├── query-client-manager.ts │ ├── reactotron-helpers.spec.ts │ ├── reactotron-helpers.ts │ └── reactotron-react-query.ts ├── tsconfig.json └── tsconfig.prod.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # https://circleci.com/docs/2.0/language-javascript/ 2 | version: 2 3 | jobs: 4 | 'node-10': 5 | docker: 6 | - image: circleci/node:10 7 | steps: 8 | - checkout 9 | # Download and cache dependencies 10 | - restore_cache: 11 | keys: 12 | - v1-dependencies-{{ checksum "package.json" }} 13 | # fallback to using the latest cache if no exact match is found 14 | - v1-dependencies- 15 | - run: npm install 16 | - save_cache: 17 | paths: 18 | - node_modules 19 | key: v1-dependencies-{{ checksum "package.json" }} 20 | - run: npm test 21 | - run: npm run cov:send 22 | - run: npm run cov:check 23 | 'node-12': 24 | docker: 25 | - image: circleci/node:12 26 | steps: 27 | - checkout 28 | - restore_cache: 29 | keys: 30 | - v1-dependencies-{{ checksum "package.json" }} 31 | - v1-dependencies- 32 | - run: npm install 33 | - save_cache: 34 | paths: 35 | - node_modules 36 | key: v1-dependencies-{{ checksum "package.json" }} 37 | - run: npm test 38 | - run: npm run cov:send 39 | - run: npm run cov:check 40 | 'node-latest': 41 | docker: 42 | - image: circleci/node:latest 43 | steps: 44 | - checkout 45 | - restore_cache: 46 | keys: 47 | - v1-dependencies-{{ checksum "package.json" }} 48 | - v1-dependencies- 49 | - run: npm install 50 | - save_cache: 51 | paths: 52 | - node_modules 53 | key: v1-dependencies-{{ checksum "package.json" }} 54 | - run: npm test 55 | - run: npm run cov:send 56 | - run: npm run cov:check 57 | 58 | workflows: 59 | version: 2 60 | build: 61 | jobs: 62 | - 'node-10' 63 | - 'node-12' 64 | - 'node-latest' 65 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { "project": "./tsconfig.json" }, 5 | "env": { "es6": true }, 6 | "ignorePatterns": ["node_modules", "build", "coverage"], 7 | "plugins": ["import"], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/typescript", 12 | "prettier" 13 | ], 14 | "globals": { "BigInt": true, "console": true, "WebAssembly": true }, 15 | "rules": { 16 | "@typescript-eslint/explicit-module-boundary-types": "off", 17 | "import/order": [ 18 | "error", 19 | { "newlines-between": "always", "alphabetize": { "order": "asc" } } 20 | ], 21 | "sort-imports": [ 22 | "error", 23 | { "ignoreDeclarationSort": true, "ignoreCase": true } 24 | ], 25 | "@typescript-eslint/no-explicit-any": "off" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Example Contributing Guidelines 2 | 3 | This is an example of GitHub's contributing guidelines file. Check out GitHub's [CONTRIBUTING.md help center article](https://help.github.com/articles/setting-guidelines-for-repository-contributors/) for more information. 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **I'm submitting a ...** 2 | [ ] bug report 3 | [ ] feature request 4 | [ ] question about the decisions made in the repository 5 | [ ] question about how to use this project 6 | 7 | - **Summary** 8 | 9 | - **Other information** (e.g. detailed explanation, stack traces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.) 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 2 | 3 | - **What is the current behavior?** (You can also link to an open issue here) 4 | 5 | - **What is the new behavior (if this is a feature change)?** 6 | 7 | - **Other information**: 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | .nyc_output 3 | build 4 | node_modules 5 | test 6 | src/**.js 7 | coverage 8 | *.log 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore(): release v${version}" 4 | }, 5 | "github": { 6 | "release": true 7 | } 8 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "eamodio.gitlens", 6 | "streetsidesoftware.code-spell-checker", 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | // To debug, make sure a *.spec.ts file is active in the editor, then run a configuration 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Debug Active Spec", 9 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", 10 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"], 11 | "port": 9229, 12 | "outputCapture": "std", 13 | "skipFiles": ["/**/*.js"], 14 | "preLaunchTask": "npm: build" 15 | // "smartStep": true 16 | }, 17 | { 18 | // Use this one if you're already running `yarn watch` 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Debug Active Spec (no build)", 22 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", 23 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"], 24 | "port": 9229, 25 | "outputCapture": "std", 26 | "skipFiles": ["/**/*.js"] 27 | // "smartStep": true 28 | }] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.userWords": [], // only use words from .cspell.json 3 | "cSpell.enabled": true, 4 | "editor.formatOnSave": true, 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | "typescript.enablePromptUseWorkspaceTsdk": true 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### 1.0.4 (2024-04-08) 6 | 7 | - Updated reactotron-core-client version 8 | - Added custom command to invalidate queries 9 | - Updated readme 10 | 11 | # Changelog 12 | 13 | ### 1.0.1 (2023-01-01) 14 | 15 | - Update README.md 16 | 17 | # Changelog 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactotron React Query for React Native 2 | 3 | ![Screenshot](reactotron.png) 4 | 5 | Is there a plugin for Reactotron that allows for similar functionality to the React Query Devtools? Yes! This plugin helps you debug your React Query cache and queries in Reactotron. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm i @tanstack/react-query 11 | npm i reactotron-react-native --save-dev 12 | npm i reactotron-react-query --save-dev 13 | ``` 14 | 15 | ## Usage 16 | 17 | Create a file queryClient.ts 18 | 19 | ```typescript 20 | import { QueryClient } from '@tanstack/react-query'; 21 | const queryClient = new QueryClient(); 22 | 23 | export { queryClient }; 24 | ``` 25 | 26 | Create a file reactotron.ts 27 | 28 | ```typescript 29 | import Reactotron from 'reactotron-react-native'; 30 | import { 31 | QueryClientManager, 32 | reactotronReactQuery, 33 | } from 'reactotron-react-query'; 34 | import { queryClient } from './queryClient'; 35 | 36 | const queryClientManager = new QueryClientManager({ 37 | // @ts-ignore 38 | queryClient, 39 | }); 40 | 41 | Reactotron.use(reactotronReactQuery(queryClientManager)) 42 | .configure({ 43 | onDisconnect: () => { 44 | queryClientManager.unsubscribe(); 45 | }, 46 | }) 47 | .useReactNative() 48 | .connect(); 49 | ``` 50 | 51 | Import the queryClient and reactotron in your App.jsx file. 52 | 53 | ```jsx 54 | import { StyleSheet, Text, View } from 'react-native'; 55 | import { QueryClientProvider } from 'react-query'; 56 | import { queryClient } from './queryClient'; 57 | 58 | if (__DEV__) { 59 | require('./reactotron.ts'); 60 | } 61 | 62 | export default function App() { 63 | return ( 64 | 65 | 66 | Open up App.js to start working on your app! 67 | 68 | 69 | ); 70 | } 71 | 72 | const styles = StyleSheet.create({ 73 | container: { 74 | flex: 1, 75 | backgroundColor: '#fff', 76 | alignItems: 'center', 77 | justifyContent: 'center', 78 | }, 79 | }); 80 | ``` 81 | 82 | ### How to invalidate queries 83 | 84 | If you want to invalidate a query, you can use custom commands. 85 | 86 | ![Screenshot](art/invalidate-queries.png) 87 | -------------------------------------------------------------------------------- /art/invalidate-queries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsndmr/reactotron-react-query/c78b87c63df47bc5cfc6b569cb9be42fe479c9c5/art/invalidate-queries.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, View } from 'react-native'; 2 | import { QueryClientProvider } from '@tanstack/react-query'; 3 | import { queryClient } from './queryClient'; 4 | import PokemonList from './PokemonList'; 5 | import React from 'react'; 6 | 7 | if (__DEV__) { 8 | require('./reactotron.ts'); 9 | } 10 | 11 | export default function App() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | flex: 1, 24 | backgroundColor: '#fff', 25 | alignItems: 'center', 26 | justifyContent: 'center', 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /example/PokemonList.tsx: -------------------------------------------------------------------------------- 1 | import { FlatList, Text, View } from 'react-native'; 2 | import { useQuery } from '@tanstack/react-query'; 3 | import React from 'react'; 4 | 5 | export interface Pokemon { 6 | id: number; 7 | name: string; 8 | } 9 | 10 | const fetchPokemons = async function (): Promise { 11 | const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=151'); 12 | const data = await response.json(); 13 | return data.results.map((result: any, index: number) => { 14 | return { 15 | id: index + 1, 16 | name: result.name, 17 | }; 18 | }); 19 | }; 20 | 21 | export default function PokemonList() { 22 | const { isLoading, data } = useQuery({ 23 | queryKey: ['pokemons'], 24 | queryFn: fetchPokemons, 25 | }) as { 26 | isLoading: boolean; 27 | data: Pokemon[]; 28 | }; 29 | 30 | if (isLoading) { 31 | return ( 32 | 33 | Loading... 34 | 35 | ); 36 | } 37 | 38 | return ( 39 | item.id.toString()} 41 | data={data} 42 | renderItem={({ item }) => { 43 | return ( 44 | 45 | {item.name} 46 | 47 | ); 48 | }} 49 | /> 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./assets/adaptive-icon.png", 23 | "backgroundColor": "#ffffff" 24 | } 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsndmr/reactotron-react-query/c78b87c63df47bc5cfc6b569cb9be42fe479c9c5/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsndmr/reactotron-react-query/c78b87c63df47bc5cfc6b569cb9be42fe479c9c5/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsndmr/reactotron-react-query/c78b87c63df47bc5cfc6b569cb9be42fe479c9c5/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsndmr/reactotron-react-query/c78b87c63df47bc5cfc6b569cb9be42fe479c9c5/example/assets/splash.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@tanstack/react-query": "^5.8.4", 13 | "expo": "~49.0.15", 14 | "expo-status-bar": "~1.6.0", 15 | "react": "18.2.0", 16 | "react-native": "0.72.6", 17 | "reactotron-react-query": "file:../reactotron-react-query-1.0.3.tgz", 18 | "reactotron-react-native": "^5.0.3" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.20.0", 22 | "@types/react": "~18.2.14", 23 | "typescript": "^5.1.3" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /example/queryClient.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query'; 2 | 3 | const queryClient = new QueryClient(); 4 | 5 | export { queryClient }; 6 | -------------------------------------------------------------------------------- /example/reactotron.ts: -------------------------------------------------------------------------------- 1 | import Reactotron from 'reactotron-react-native'; 2 | import { 3 | QueryClientManager, 4 | reactotronReactQuery, 5 | } from 'reactotron-react-query'; 6 | import { queryClient } from './queryClient'; 7 | 8 | const queryClientManager = new QueryClientManager({ 9 | // @ts-ignore 10 | queryClient, 11 | }); 12 | 13 | Reactotron.use(reactotronReactQuery(queryClientManager)) 14 | .configure({ 15 | onDisconnect: () => { 16 | queryClientManager.unsubscribe(); 17 | }, 18 | }) 19 | .useReactNative() 20 | .connect(); 21 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "strict": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactotron-react-query", 3 | "version": "1.0.3", 4 | "description": "", 5 | "main": "build/main/index.js", 6 | "typings": "build/main/index.d.ts", 7 | "repository": "https://github.com/hsndmr/reactotron-react-query", 8 | "license": "MIT", 9 | "keywords": [], 10 | "scripts": { 11 | "build": "rimraf -rf build && tsc -p tsconfig.prod.json", 12 | "format": "prettier --write \"{src,test}/**/*.ts\"", 13 | "lint": "eslint 'src/**/*.ts' --fix", 14 | "prepublish:npm": "npm run build", 15 | "publish:npm": "npm publish --access public", 16 | "prepublish:next": "npm run build", 17 | "publish:next": "npm publish --access public --tag next", 18 | "test": "jest", 19 | "prerelease": "npm run build", 20 | "release": "release-it" 21 | }, 22 | "engines": { 23 | "node": ">=10" 24 | }, 25 | "peerDependencies": { 26 | "react-query": "^3.39.2", 27 | "reactotron-core-client": "^2.9.3" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "29.5.12", 31 | "@typescript-eslint/eslint-plugin": "7.5.0", 32 | "@typescript-eslint/parser": "7.5.0", 33 | "eslint": "8.57.0", 34 | "eslint-config-prettier": "9.1.0", 35 | "eslint-plugin-import": "2.29.1", 36 | "husky": "8.0.2", 37 | "jest": "29.7.0", 38 | "lint-staged": "13.1.0", 39 | "prettier": "3.2.5", 40 | "react-dom": "^18.2.0", 41 | "react-query": "^3.39.2", 42 | "reflect-metadata": "0.1.13", 43 | "release-it": "17.1.1", 44 | "rimraf": "3.0.2", 45 | "ts-jest": "29.0.3", 46 | "typescript": "4.7.4" 47 | }, 48 | "files": [ 49 | "build/main", 50 | "!**/*.spec.*", 51 | "!**/*.json", 52 | "CHANGELOG.md", 53 | "LICENSE", 54 | "README.md" 55 | ], 56 | "config": { 57 | "commitizen": { 58 | "path": "cz-conventional-changelog" 59 | } 60 | }, 61 | "prettier": { 62 | "singleQuote": true 63 | }, 64 | "jest": { 65 | "moduleFileExtensions": [ 66 | "js", 67 | "json", 68 | "ts" 69 | ], 70 | "rootDir": "src", 71 | "testRegex": ".*\\.spec\\.ts$", 72 | "transform": { 73 | "^.+\\.(t|j)s$": "ts-jest" 74 | }, 75 | "collectCoverageFrom": [ 76 | "**/*.(t|j)s" 77 | ], 78 | "coverageDirectory": "../coverage", 79 | "testEnvironment": "node" 80 | }, 81 | "lint-staged": { 82 | "*.ts": [ 83 | "prettier --write" 84 | ] 85 | }, 86 | "husky": { 87 | "hooks": { 88 | "pre-commit": "lint-staged" 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /reactotron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsndmr/reactotron-react-query/c78b87c63df47bc5cfc6b569cb9be42fe479c9c5/reactotron.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/reactotron-react-query'; 2 | export * from './lib/query-client-manager'; 3 | -------------------------------------------------------------------------------- /src/lib/invalidate-react-query-command.spec.ts: -------------------------------------------------------------------------------- 1 | import invalidateReactQueryCommand from './invalidate-react-query-command'; 2 | 3 | const queryClientManagerMock = { 4 | invalidateQueries: jest.fn(), 5 | }; 6 | 7 | describe('invalidateReactQueryCommand', () => { 8 | it('should invalidate query when queryKey is provided', () => { 9 | // Arrange 10 | const args = { queryKey: 'exampleQueryKey' }; 11 | 12 | // Act 13 | const command = invalidateReactQueryCommand(queryClientManagerMock as any); 14 | command.handler(args); 15 | 16 | // Assertion 17 | expect(queryClientManagerMock.invalidateQueries).toHaveBeenCalledWith( 18 | 'exampleQueryKey', 19 | ); 20 | }); 21 | 22 | it('should not invalidate query when queryKey is not provided', () => { 23 | // Act 24 | const command = invalidateReactQueryCommand(queryClientManagerMock as any); 25 | command.handler({} as any); 26 | 27 | // Assertion 28 | expect(queryClientManagerMock.invalidateQueries).toHaveBeenCalledWith( 29 | undefined, 30 | ); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/lib/invalidate-react-query-command.ts: -------------------------------------------------------------------------------- 1 | import { ArgType, CustomCommand } from 'reactotron-core-client'; 2 | 3 | import { QueryClientManager } from './query-client-manager'; 4 | 5 | export default function invalidateReactQueryCommand( 6 | queryClientManager: QueryClientManager, 7 | ): CustomCommand<[{ name: 'queryKey'; type: ArgType.String }]> { 8 | return { 9 | command: 'invalidate-react-query', 10 | handler: (args) => { 11 | const { queryKey } = args ?? {}; 12 | queryClientManager.invalidateQueries(queryKey); 13 | }, 14 | title: 'Invalidate React Query', 15 | description: 16 | 'Invalidate a query by key. If no key is provided, all queries will be invalidated.', 17 | args: [{ name: 'queryKey', type: ArgType.String }], 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/query-cache-notify-event.interface.ts: -------------------------------------------------------------------------------- 1 | import { Action, Query } from 'react-query/types/core/query'; 2 | import { QueryObserver } from 'react-query/types/core/queryObserver'; 3 | 4 | interface NotifyEventQueryAdded { 5 | type: 'queryAdded'; 6 | query: Query; 7 | } 8 | interface NotifyEventQueryRemoved { 9 | type: 'queryRemoved'; 10 | query: Query; 11 | } 12 | interface NotifyEventQueryUpdated { 13 | type: 'queryUpdated'; 14 | query: Query; 15 | action: Action; 16 | } 17 | interface NotifyEventObserverAdded { 18 | type: 'observerAdded'; 19 | query: Query; 20 | observer: QueryObserver; 21 | } 22 | interface NotifyEventObserverRemoved { 23 | type: 'observerRemoved'; 24 | query: Query; 25 | observer: QueryObserver; 26 | } 27 | interface NotifyEventObserverResultsUpdated { 28 | type: 'observerResultsUpdated'; 29 | query: Query; 30 | } 31 | export type QueryCacheNotifyEvent = 32 | | NotifyEventQueryAdded 33 | | NotifyEventQueryRemoved 34 | | NotifyEventQueryUpdated 35 | | NotifyEventObserverAdded 36 | | NotifyEventObserverRemoved 37 | | NotifyEventObserverResultsUpdated; 38 | -------------------------------------------------------------------------------- /src/lib/query-client-manager-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from 'react-query'; 2 | 3 | export interface QueryClientManagerOptions { 4 | queryClient: QueryClient; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/query-client-manager.spec.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from 'react-query'; 2 | 3 | import { QueryClientManager } from './query-client-manager'; 4 | 5 | let eventCacheListener: (() => void) | undefined; 6 | jest.mock('react-query', () => { 7 | const originalModule = jest.requireActual('react-query'); 8 | const queryClientMock = class QueryClientMock { 9 | eventListener: undefined; 10 | getQueryCache() { 11 | return { 12 | subscribe(callback: () => undefined) { 13 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 14 | // @ts-ignore 15 | eventCacheListener = callback; 16 | return () => { 17 | eventCacheListener = undefined; 18 | }; 19 | }, 20 | getAll() { 21 | return [ 22 | { 23 | queryHash: 'queryHash', 24 | fetch() { 25 | return 'fetch'; 26 | }, 27 | }, 28 | ]; 29 | }, 30 | }; 31 | } 32 | }; 33 | 34 | return { 35 | __esModule: true, 36 | ...originalModule, 37 | QueryClient: queryClientMock, 38 | }; 39 | }); 40 | 41 | describe('QueryClientManager', () => { 42 | const queryClient = new QueryClient(); 43 | const queryClientManager = new QueryClientManager({ 44 | queryClient, 45 | }); 46 | 47 | it('can have queries', () => { 48 | expect(1).toBe(queryClientManager.getQueries().length); 49 | }); 50 | 51 | it('can find a query by hash', () => { 52 | expect(queryClientManager.getQueryByHash('queryHash')).toBeDefined(); 53 | }); 54 | 55 | it('can fetch a query by hash', () => { 56 | expect(queryClientManager.fetchQueryByHash('queryHash')).toBe('fetch'); 57 | }); 58 | 59 | it('can subscribe to query cache events', () => { 60 | const callback = jest.fn(); 61 | queryClientManager.subscribe(callback); 62 | 63 | eventCacheListener && eventCacheListener(); 64 | eventCacheListener && eventCacheListener(); 65 | 66 | expect(callback).toBeCalledTimes(2); 67 | }); 68 | 69 | it('can unsubscribe from query cache events', () => { 70 | const callback = jest.fn(); 71 | queryClientManager.subscribe(callback); 72 | 73 | queryClientManager.unsubscribe(); 74 | 75 | expect(queryClientManager.queryCacheEvent).toBeUndefined(); 76 | }); 77 | 78 | it('can invalidate queries', () => { 79 | // Arrange 80 | queryClient.invalidateQueries = jest.fn(); 81 | queryClientManager.invalidateQueries('key'); 82 | 83 | // Act & Assert 84 | expect(queryClient.invalidateQueries).toBeCalledWith({ 85 | queryKey: ['key'], 86 | }); 87 | }); 88 | 89 | it('can invalidate all queries', () => { 90 | // Arrange 91 | queryClient.invalidateQueries = jest.fn(); 92 | queryClientManager.invalidateQueries(); 93 | 94 | // Act & Assert 95 | expect(queryClient.invalidateQueries).toBeCalledWith({ 96 | queryKey: [undefined], 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/lib/query-client-manager.ts: -------------------------------------------------------------------------------- 1 | import { Query, QueryClient } from 'react-query'; 2 | 3 | import { QueryCacheNotifyEvent } from './query-cache-notify-event.interface'; 4 | import { QueryClientManagerOptions } from './query-client-manager-options.interface'; 5 | export class QueryClientManager { 6 | queryClient: QueryClient; 7 | 8 | queryCacheEvent: (() => void) | undefined; 9 | 10 | constructor(options: QueryClientManagerOptions) { 11 | this.queryClient = options.queryClient; 12 | } 13 | 14 | getQueryCache() { 15 | return this.queryClient.getQueryCache(); 16 | } 17 | 18 | getQueries(): Query[] { 19 | return this.getQueryCache().getAll(); 20 | } 21 | 22 | getQueryByHash(queryHash: string): Query | undefined { 23 | return this.getQueries().find((query) => query.queryHash === queryHash); 24 | } 25 | 26 | fetchQueryByHash(queryHash: string) { 27 | return this.getQueryByHash(queryHash)?.fetch(); 28 | } 29 | 30 | subscribe(callback: (event: QueryCacheNotifyEvent | undefined) => void) { 31 | this.queryCacheEvent = this.queryClient 32 | .getQueryCache() 33 | .subscribe((_event) => { 34 | callback(_event); 35 | }); 36 | } 37 | 38 | unsubscribe() { 39 | if (this.queryCacheEvent) { 40 | this.queryCacheEvent(); 41 | this.queryCacheEvent = undefined; 42 | } 43 | } 44 | 45 | invalidateQueries(key?: string) { 46 | return this.queryClient.invalidateQueries({ 47 | queryKey: [key], 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/reactotron-helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { broadcastReactQueryEvent } from './reactotron-helpers'; 2 | jest.mock('reactotron-core-client'); 3 | 4 | describe('broadcastReactQueryEvent', () => { 5 | const createReactotron = () => { 6 | return { 7 | stateActionComplete: jest.fn(), 8 | stateValuesResponse: jest.fn(), 9 | display: jest.fn(), 10 | }; 11 | }; 12 | 13 | it('can broadcast a queryUpdated event with isFetching=true', () => { 14 | const reactotron = createReactotron(); 15 | const event = { 16 | type: 'queryUpdated', 17 | query: { 18 | queryHash: 'queryHash', 19 | state: { 20 | isFetching: false, 21 | }, 22 | }, 23 | }; 24 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 25 | // @ts-ignore 26 | broadcastReactQueryEvent(reactotron, event); 27 | 28 | expect(reactotron.display).toHaveBeenCalledWith({ 29 | name: `${event?.type}${event?.query.queryHash}`, 30 | value: event?.query, 31 | }); 32 | }); 33 | it('can broadcast a event that is not queryUpdated', () => { 34 | const reactotron = createReactotron(); 35 | const event = { 36 | type: 'event', 37 | query: { 38 | queryHash: 'queryHash', 39 | state: { 40 | isFetching: true, 41 | }, 42 | }, 43 | }; 44 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 45 | // @ts-ignore 46 | broadcastReactQueryEvent(reactotron, event); 47 | 48 | expect(reactotron.display).toHaveBeenCalledWith({ 49 | name: `${event?.type}${event?.query.queryHash}`, 50 | value: event?.query, 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/lib/reactotron-helpers.ts: -------------------------------------------------------------------------------- 1 | import { ReactotronCore } from 'reactotron-core-client'; 2 | 3 | import { QueryCacheNotifyEvent } from './query-cache-notify-event.interface'; 4 | 5 | export const broadcastReactQueryEvent = ( 6 | reactotron: ReactotronCore, 7 | event: QueryCacheNotifyEvent | undefined, 8 | ) => { 9 | reactotron.display({ 10 | name: `${event?.type}${event?.query.queryHash}`, 11 | value: event?.query, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/reactotron-react-query.ts: -------------------------------------------------------------------------------- 1 | import { ReactotronCore } from 'reactotron-core-client'; 2 | 3 | import invalidateReactQueryCommand from './invalidate-react-query-command'; 4 | import { QueryClientManager } from './query-client-manager'; 5 | import { broadcastReactQueryEvent } from './reactotron-helpers'; 6 | 7 | // fix types next release 8 | type ReactotronReactQuery = (queryClientManager: any) => any; 9 | 10 | function reactotronReactQuery( 11 | queryClientManager: QueryClientManager, 12 | ): ReactotronReactQuery { 13 | return (reactotron: ReactotronCore) => { 14 | queryClientManager.subscribe((event) => 15 | broadcastReactQueryEvent(reactotron, event), 16 | ); 17 | 18 | reactotron.onCustomCommand(invalidateReactQueryCommand(queryClientManager)); 19 | 20 | return { 21 | onCommand: ({ type, payload }: { type: string; payload?: any }) => { 22 | switch (type) { 23 | case 'state.action.dispatch': 24 | if (payload.action.queryHash) { 25 | queryClientManager.fetchQueryByHash(payload.action.queryHash); 26 | } 27 | break; 28 | } 29 | }, 30 | }; 31 | }; 32 | } 33 | 34 | export { reactotronReactQuery }; 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2017", 5 | "outDir": "build/main", 6 | "rootDir": "src", 7 | "moduleResolution": "node", 8 | "module": "commonjs", 9 | "declaration": true, 10 | "inlineSourceMap": true, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "resolveJsonModule": true /* Include modules imported with .json extension. */, 13 | 14 | // "strict": true /* Enable all strict type-checking options. */, 15 | 16 | /* Strict Type-Checking Options */ 17 | // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 18 | // "strictNullChecks": true /* Enable strict null checks. */, 19 | // "strictFunctionTypes": true /* Enable strict checking of function types. */, 20 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 21 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 22 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 23 | 24 | /* Additional Checks */ 25 | "noUnusedLocals": true /* Report errors on unused locals. */, 26 | "noUnusedParameters": true /* Report errors on unused parameters. */, 27 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 28 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 29 | 30 | /* Debugging Options */ 31 | "traceResolution": false /* Report module resolution log messages. */, 32 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 33 | "listFiles": false /* Print names of files part of the compilation. */, 34 | "pretty": true /* Stylize errors and messages using color and context. */, 35 | 36 | /* Experimental Options */ 37 | // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 38 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 39 | 40 | "lib": ["es2017"], 41 | "types": ["node", "jest"], 42 | "typeRoots": ["node_modules/@types", "src/types"], 43 | "skipLibCheck": true 44 | }, 45 | "include": ["src/**/*.ts"], 46 | "exclude": ["node_modules"], 47 | "compileOnSave": false 48 | } 49 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "**/*.spec.ts"] 4 | } 5 | --------------------------------------------------------------------------------