├── .babelrc ├── .browserslistrc ├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── publish.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── jest.config.js ├── media ├── header.sketch └── repo-dark.png ├── package.json ├── prettier.config.js ├── rollup.config.js ├── src ├── Explorer.js ├── Logo.js ├── __tests__ │ └── index.js ├── index.js ├── styledComponents.js ├── theme.js ├── useLocalStorage.js ├── useMediaQuery.js └── utils.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "modules": false, 7 | "loose": true, 8 | "exclude": ["@babel/plugin-transform-regenerator"] 9 | } 10 | ], 11 | "@babel/react" 12 | ], 13 | "plugins": ["babel-plugin-transform-async-to-promises"], 14 | "env": { 15 | "test": { 16 | "presets": [ 17 | [ 18 | "@babel/env", 19 | { 20 | "modules": "commonjs", 21 | "loose": true, 22 | "exclude": ["@babel/plugin-transform-regenerator"] 23 | } 24 | ] 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers we support 2 | > 0.5% 3 | Chrome >= 73 4 | ChromeAndroid >= 75 5 | Firefox >= 67 6 | Edge >= 17 7 | IE 11 8 | Safari >= 12.1 9 | iOS >= 11.3 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["react-app", "prettier"], 4 | "env": { 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "sourceType": "module" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tannerlinsley 2 | custom: https://youtube.com/tannerlinsley 3 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: react-query-devtools publish 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | - 'beta' 7 | pull_request: 8 | 9 | jobs: 10 | publish-module: 11 | name: 'Publish Module to NPM' 12 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/beta' 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 1 18 | - uses: actions/setup-node@v1 19 | with: 20 | node-version: 12 21 | registry-url: https://registry.npmjs.org/ 22 | - run: npm i -g yarn 23 | - run: yarn --frozen-lockfile 24 | - run: npx semantic-release@17 25 | env: 26 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 27 | GH_TOKEN: ${{secrets.GH_TOKEN}} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | artifacts 11 | .rpt2_cache 12 | coverage 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | .next 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .history 27 | size-plugin.json 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "titleBar.activeBackground": "#00ac4e", // change this color! 4 | "titleBar.inactiveBackground": "#00ac4e", // change this color! 5 | "titleBar.activeForeground": "#ffffff", // change this color! 6 | "titleBar.inactiveForeground": "#ffffff" // change this color! 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.5 4 | 5 | - Added default styles for inputs and select boxes 6 | - Fixed a bug where `match-sorter` was not a dependency 7 | 8 | ## 1.1.4 9 | 10 | - Added the ability to filter the query list 11 | 12 | ## 1.1.2 - 1.1.3 13 | 14 | - Updated the global package namespace from `ReactQuery` to `ReactQueryDevtools` 15 | 16 | ## 1.1.1 17 | 18 | - Fixed an issue where arrow functions were present in the production placeholder build 19 | 20 | ## 1.1.0 21 | 22 | - Added the ability to add props to all three internal dev tools components 23 | - Fixed an issue where z-index was not initially set to something very high 24 | 25 | ## 1.0.13 26 | 27 | - Better overflow styles 28 | - No more useLayoutEffect warnings in non-window environments like Next.js 29 | 30 | ## 1.0.12 31 | 32 | - Prevent queries header from scrolling 33 | 34 | ## 1.0.11 35 | 36 | - No text shadow on stale indicators 37 | 38 | ## 1.0.10 39 | 40 | - Fix instance count styles 41 | 42 | ## 1.0.9 43 | 44 | - Default sort by status 45 | - Add instance count next to query 46 | 47 | ## 1.0.8 48 | 49 | - Sort by status is now fixed for fresh queries 50 | 51 | ## 1.0.7 52 | 53 | - Buttons now use a pointer cursor style 54 | 55 | ## 1.0.6 56 | 57 | - Removes Codemirror Editing until a non-css-file based solution can be found 58 | 59 | ## 1.0.5 60 | 61 | - Fixes an issue where localStorage could not be accessed and would crash 62 | - Removes some stray logging 63 | 64 | ## 1.0.4 65 | 66 | - Fix: Devtools will import noop components during production by default. Updated Readme with information on how to use in production as well. 67 | 68 | ## 1.0.3 69 | 70 | - Styles: Toggle button has less of a text-shadow now 71 | 72 | ## 1.0.2 73 | 74 | Initial Release! 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tanner Linsley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This repo and package has been deprecated in favor of the new [built-in devtools](https://react-query.tanstack.com/devtools) that ship with React Query v3. This repo will remain here for people still using v2. 4 | 5 | # Documentation 6 | 7 | 8 | 9 | 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Floating Mode](#floating-mode) 13 | - [Embedded Mode](#embedded-mode) 14 | 15 | 16 | 17 | # Installation 18 | 19 | ```bash 20 | $ npm i --save react-query-devtools 21 | # or 22 | $ yarn add react-query-devtools 23 | ``` 24 | 25 | Using React Native? Try [react-query-native-devtools](https://github.com/bgaleotti/react-query-native-devtools) instead. 26 | 27 | # Usage 28 | 29 | By default, React Query Devtools are not imported and used when `process.env.NODE_ENV === 'production'`, so you don't need to worry about excluding them during a production build. 30 | 31 | If you want to use the devtools in production, you can manually import them (preferably asynchronously code-split) by importing the `dist/react-query-devtools.production.min.js` file directly. 32 | 33 | ## Floating Mode 34 | 35 | Floating Mode will mount the devtools as a fixed, floating element in your app and provide a toggle in the corner of the screen to show and hide the devtools. This toggle state will be stored and remembered in localStorage across reloads. 36 | 37 | Place the following code as high in your React app as you can. The closer it is to the root of the page, the better it will work! 38 | 39 | ```js 40 | import { ReactQueryDevtools } from 'react-query-devtools' 41 | 42 | function App() { 43 | return ( 44 | <> 45 | {/* The rest of your application */} 46 | 47 | 48 | ) 49 | } 50 | ``` 51 | 52 | ### Options 53 | 54 | - `initialIsOpen: Boolean` 55 | - Set this `true` if you want the dev tools to default to being open 56 | - `panelProps: PropsObject` 57 | - Use this to add props to the panel. For example, you can add `className`, `style` (merge and override default style), etc. 58 | - `closeButtonProps: PropsObject` 59 | - Use this to add props to the close button. For example, you can add `className`, `style` (merge and override default style), `onClick` (extend default handler), etc. 60 | - `toggleButtonProps: PropsObject` 61 | - Use this to add props to the toggle button. For example, you can add `className`, `style` (merge and override default style), `onClick` (extend default handler), etc. 62 | - `position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"` 63 | - Defaults to `bottom-left` 64 | - The position of the React Query logo to open and close the devtools panel 65 | 66 | ## Embedded Mode 67 | 68 | Embedded Mode will embed the devtools as a regular component in your application. You can style it however you'd like after that! 69 | 70 | ```js 71 | import { ReactQueryDevtoolsPanel } from 'react-query-devtools' 72 | 73 | function App() { 74 | return ( 75 | <> 76 | {/* The rest of your application */} 77 | 78 | 79 | ) 80 | } 81 | ``` 82 | 83 | ### Options 84 | 85 | Use these options to style the dev tools. 86 | 87 | - `style: StyleObject` 88 | - The standard React style object used to style a component with inline styles 89 | - `className: string` 90 | - The standard React className property used to style a component with classes 91 | 92 | 93 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export function ReactQueryDevtools(props: { 2 | /** 3 | * Set this true if you want the dev tools to default to being open 4 | */ 5 | initialIsOpen?: boolean 6 | /** 7 | * Use this to add props to the panel. For example, you can add className, style (merge and override default style), etc. 8 | */ 9 | panelProps?: React.DetailedHTMLProps< 10 | React.HTMLAttributes, 11 | HTMLDivElement 12 | > 13 | /** 14 | * Use this to add props to the close button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc. 15 | */ 16 | closeButtonProps?: React.DetailedHTMLProps< 17 | React.ButtonHTMLAttributes, 18 | HTMLButtonElement 19 | > 20 | /** 21 | * Use this to add props to the toggle button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc. 22 | */ 23 | toggleButtonProps?: React.DetailedHTMLProps< 24 | React.ButtonHTMLAttributes, 25 | HTMLButtonElement 26 | > 27 | /** 28 | * The position of the React Query logo to open and close the devtools panel. 29 | * Defaults to 'bottom-left'. 30 | */ 31 | position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' 32 | /** 33 | * Use this to render the devtools inside a different type of container element for a11y purposes. 34 | * Any string which corresponds to a valid intrinsic JSX element is allowed. 35 | * Defaults to 'footer'. 36 | */ 37 | containerElement?: keyof JSX.IntrinsicElements 38 | }): React.ReactElement 39 | 40 | export function ReactQueryDevtoolsPanel(props: { 41 | /** 42 | * The standard React style object used to style a component with inline styles 43 | */ 44 | style?: React.CSSProperties 45 | /** 46 | * The standard React className property used to style a component with classes 47 | */ 48 | className?: string 49 | }): React.ReactElement 50 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = { 3 | ReactQueryDevtools: function() {return null;}, 4 | ReactQueryDevtoolsPanel: function() {return null;}, 5 | } 6 | } else { 7 | module.exports = require('./dist/react-query-devtools.development.js') 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | coverageReporters: ["json", "lcov", "text", "clover", "text-summary"] 4 | } 5 | -------------------------------------------------------------------------------- /media/header.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tannerlinsley/react-query-devtools/d8cd2cb107a7cfaabb42b0fbc9fb144b343ec51c/media/header.sketch -------------------------------------------------------------------------------- /media/repo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tannerlinsley/react-query-devtools/d8cd2cb107a7cfaabb42b0fbc9fb144b343ec51c/media/repo-dark.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-query-devtools", 3 | "version": "0.0.0", 4 | "description": "Devtools for React Query", 5 | "author": "tannerlinsley", 6 | "license": "MIT", 7 | "repository": "tannerlinsley/react-query-devtools", 8 | "main": "index.js", 9 | "types": "index.d.ts", 10 | "sideEffects": false, 11 | "scripts": { 12 | "test": "is-ci \"test:ci\" \"test:dev\"", 13 | "test:dev": "jest --watch", 14 | "test:ci": "jest", 15 | "test:coverage": "yarn test:ci; open coverage/lcov-report/index.html", 16 | "build": "NODE_ENV=production rollup -c", 17 | "now-build": "yarn && cd www && yarn && yarn build", 18 | "start": "rollup -c -w", 19 | "prepare": "yarn build", 20 | "release": "yarn publish", 21 | "releaseNext": "yarn publish --tag next", 22 | "format": "prettier {src,src/**,example/src,example/src/**}/*.{md,js,jsx,tsx} --write", 23 | "doctoc": "npx doctoc --maxlevel 2 README.md" 24 | }, 25 | "files": [ 26 | "dist", 27 | "index.d.ts" 28 | ], 29 | "dependencies": { 30 | "match-sorter": "^4.1.0" 31 | }, 32 | "peerDependencies": { 33 | "react": "^16.6.3 || ^17.0.0", 34 | "react-query": "^2.0.0" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.9.0", 38 | "@babel/preset-env": "^7.9.0", 39 | "@babel/preset-react": "^7.8.3", 40 | "@rollup/plugin-json": "^4.0.2", 41 | "@rollup/plugin-replace": "^2.3.1", 42 | "@svgr/rollup": "^4.3.0", 43 | "@testing-library/react": "^9.4.0", 44 | "babel-core": "7.0.0-bridge.0", 45 | "babel-eslint": "9.x", 46 | "babel-jest": "^24.9.0", 47 | "babel-plugin-transform-async-to-promises": "^0.8.15", 48 | "cross-env": "^5.1.4", 49 | "eslint": "5.x", 50 | "eslint-config-prettier": "^4.3.0", 51 | "eslint-config-react-app": "^4.0.1", 52 | "eslint-config-standard": "^12.0.0", 53 | "eslint-config-standard-react": "^7.0.2", 54 | "eslint-plugin-flowtype": "2.x", 55 | "eslint-plugin-import": "2.x", 56 | "eslint-plugin-jsx-a11y": "6.x", 57 | "eslint-plugin-node": "^9.1.0", 58 | "eslint-plugin-prettier": "^3.1.2", 59 | "eslint-plugin-promise": "^4.1.1", 60 | "eslint-plugin-react": "7.18.3", 61 | "eslint-plugin-react-hooks": "1.5.0", 62 | "eslint-plugin-standard": "^4.0.0", 63 | "is-ci-cli": "^2.0.0", 64 | "jest": "^24.9.0", 65 | "prettier": "^1.19.1", 66 | "react": "^16.12.0", 67 | "react-dom": "^16.12.0", 68 | "rollup": "^1.31.1", 69 | "rollup-plugin-babel": "^4.3.2", 70 | "rollup-plugin-commonjs": "^10.1.0", 71 | "rollup-plugin-jscc": "^1.0.0", 72 | "rollup-plugin-node-resolve": "^5.2.0", 73 | "rollup-plugin-peer-deps-external": "^2.2.2", 74 | "rollup-plugin-prettier": "^0.6.0", 75 | "rollup-plugin-size": "^0.2.2", 76 | "rollup-plugin-size-snapshot": "^0.10.0", 77 | "rollup-plugin-terser": "^5.2.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: false, 6 | singleQuote: true, 7 | trailingComma: "es5", 8 | bracketSpacing: true, 9 | jsxBracketSameLine: false, 10 | arrowParens: "avoid", 11 | endOfLine: "auto" 12 | }; 13 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import { terser } from 'rollup-plugin-terser' 3 | import size from 'rollup-plugin-size' 4 | import json from '@rollup/plugin-json' 5 | import externalDeps from 'rollup-plugin-peer-deps-external' 6 | import resolve from 'rollup-plugin-node-resolve' 7 | import commonJS from 'rollup-plugin-commonjs' 8 | 9 | const globals = { 10 | react: 'React', 11 | 'react-query': 'ReactQuery', 12 | 'react-json-tree': 'JSONTree', 13 | 'match-sorter': 'matchSorter', 14 | } 15 | 16 | export default [ 17 | { 18 | input: 'src/index.js', 19 | output: { 20 | name: 'ReactQueryDevtools', 21 | file: 'dist/react-query-devtools.development.js', 22 | format: 'umd', 23 | sourcemap: true, 24 | globals, 25 | }, 26 | plugins: [resolve(), babel(), commonJS(), externalDeps(), json()], 27 | }, 28 | { 29 | input: 'src/index.js', 30 | output: { 31 | name: 'ReactQueryDevtools', 32 | file: 'dist/react-query-devtools.production.min.js', 33 | format: 'umd', 34 | sourcemap: true, 35 | globals, 36 | }, 37 | plugins: [ 38 | resolve(), 39 | babel(), 40 | commonJS(), 41 | json(), 42 | externalDeps(), 43 | terser(), 44 | size(), 45 | ], 46 | }, 47 | ] 48 | -------------------------------------------------------------------------------- /src/Explorer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { styled } from './utils' 4 | 5 | export const Entry = styled('div', { 6 | fontFamily: 'Menlo, monospace', 7 | fontSize: '0.9rem', 8 | lineHeight: '1.7', 9 | outline: 'none', 10 | }) 11 | 12 | export const Label = styled('span', { 13 | cursor: 'pointer', 14 | color: 'white', 15 | }) 16 | 17 | export const Value = styled('span', (props, theme) => ({ 18 | color: theme.danger, 19 | })) 20 | 21 | export const SubEntries = styled('div', { 22 | marginLeft: '.1rem', 23 | paddingLeft: '1rem', 24 | borderLeft: '2px solid rgba(0,0,0,.15)', 25 | }) 26 | 27 | export const Info = styled('span', { 28 | color: 'grey', 29 | fontSize: '.7rem', 30 | }) 31 | 32 | export const Expander = ({ expanded, style = {}, ...rest }) => ( 33 | 41 | ▶ 42 | 43 | ) 44 | 45 | const DefaultRenderer = ({ 46 | handleEntry, 47 | label, 48 | value, 49 | // path, 50 | subEntries, 51 | subEntryPages, 52 | type, 53 | // depth, 54 | expanded, 55 | toggle, 56 | pageSize, 57 | }) => { 58 | const [expandedPages, setExpandedPages] = React.useState([]) 59 | 60 | return ( 61 | 62 | {subEntryPages?.length ? ( 63 | <> 64 | 71 | {expanded ? ( 72 | subEntryPages.length === 1 ? ( 73 | 74 | {subEntries.map(entry => handleEntry(entry))} 75 | 76 | ) : ( 77 | 78 | {subEntryPages.map((entries, index) => ( 79 |
80 | 81 | 93 | {expandedPages.includes(index) ? ( 94 | 95 | {entries.map(entry => handleEntry(entry))} 96 | 97 | ) : null} 98 | 99 |
100 | ))} 101 |
102 | ) 103 | ) : null} 104 | 105 | ) : ( 106 | <> 107 | {' '} 108 | 109 | {JSON.stringify(value, Object.getOwnPropertyNames(Object(value)))} 110 | 111 | 112 | )} 113 |
114 | ) 115 | } 116 | 117 | export default function Explorer({ 118 | value, 119 | defaultExpanded, 120 | renderer = DefaultRenderer, 121 | pageSize = 100, 122 | depth = 0, 123 | ...rest 124 | }) { 125 | const [expanded, setExpanded] = React.useState(defaultExpanded) 126 | 127 | const toggle = set => { 128 | setExpanded(old => (typeof set !== 'undefined' ? set : !old)) 129 | } 130 | 131 | const path = [] 132 | 133 | let type = typeof value 134 | let subEntries 135 | let subEntryPages = [] 136 | 137 | const makeProperty = sub => { 138 | const newPath = path.concat(sub.label) 139 | const subDefaultExpanded = 140 | defaultExpanded === true 141 | ? { [sub.label]: true } 142 | : defaultExpanded?.[sub.label] 143 | return { 144 | ...sub, 145 | path: newPath, 146 | depth: depth + 1, 147 | defaultExpanded: subDefaultExpanded, 148 | } 149 | } 150 | 151 | if (Array.isArray(value)) { 152 | type = 'array' 153 | subEntries = value.map((d, i) => 154 | makeProperty({ 155 | label: i, 156 | value: d, 157 | }) 158 | ) 159 | } else if ( 160 | value !== null && 161 | typeof value === 'object' && 162 | typeof value[Symbol.iterator] === 'function' 163 | ) { 164 | type = 'Iterable' 165 | subEntries = Array.from(value, (val, i) => 166 | makeProperty({ 167 | label: i, 168 | value: val, 169 | }) 170 | ) 171 | } else if (typeof value === 'object' && value !== null) { 172 | type = 'object' 173 | subEntries = Object.entries(value).map(([label, value]) => 174 | makeProperty({ 175 | label, 176 | value, 177 | }) 178 | ) 179 | } 180 | 181 | if (subEntries) { 182 | let i = 0 183 | 184 | while (i < subEntries.length) { 185 | subEntryPages.push(subEntries.slice(i, i + pageSize)) 186 | i = i + pageSize 187 | } 188 | } 189 | 190 | return renderer({ 191 | handleEntry: entry => ( 192 | 193 | ), 194 | type, 195 | subEntries, 196 | subEntryPages, 197 | depth, 198 | value, 199 | path, 200 | expanded, 201 | toggle, 202 | pageSize, 203 | ...rest, 204 | }) 205 | } 206 | -------------------------------------------------------------------------------- /src/Logo.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function Logo(props) { 4 | return ( 5 | 12 | 19 | 20 | 27 | 32 | 37 | 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | describe('ReactQueryDevtools', () => { 2 | it('does not have tests yet', () => { 3 | expect('yup').toEqual('yup') 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import match from 'match-sorter' 3 | import { queryCache as cache, useQueryCache } from 'react-query' 4 | import useLocalStorage from './useLocalStorage' 5 | import { useSafeState, isStale } from './utils' 6 | 7 | import { 8 | Panel, 9 | QueryKeys, 10 | QueryKey, 11 | Button, 12 | Code, 13 | Input, 14 | Select, 15 | QueryCountStyles, 16 | ActiveQueryPanel, 17 | } from './styledComponents' 18 | import { ThemeProvider } from './theme' 19 | import { 20 | getQueryStatusLabel, 21 | getQueryStatusColor, 22 | getQueryOpacity, 23 | } from './utils' 24 | import Explorer from './Explorer' 25 | import Logo from './Logo' 26 | 27 | const isServer = typeof window === 'undefined' 28 | 29 | const theme = { 30 | background: '#0b1521', 31 | backgroundAlt: '#132337', 32 | foreground: 'white', 33 | gray: '#3f4e60', 34 | grayAlt: '#222e3e', 35 | inputBackgroundColor: '#fff', 36 | inputTextColor: '#000', 37 | success: '#00ab52', 38 | danger: '#ff0085', 39 | active: '#006bff', 40 | warning: '#ffb200', 41 | } 42 | 43 | export function ReactQueryDevtools({ 44 | initialIsOpen, 45 | panelProps = {}, 46 | closeButtonProps = {}, 47 | toggleButtonProps = {}, 48 | position = 'bottom-left', 49 | containerElement: Container = 'footer', 50 | }) { 51 | const rootRef = React.useRef() 52 | const panelRef = React.useRef() 53 | const [isOpen, setIsOpen] = useLocalStorage( 54 | 'reactQueryDevtoolsOpen', 55 | initialIsOpen 56 | ) 57 | const [isResolvedOpen, setIsResolvedOpen] = useSafeState(false) 58 | 59 | React.useEffect(() => { 60 | setIsResolvedOpen(isOpen) 61 | }, [isOpen, isResolvedOpen, setIsResolvedOpen]) 62 | 63 | React[isServer ? 'useEffect' : 'useLayoutEffect'](() => { 64 | if (isResolvedOpen) { 65 | const previousValue = rootRef.current?.parentElement.style.paddingBottom 66 | 67 | const run = () => { 68 | const containerHeight = panelRef.current?.getBoundingClientRect().height 69 | rootRef.current.parentElement.style.paddingBottom = `${containerHeight}px` 70 | } 71 | 72 | run() 73 | 74 | window.addEventListener('resize', run) 75 | 76 | return () => { 77 | window.removeEventListener('resize', run) 78 | rootRef.current.parentElement.style.paddingBottom = previousValue 79 | } 80 | } 81 | }, [isResolvedOpen]) 82 | 83 | const { style: panelStyle = {}, ...otherPanelProps } = panelProps 84 | 85 | const { 86 | style: closeButtonStyle = {}, 87 | onClick: onCloseClick, 88 | ...otherCloseButtonProps 89 | } = closeButtonProps 90 | 91 | const { 92 | style: toggleButtonStyle = {}, 93 | onClick: onToggleClick, 94 | ...otherToggleButtonProps 95 | } = toggleButtonProps 96 | 97 | return ( 98 | 99 | {isResolvedOpen ? ( 100 | 101 | 118 | 149 | 150 | ) : ( 151 | 195 | )} 196 | 197 | ) 198 | } 199 | 200 | const getStatusRank = q => 201 | q.state.isFetching ? 0 : !q.observers.length ? 3 : isStale(q) ? 2 : 1 202 | 203 | const sortFns = { 204 | 'Status > Last Updated': (a, b) => 205 | getStatusRank(a) === getStatusRank(b) 206 | ? sortFns['Last Updated'](a, b) 207 | : getStatusRank(a) > getStatusRank(b) 208 | ? 1 209 | : -1, 210 | 'Query Hash': (a, b) => (a.queryHash > b.queryHash ? 1 : -1), 211 | 'Last Updated': (a, b) => (a.state.updatedAt < b.state.updatedAt ? 1 : -1), 212 | } 213 | 214 | export const ReactQueryDevtoolsPanel = React.forwardRef( 215 | function ReactQueryDevtoolsPanel(props, ref) { 216 | const { setIsOpen, ...panelProps } = props 217 | 218 | const queryCache = useQueryCache ? useQueryCache() : cache 219 | 220 | const [sort, setSort] = useLocalStorage( 221 | 'reactQueryDevtoolsSortFn', 222 | Object.keys(sortFns)[0] 223 | ) 224 | 225 | const [filter, setFilter] = useLocalStorage('reactQueryDevtoolsFilter', '') 226 | 227 | const [sortDesc, setSortDesc] = useLocalStorage( 228 | 'reactQueryDevtoolsSortDesc', 229 | false 230 | ) 231 | 232 | const [isDragging, setIsDragging] = useSafeState(false) 233 | 234 | const sortFn = React.useMemo(() => sortFns[sort], [sort]) 235 | 236 | React[isServer ? 'useEffect' : 'useLayoutEffect'](() => { 237 | if (!sortFn) { 238 | setSort(Object.keys(sortFns)[0]) 239 | } 240 | }, [setSort, sortFn]) 241 | 242 | React[isServer ? 'useEffect' : 'useLayoutEffect'](() => { 243 | if (isDragging) { 244 | const run = e => { 245 | const containerHeight = window.innerHeight - e.pageY 246 | 247 | if (containerHeight < 70) { 248 | setIsOpen(false) 249 | } else { 250 | ref.current.style.height = `${containerHeight}px` 251 | } 252 | } 253 | document.addEventListener('mousemove', run) 254 | document.addEventListener('mouseup', handleDragEnd) 255 | 256 | return () => { 257 | document.removeEventListener('mousemove', run) 258 | document.removeEventListener('mouseup', handleDragEnd) 259 | } 260 | } 261 | }, [isDragging]) 262 | 263 | const handleDragStart = e => { 264 | if (e.button !== 0) return // Only allow left click for drag 265 | setIsDragging(true) 266 | } 267 | 268 | const handleDragEnd = e => { 269 | setIsDragging(false) 270 | } 271 | 272 | const [unsortedQueries, setUnsortedQueries] = useSafeState( 273 | Object.values(queryCache.queries) 274 | ) 275 | 276 | const [activeQueryHash, setActiveQueryHash] = useLocalStorage( 277 | 'reactQueryDevtoolsActiveQueryHash', 278 | '' 279 | ) 280 | 281 | const queries = React.useMemo(() => { 282 | const sorted = [...unsortedQueries].sort(sortFn) 283 | 284 | if (sortDesc) { 285 | sorted.reverse() 286 | } 287 | 288 | return match(sorted, filter, { keys: ['queryHash'] }).filter( 289 | d => d.queryHash 290 | ) 291 | }, [sortDesc, sortFn, unsortedQueries, filter]) 292 | 293 | const activeQuery = React.useMemo(() => { 294 | return queries.find(query => query.queryHash === activeQueryHash) 295 | }, [activeQueryHash, queries]) 296 | 297 | const hasFresh = queries.filter(q => getQueryStatusLabel(q) === 'fresh') 298 | .length 299 | const hasFetching = queries.filter( 300 | q => getQueryStatusLabel(q) === 'fetching' 301 | ).length 302 | const hasStale = queries.filter(q => getQueryStatusLabel(q) === 'stale') 303 | .length 304 | const hasInactive = queries.filter( 305 | q => getQueryStatusLabel(q) === 'inactive' 306 | ).length 307 | 308 | React.useEffect(() => { 309 | return queryCache.subscribe(queryCache => { 310 | setUnsortedQueries(Object.values(queryCache.queries)) 311 | }) 312 | }, [sort, sortFn, sortDesc, queryCache, setUnsortedQueries]) 313 | 314 | return ( 315 | 316 | 317 |
331 |
342 |
351 | 352 |
357 | Queries ({queries.length}) 358 |
359 |
360 |
366 | 367 | 373 | fresh ({hasFresh}) 374 | {' '} 375 | 381 | fetching ({hasFetching}) 382 | {' '} 383 | 391 | stale ({hasStale}) 392 | {' '} 393 | 399 | inactive ({hasInactive}) 400 | 401 | 402 |
408 | setFilter(e.target.value)} 412 | onKeyDown={e => { 413 | if (e.key === 'Escape') setFilter('') 414 | }} 415 | style={{ 416 | flex: '1', 417 | marginRight: '.5rem', 418 | }} 419 | /> 420 | 435 | 443 |
444 |
445 |
446 |
451 | {queries.map((query, i) => ( 452 |
455 | setActiveQueryHash( 456 | activeQueryHash === query.queryHash ? '' : query.queryHash 457 | ) 458 | } 459 | style={{ 460 | display: 'flex', 461 | borderBottom: `solid 1px ${theme.grayAlt}`, 462 | cursor: 'pointer', 463 | background: 464 | query === activeQuery 465 | ? 'rgba(255,255,255,.1)' 466 | : undefined, 467 | }} 468 | > 469 |
490 | {query.observers.length} 491 |
492 | 497 | {`${query.queryHash}`} 498 | 499 |
500 | ))} 501 |
502 |
503 | {activeQuery ? ( 504 | 505 |
514 | Query Details 515 |
516 |
521 |
529 | 534 |
540 |                       {JSON.stringify(activeQuery.queryKey, null, 2)}
541 |                     
542 |
543 | 554 | {getQueryStatusLabel(activeQuery)} 555 | 556 |
557 |
565 | Last Updated:{' '} 566 | 567 | {new Date(activeQuery.state.updatedAt).toLocaleTimeString()} 568 | 569 |
570 |
571 |
580 | Actions 581 |
582 |
587 | {' '} 596 | {' '} 606 |
607 |
616 | Data Explorer 617 |
618 |
623 | 628 |
629 |
638 | Query Explorer 639 |
640 |
645 | 652 |
653 |
654 | ) : null} 655 |
656 |
657 | ) 658 | } 659 | ) 660 | -------------------------------------------------------------------------------- /src/styledComponents.js: -------------------------------------------------------------------------------- 1 | import { styled } from './utils' 2 | 3 | export const Panel = styled( 4 | 'div', 5 | (props, theme) => ({ 6 | fontSize: 'clamp(12px, 1.5vw, 14px)', 7 | fontFamily: `sans-serif`, 8 | display: 'flex', 9 | backgroundColor: theme.background, 10 | color: theme.foreground, 11 | }), 12 | { 13 | '(max-width: 700px)': { 14 | flexDirection: 'column', 15 | }, 16 | '(max-width: 600px)': { 17 | fontSize: '.9rem', 18 | // flexDirection: 'column', 19 | }, 20 | } 21 | ) 22 | 23 | export const ActiveQueryPanel = styled( 24 | 'div', 25 | (props, theme) => ({ 26 | flex: '1 1 500px', 27 | display: 'flex', 28 | flexDirection: 'column', 29 | overflow: 'auto', 30 | height: '100%', 31 | }), 32 | { 33 | '(max-width: 700px)': (props, theme) => ({ 34 | borderTop: `2px solid ${theme.gray}`, 35 | }), 36 | } 37 | ) 38 | 39 | export const Button = styled('button', (props, theme) => ({ 40 | appearance: 'none', 41 | fontSize: '.9em', 42 | fontWeight: 'bold', 43 | background: theme.gray, 44 | border: '0', 45 | borderRadius: '.3em', 46 | color: 'white', 47 | padding: '.5em', 48 | opacity: props.disabled ? '.5' : undefined, 49 | cursor: 'pointer', 50 | })) 51 | 52 | export const QueryKeys = styled('span', { 53 | display: 'inline-block', 54 | fontSize: '0.9em', 55 | }) 56 | 57 | export const QueryKey = styled('span', { 58 | display: 'inline-flex', 59 | alignItems: 'center', 60 | padding: '.2em .4em', 61 | fontWeight: 'bold', 62 | textShadow: '0 0 10px black', 63 | borderRadius: '.2em', 64 | }) 65 | 66 | export const Code = styled('code', { 67 | fontSize: '.9em', 68 | }) 69 | 70 | export const Input = styled('input', (props, theme) => ({ 71 | backgroundColor: theme.inputBackgroundColor, 72 | border: 0, 73 | borderRadius: '.2em', 74 | color: theme.inputTextColor, 75 | fontSize: '.9em', 76 | lineHeight: `1.3`, 77 | padding: '.3em .4em', 78 | })) 79 | 80 | export const Select = styled( 81 | 'select', 82 | (props, theme) => ({ 83 | display: `inline-block`, 84 | fontSize: `.9em`, 85 | fontFamily: `sans-serif`, 86 | fontWeight: 'normal', 87 | lineHeight: `1.3`, 88 | padding: `.3em 1.5em .3em .5em`, 89 | height: 'auto', 90 | border: 0, 91 | borderRadius: `.2em`, 92 | appearance: `none`, 93 | WebkitAppearance: 'none', 94 | backgroundColor: theme.inputBackgroundColor, 95 | backgroundImage: `url("data:image/svg+xml;utf8,")`, 96 | backgroundRepeat: `no-repeat`, 97 | backgroundPosition: `right .55em center`, 98 | backgroundSize: `.65em auto, 100%`, 99 | color: theme.inputTextColor, 100 | }), 101 | { 102 | '(max-width: 500px)': { 103 | display: 'none', 104 | }, 105 | } 106 | ) 107 | 108 | export const QueryCountStyles = styled( 109 | 'div', 110 | { 111 | fontSize: '1.2rem', 112 | display: 'flex', 113 | flexDirection: 'column', 114 | }, 115 | { 116 | '(max-width: 500px)': { 117 | display: 'none', 118 | }, 119 | } 120 | ) 121 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ThemeContext = React.createContext() 4 | 5 | export function ThemeProvider({ theme, ...rest }) { 6 | return 7 | } 8 | 9 | export function useTheme() { 10 | return React.useContext(ThemeContext) 11 | } 12 | -------------------------------------------------------------------------------- /src/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const getItem = key => { 4 | try { 5 | return JSON.parse(localStorage.getItem(key)) 6 | } catch { 7 | return undefined 8 | } 9 | } 10 | 11 | export default function useLocalStorage(key, defaultValue) { 12 | const [value, setValue] = React.useState(() => { 13 | const val = getItem(key) 14 | if (typeof val === 'undefined' || val === null) { 15 | return typeof defaultValue === 'function' ? defaultValue() : defaultValue 16 | } 17 | return val 18 | }) 19 | 20 | const setter = React.useCallback( 21 | updater => { 22 | setValue(old => { 23 | let newVal = updater 24 | 25 | if (typeof updater == 'function') { 26 | newVal = updater(old) 27 | } 28 | try { 29 | localStorage.setItem(key, JSON.stringify(newVal)) 30 | } catch {} 31 | 32 | return newVal 33 | }) 34 | }, 35 | [key] 36 | ) 37 | 38 | return [value, setter] 39 | } 40 | -------------------------------------------------------------------------------- /src/useMediaQuery.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function useMediaQuery(query) { 4 | // Keep track of the preference in state, start with the current match 5 | const [isMatch, setIsMatch] = React.useState( 6 | () => window.matchMedia && window.matchMedia(query).matches 7 | ) 8 | 9 | // Watch for changes 10 | React.useEffect(() => { 11 | if (!window.matchMedia) { 12 | return 13 | } 14 | 15 | // Create a matcher 16 | const matcher = window.matchMedia(query) 17 | 18 | // Create our handler 19 | const onChange = ({ matches }) => setIsMatch(matches) 20 | 21 | // Listen for changes 22 | matcher.addListener(onChange) 23 | 24 | return () => { 25 | // Stop listening for changes 26 | matcher.removeListener(onChange) 27 | } 28 | }, [isMatch, query, setIsMatch]) 29 | 30 | return isMatch 31 | } 32 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { useTheme } from './theme' 4 | import useMediaQuery from './useMediaQuery' 5 | 6 | export const isServer = typeof window === 'undefined' 7 | 8 | export function isStale(query) { 9 | return typeof query.isStale === 'function' 10 | ? query.isStale() 11 | : query.state.isStale 12 | } 13 | 14 | export function getQueryStatusColor(query, theme) { 15 | return query.state.isFetching 16 | ? theme.active 17 | : isStale(query) 18 | ? theme.warning 19 | : theme.success 20 | } 21 | 22 | export function getQueryOpacity(query) { 23 | return !query.observers.length ? 0.3 : 1 24 | } 25 | 26 | export function getQueryStatusLabel(query) { 27 | return query.state.isFetching 28 | ? 'fetching' 29 | : !query.observers.length 30 | ? 'inactive' 31 | : isStale(query) 32 | ? 'stale' 33 | : 'fresh' 34 | } 35 | 36 | export function styled(type, newStyles, queries = {}) { 37 | return React.forwardRef(({ style, ...rest }, ref) => { 38 | const theme = useTheme() 39 | 40 | const mediaStyles = Object.entries(queries).reduce( 41 | (current, [key, value]) => { 42 | return useMediaQuery(key) 43 | ? { 44 | ...current, 45 | ...(typeof value === 'function' ? value(rest, theme) : value), 46 | } 47 | : current 48 | }, 49 | {} 50 | ) 51 | 52 | return React.createElement(type, { 53 | ...rest, 54 | style: { 55 | ...(typeof newStyles === 'function' 56 | ? newStyles(rest, theme) 57 | : newStyles), 58 | ...style, 59 | ...mediaStyles, 60 | }, 61 | ref, 62 | }) 63 | }) 64 | } 65 | 66 | function useIsMounted() { 67 | const mountedRef = React.useRef(false) 68 | const isMounted = React.useCallback(() => mountedRef.current, []) 69 | 70 | React[isServer ? 'useEffect' : 'useLayoutEffect'](() => { 71 | mountedRef.current = true 72 | return () => { 73 | mountedRef.current = false 74 | } 75 | }, []) 76 | 77 | return isMounted 78 | } 79 | 80 | /** 81 | * This hook is a safe useState version which schedules state updates in microtasks 82 | * to prevent updating a component state while React is rendering different components 83 | * or when the component is not mounted anymore. 84 | */ 85 | export function useSafeState(initialState) { 86 | const isMounted = useIsMounted() 87 | const [state, setState] = React.useState(initialState) 88 | 89 | const safeSetState = React.useCallback( 90 | value => { 91 | scheduleMicrotask(() => { 92 | if (isMounted()) { 93 | setState(value) 94 | } 95 | }) 96 | }, 97 | [isMounted] 98 | ) 99 | 100 | return [state, safeSetState] 101 | } 102 | 103 | /** 104 | * Schedules a microtask. 105 | * This can be useful to schedule state updates after rendering. 106 | */ 107 | function scheduleMicrotask(callback) { 108 | Promise.resolve() 109 | .then(callback) 110 | .catch(error => 111 | setTimeout(() => { 112 | throw error 113 | }) 114 | ) 115 | } 116 | --------------------------------------------------------------------------------