diff --git a/docs/framework/react/devtools.md b/docs/framework/react/devtools.md index e9331622a3..31eb9e59a0 100644 --- a/docs/framework/react/devtools.md +++ b/docs/framework/react/devtools.md @@ -91,6 +91,45 @@ function App() { - Default behavior will apply the devtool's styles to the head tag within the DOM. - Use this to pass a shadow DOM target to the devtools so that the styles will be applied within the shadow DOM instead of within the head tag in the light DOM. +## Panel Mode + +Panel mode will show the development tools as a fixed element in your application, so you can use our panel in your own development tools. + +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! + +```tsx +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + +function App() { + const [isOpen, setIsOpen] = React.useState(false) + + return ( + + {/* The rest of your application */} + + + + ) +} +``` + +### Options + +- `isOpen: Boolean` + - Defaults to `false` +- `position?: "top" | "bottom" | "left" | "right"` + - Defaults to `bottom` + - The position of the React Query devtools panel +- `client?: QueryClient`, + - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. +- `errorTypes?: { name: string; initializer: (query: Query) => TError}` + - Use this to predefine some errors that can be triggered on your queries. Initializer will be called (with the specific query) when that error is toggled on from the UI. It must return an Error. +- `styleNonce?: string` + - Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. +- `shadowDOMTarget?: ShadowRoot` + - Default behavior will apply the devtool's styles to the head tag within the DOM. + - Use this to pass a shadow DOM target to the devtools so that the styles will be applied within the shadow DOM instead of within the head tag in the light DOM. + ## Devtools in production Devtools are excluded in production builds. However, it might be desirable to lazy load the devtools in production: diff --git a/examples/react/devtools-panel/.eslintrc b/examples/react/devtools-panel/.eslintrc new file mode 100644 index 0000000000..4e03b9e10b --- /dev/null +++ b/examples/react/devtools-panel/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["plugin:react/jsx-runtime", "plugin:react-hooks/recommended"] +} diff --git a/examples/react/devtools-panel/.gitignore b/examples/react/devtools-panel/.gitignore new file mode 100644 index 0000000000..4673b022e5 --- /dev/null +++ b/examples/react/devtools-panel/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +pnpm-lock.yaml +yarn.lock +package-lock.json + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/react/devtools-panel/README.md b/examples/react/devtools-panel/README.md new file mode 100644 index 0000000000..1cf8892652 --- /dev/null +++ b/examples/react/devtools-panel/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/react/devtools-panel/index.html b/examples/react/devtools-panel/index.html new file mode 100644 index 0000000000..204cab6a43 --- /dev/null +++ b/examples/react/devtools-panel/index.html @@ -0,0 +1,16 @@ + + + + + + + + + TanStack Query React Devtools Panel Example App + + + +
+ + + diff --git a/examples/react/devtools-panel/package.json b/examples/react/devtools-panel/package.json new file mode 100644 index 0000000000..edbc385dca --- /dev/null +++ b/examples/react/devtools-panel/package.json @@ -0,0 +1,21 @@ +{ + "name": "@tanstack/query-example-react-devtools-panel", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/react-query": "^5.52.0", + "@tanstack/react-query-devtools": "^5.52.0", + "react": "19.0.0-rc-4c2e457c7c-20240522", + "react-dom": "19.0.0-rc-4c2e457c7c-20240522" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "typescript": "5.3.3", + "vite": "^5.3.5" + } +} diff --git a/examples/react/devtools-panel/public/emblem-light.svg b/examples/react/devtools-panel/public/emblem-light.svg new file mode 100644 index 0000000000..a58e69ad5e --- /dev/null +++ b/examples/react/devtools-panel/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/react/devtools-panel/src/index.tsx b/examples/react/devtools-panel/src/index.tsx new file mode 100644 index 0000000000..6b564baad5 --- /dev/null +++ b/examples/react/devtools-panel/src/index.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { + QueryClient, + QueryClientProvider, + useQuery, +} from '@tanstack/react-query' +import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' + +const queryClient = new QueryClient() + +export default function App() { + const [isOpen, setIsOpen] = React.useState(false) + + return ( + + + + + + ) +} + +function Example() { + const { isPending, error, data, isFetching } = useQuery({ + queryKey: ['repoData'], + queryFn: async () => { + const response = await fetch( + 'https://api.github.com/repos/TanStack/query', + ) + return await response.json() + }, + }) + + if (isPending) return 'Loading...' + + if (error) return 'An error has occurred: ' + error.message + + return ( +
+

{data.full_name}

+

{data.description}

+ 👀 {data.subscribers_count}{' '} + ✨ {data.stargazers_count}{' '} + 🍴 {data.forks_count} +
{isFetching ? 'Updating...' : ''}
+
+ ) +} + +const rootElement = document.getElementById('root') as HTMLElement +ReactDOM.createRoot(rootElement).render() diff --git a/examples/react/devtools-panel/tsconfig.json b/examples/react/devtools-panel/tsconfig.json new file mode 100644 index 0000000000..23a8707ef4 --- /dev/null +++ b/examples/react/devtools-panel/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "eslint.config.js"] +} diff --git a/examples/react/devtools-panel/vite.config.ts b/examples/react/devtools-panel/vite.config.ts new file mode 100644 index 0000000000..9ffcc67574 --- /dev/null +++ b/examples/react/devtools-panel/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], +}) diff --git a/packages/query-devtools/src/Devtools.tsx b/packages/query-devtools/src/Devtools.tsx index d88f911419..a6f1ff7d38 100644 --- a/packages/query-devtools/src/Devtools.tsx +++ b/packages/query-devtools/src/Devtools.tsx @@ -77,6 +77,7 @@ interface DevtoolsPanelProps { localStore: StorageObject setLocalStore: StorageSetter withCloseButton?: boolean + withOpenButton?: boolean withDragPositionPanel?: boolean withPIPButton?: boolean withPositionButton?: boolean @@ -114,6 +115,7 @@ const [offline, setOffline] = createSignal(false) export const Devtools: Component = (_props) => { const props = mergeProps({ withCloseButton: true, + withOpenButton: true, withDragPositionPanel: true, withPIPButton: true, withPositionButton: true @@ -133,6 +135,11 @@ export const Devtools: Component = (_props) => { }) const isOpen = createMemo(() => { + const isDevtoolsPanelOpen = useQueryDevtoolsContext().isOpen + if (isDevtoolsPanelOpen !== undefined) { + return isDevtoolsPanelOpen || INITIAL_IS_OPEN + } + return props.localStore.open === 'true' ? true : props.localStore.open === 'false' @@ -242,7 +249,7 @@ export const Devtools: Component = (_props) => { - +
= (props) => { ref={panelRef} aria-label="Tanstack query devtools" > - {props.withDragPositionPanel && ( +
- )} - {props.withCloseButton && ( +
+ - )} + ) @@ -543,7 +550,6 @@ const DevtoolsPanel: Component = (props) => { const ContentView: Component = (props) => { setupQueryCacheSubscription() setupMutationCacheSubscription() - console.log('ContentView ', props); let containerRef!: HTMLDivElement const theme = useTheme() const css = useQueryDevtoolsContext().shadowDOMTarget @@ -910,27 +916,25 @@ const ContentView: Component = (props) => { > {offline() ? : } - {props.withPIPButton && ( - - - - )} + + + = (props) => { > Settings
- {props.withPositionButton && ( + = (props) => { - )} + > & { +export type DevtoolsComponentType = Component> & { shadowDOMTarget?: ShadowRoot } diff --git a/packages/query-devtools/src/DevtoolsPanelComponent.tsx b/packages/query-devtools/src/DevtoolsPanelComponent.tsx index cf8c5894f7..f704c165c5 100644 --- a/packages/query-devtools/src/DevtoolsPanelComponent.tsx +++ b/packages/query-devtools/src/DevtoolsPanelComponent.tsx @@ -10,7 +10,7 @@ import { import { Devtools, THEME_PREFERENCE } from './Devtools' import type { Component } from 'solid-js' -export type DevtoolsPanelComponentType = Component & { +export type DevtoolsPanelComponentType = Component> & { shadowDOMTarget?: ShadowRoot } @@ -38,6 +38,7 @@ const DevtoolsPanelComponent: DevtoolsPanelComponentType = (props) => { localStore={localStore} setLocalStore={setLocalStore} withCloseButton={false} + withOpenButton={false} withDragPositionPanel={false} withPIPButton={false} withPositionButton={false} diff --git a/packages/query-devtools/src/TanstackQueryDevtoolsPanel.tsx b/packages/query-devtools/src/TanstackQueryDevtoolsPanel.tsx index d4eaad38b6..fbbb29b0c4 100644 --- a/packages/query-devtools/src/TanstackQueryDevtoolsPanel.tsx +++ b/packages/query-devtools/src/TanstackQueryDevtoolsPanel.tsx @@ -5,7 +5,7 @@ import type { QueryClient, onlineManager as TOnlineManager, } from '@tanstack/query-core' -import type { DevtoolsComponentType } from './DevtoolsComponent' +import type { DevtoolsPanelComponentType } from './DevtoolsPanelComponent' import type { DevToolsErrorType, DevtoolsPosition, @@ -27,9 +27,9 @@ class TanstackQueryDevtoolsPanel { #styleNonce?: string #shadowDOMTarget?: ShadowRoot #position: Signal - #initialIsOpen: Signal + #isOpen: Signal #errorTypes: Signal | undefined> - #Component: DevtoolsComponentType | undefined + #Component: DevtoolsPanelComponentType | undefined #dispose?: () => void constructor(config: TanstackQueryDevtoolsPanelConfig) { @@ -39,7 +39,7 @@ class TanstackQueryDevtoolsPanel { version, onlineManager, position, - initialIsOpen, + isOpen, errorTypes, styleNonce, shadowDOMTarget, @@ -51,7 +51,7 @@ class TanstackQueryDevtoolsPanel { this.#styleNonce = styleNonce this.#shadowDOMTarget = shadowDOMTarget this.#position = createSignal(position) - this.#initialIsOpen = createSignal(initialIsOpen) + this.#isOpen = createSignal(isOpen) this.#errorTypes = createSignal(errorTypes) } @@ -59,8 +59,8 @@ class TanstackQueryDevtoolsPanel { this.#position[1](position) } - setInitialIsOpen(isOpen: boolean) { - this.#initialIsOpen[1](isOpen) + setIsOpen(isOpen: boolean) { + this.#isOpen[1](isOpen) } setErrorTypes(errorTypes: Array) { @@ -77,10 +77,10 @@ class TanstackQueryDevtoolsPanel { } const dispose = render(() => { const [pos] = this.#position - const [isOpen] = this.#initialIsOpen + const [isOpen] = this.#isOpen const [errors] = this.#errorTypes const [queryClient] = this.#client - let Devtools: DevtoolsComponentType + let Devtools: DevtoolsPanelComponentType if (this.#Component) { Devtools = this.#Component @@ -103,7 +103,7 @@ class TanstackQueryDevtoolsPanel { get position() { return pos() }, - get initialIsOpen() { + get isOpen() { return isOpen() }, get errorTypes() { diff --git a/packages/query-devtools/src/contexts/QueryDevtoolsContext.ts b/packages/query-devtools/src/contexts/QueryDevtoolsContext.ts index d699740395..d2944931dd 100644 --- a/packages/query-devtools/src/contexts/QueryDevtoolsContext.ts +++ b/packages/query-devtools/src/contexts/QueryDevtoolsContext.ts @@ -26,6 +26,7 @@ export interface QueryDevtoolsProps { buttonPosition?: DevtoolsButtonPosition position?: DevtoolsPosition initialIsOpen?: boolean + isOpen?: boolean errorTypes?: Array shadowDOMTarget?: ShadowRoot } diff --git a/packages/react-query-devtools/src/ReactQueryDevtoolsPanel.tsx b/packages/react-query-devtools/src/ReactQueryDevtoolsPanel.tsx index e69602dd1a..73599a8361 100644 --- a/packages/react-query-devtools/src/ReactQueryDevtoolsPanel.tsx +++ b/packages/react-query-devtools/src/ReactQueryDevtoolsPanel.tsx @@ -9,7 +9,7 @@ export interface DevtoolsPanelOptions { /** * Set this true if you want the dev tools to default to being open */ - initialIsOpen?: boolean + isOpen?: boolean /** * The position of the React Query devtools panel. * 'top' | 'bottom' | 'left' | 'right' @@ -41,7 +41,7 @@ export function ReactQueryDevtoolsPanel( const ref = React.useRef(null) const { position, - initialIsOpen, + isOpen, errorTypes, styleNonce, shadowDOMTarget, @@ -53,7 +53,7 @@ export function ReactQueryDevtoolsPanel( version: '5', onlineManager, position, - initialIsOpen, + isOpen, errorTypes, styleNonce, shadowDOMTarget, @@ -71,8 +71,8 @@ export function ReactQueryDevtoolsPanel( }, [position, devtools]) React.useEffect(() => { - devtools.setInitialIsOpen(initialIsOpen || false) - }, [initialIsOpen, devtools]) + devtools.setIsOpen(isOpen || false) + }, [isOpen, devtools]) React.useEffect(() => { devtools.setErrorTypes(errorTypes || []) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 850d145dde..673f377620 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -487,6 +487,31 @@ importers: specifier: ^5.3.5 version: 5.3.5(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3) + examples/react/devtools-panel: + dependencies: + '@tanstack/react-query': + specifier: ^5.52.0 + version: link:../../../packages/react-query + '@tanstack/react-query-devtools': + specifier: ^5.52.0 + version: link:../../../packages/react-query-devtools + react: + specifier: 19.0.0-rc-4c2e457c7c-20240522 + version: 19.0.0-rc-4c2e457c7c-20240522 + react-dom: + specifier: 19.0.0-rc-4c2e457c7c-20240522 + version: 19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522) + devDependencies: + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.3.1(vite@5.3.5(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)) + typescript: + specifier: 5.3.3 + version: 5.3.3 + vite: + specifier: ^5.3.5 + version: 5.3.5(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3) + examples/react/infinite-query-with-max-pages: dependencies: '@tanstack/react-query': @@ -1124,7 +1149,7 @@ importers: version: 5.1.0(astro@4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3))(tailwindcss@3.4.7) '@astrojs/vercel': specifier: ^7.7.2 - version: 7.7.2(astro@4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3))(encoding@0.1.13)(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8))(react@18.3.1) + version: 7.7.2(astro@4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3))(encoding@0.1.13)(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) '@tanstack/solid-query': specifier: ^5.52.2 version: link:../../../packages/solid-query @@ -17670,10 +17695,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/vercel@7.7.2(astro@4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3))(encoding@0.1.13)(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': + '@astrojs/vercel@7.7.2(astro@4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3))(encoding@0.1.13)(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': dependencies: '@astrojs/internal-helpers': 0.4.1 - '@vercel/analytics': 1.3.1(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8))(react@18.3.1) + '@vercel/analytics': 1.3.1(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) '@vercel/edge': 1.1.2 '@vercel/nft': 0.27.3(encoding@0.1.13) astro: 4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3) @@ -23016,11 +23041,11 @@ snapshots: graphql: 15.8.0 wonka: 4.0.15 - '@vercel/analytics@1.3.1(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': + '@vercel/analytics@1.3.1(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': dependencies: server-only: 0.0.1 optionalDependencies: - next: 14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8) + next: 14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8) react: 18.3.1 '@vercel/edge@1.1.2': {} @@ -30138,7 +30163,7 @@ snapshots: next-tick@1.1.0: {} - next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8): + next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8): dependencies: '@next/env': 14.2.5 '@swc/helpers': 0.5.5 @@ -30147,7 +30172,7 @@ snapshots: graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.3.1 - react-dom: 19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522) + react-dom: 19.0.0-rc-4c2e457c7c-20240522(react@18.3.1) styled-jsx: 5.1.1(@babel/core@7.25.2)(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 14.2.5 @@ -32445,6 +32470,12 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1): + dependencies: + react: 18.3.1 + scheduler: 0.25.0-rc-4c2e457c7c-20240522 + optional: true + react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522): dependencies: react: 19.0.0-rc-4c2e457c7c-20240522