@tanstack/react-query-devtools 5.0.0-alpha.1 → 5.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/build/lib/Explorer.esm.js +2 -1
  2. package/build/lib/Explorer.esm.js.map +1 -1
  3. package/build/lib/Explorer.js +2 -1
  4. package/build/lib/Explorer.js.map +1 -1
  5. package/build/lib/Explorer.mjs +1 -0
  6. package/build/lib/Explorer.mjs.map +1 -1
  7. package/build/lib/devtools.d.ts +20 -2
  8. package/build/lib/devtools.esm.js +161 -34
  9. package/build/lib/devtools.esm.js.map +1 -1
  10. package/build/lib/devtools.js +160 -34
  11. package/build/lib/devtools.js.map +1 -1
  12. package/build/lib/devtools.mjs +105 -9
  13. package/build/lib/devtools.mjs.map +1 -1
  14. package/build/lib/index.esm.js +1 -0
  15. package/build/lib/index.esm.js.map +1 -1
  16. package/build/lib/index.js +1 -0
  17. package/build/lib/index.js.map +1 -1
  18. package/build/lib/index.mjs +1 -0
  19. package/build/lib/index.mjs.map +1 -1
  20. package/build/lib/index.prod.esm.js +164 -36
  21. package/build/lib/index.prod.esm.js.map +1 -1
  22. package/build/lib/index.prod.js +163 -36
  23. package/build/lib/index.prod.js.map +1 -1
  24. package/build/lib/index.prod.mjs +107 -10
  25. package/build/lib/index.prod.mjs.map +1 -1
  26. package/build/lib/styledComponents.d.ts +0 -1
  27. package/build/lib/styledComponents.esm.js +3 -1
  28. package/build/lib/styledComponents.esm.js.map +1 -1
  29. package/build/lib/styledComponents.js +3 -1
  30. package/build/lib/styledComponents.js.map +1 -1
  31. package/build/lib/styledComponents.mjs +3 -1
  32. package/build/lib/styledComponents.mjs.map +1 -1
  33. package/build/lib/theme.esm.js +1 -0
  34. package/build/lib/theme.esm.js.map +1 -1
  35. package/build/lib/theme.js +1 -0
  36. package/build/lib/theme.js.map +1 -1
  37. package/build/lib/theme.mjs +1 -0
  38. package/build/lib/theme.mjs.map +1 -1
  39. package/build/umd/index.development.js +106 -10
  40. package/build/umd/index.development.js.map +1 -1
  41. package/build/umd/index.production.js.map +1 -1
  42. package/package.json +4 -4
  43. package/src/__tests__/Explorer.test.tsx +3 -2
  44. package/src/__tests__/devtools.test.tsx +157 -8
  45. package/src/devtools.tsx +150 -3
  46. package/src/index.ts +2 -0
  47. package/src/styledComponents.ts +3 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.production.js","sources":["../../src/index.ts"],"sourcesContent":["import * as devtools from './devtools'\n\nexport const ReactQueryDevtools: (typeof devtools)['ReactQueryDevtools'] =\n process.env.NODE_ENV !== 'development'\n ? function () {\n return null\n }\n : devtools.ReactQueryDevtools\n\nexport const ReactQueryDevtoolsPanel: (typeof devtools)['ReactQueryDevtoolsPanel'] =\n process.env.NODE_ENV !== 'development'\n ? (function () {\n return null\n } as any)\n : devtools.ReactQueryDevtoolsPanel\n"],"names":[],"mappings":"+QAIM,WACE,OAAO,gCAMR,WACC,OAAO"}
1
+ {"version":3,"file":"index.production.js","sources":["../../src/index.ts"],"sourcesContent":["'use client'\n\nimport * as devtools from './devtools'\n\nexport const ReactQueryDevtools: (typeof devtools)['ReactQueryDevtools'] =\n process.env.NODE_ENV !== 'development'\n ? function () {\n return null\n }\n : devtools.ReactQueryDevtools\n\nexport const ReactQueryDevtoolsPanel: (typeof devtools)['ReactQueryDevtoolsPanel'] =\n process.env.NODE_ENV !== 'development'\n ? (function () {\n return null\n } as any)\n : devtools.ReactQueryDevtoolsPanel\n"],"names":[],"mappings":"+QAMM,WACE,OAAO,IACT,4BAKC,WACC,OAAO,IACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-query-devtools",
3
- "version": "5.0.0-alpha.1",
3
+ "version": "5.0.0-alpha.10",
4
4
  "description": "Developer tools to interact with and visualize the TanStack/react-query cache",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "react": "^18.2.0",
44
44
  "react-dom": "^18.2.0",
45
45
  "react-error-boundary": "^3.1.4",
46
- "@tanstack/react-query": "5.0.0-alpha.1"
46
+ "@tanstack/react-query": "5.0.0-alpha.10"
47
47
  },
48
48
  "dependencies": {
49
49
  "@tanstack/match-sorter-utils": "^8.7.0",
@@ -52,13 +52,13 @@
52
52
  "peerDependencies": {
53
53
  "react": "^18.0.0",
54
54
  "react-dom": "^18.0.0",
55
- "@tanstack/react-query": "5.0.0-alpha.1"
55
+ "@tanstack/react-query": "5.0.0-alpha.10"
56
56
  },
57
57
  "scripts": {
58
58
  "clean": "rimraf ./build",
59
59
  "test:eslint": "eslint --ext .ts,.tsx ./src",
60
60
  "test:types": "tsc",
61
- "test:lib": "jest --config ./jest.config.ts",
61
+ "test:lib": "vitest run --coverage",
62
62
  "test:lib:dev": "pnpm run test:lib --watch",
63
63
  "build:types": "tsc --build && cp build/lib/index.d.ts build/lib/index.prod.d.ts"
64
64
  }
@@ -3,6 +3,7 @@ import * as React from 'react'
3
3
 
4
4
  import { chunkArray, CopyButton, DefaultRenderer } from '../Explorer'
5
5
  import { displayValue } from '../utils'
6
+ import { vi } from 'vitest'
6
7
 
7
8
  describe('Explorer', () => {
8
9
  describe('chunkArray', () => {
@@ -30,7 +31,7 @@ describe('Explorer', () => {
30
31
 
31
32
  describe('DefaultRenderer', () => {
32
33
  it('when the entry label is clicked, toggle expanded', async () => {
33
- const toggleExpanded = jest.fn()
34
+ const toggleExpanded = vi.fn()
34
35
 
35
36
  render(
36
37
  <DefaultRenderer
@@ -90,7 +91,7 @@ describe('Explorer', () => {
90
91
  })
91
92
 
92
93
  it('when the copy button is clicked but there is an error, show error state', async () => {
93
- const consoleMock = jest.spyOn(console, 'error')
94
+ const consoleMock = vi.spyOn(console, 'error')
94
95
  consoleMock.mockImplementation(() => undefined)
95
96
 
96
97
  // Mock clipboard with error state
@@ -9,18 +9,27 @@ import {
9
9
  sleep,
10
10
  createQueryClient,
11
11
  } from './utils'
12
+ import { vi } from 'vitest'
13
+ import UserEvent from '@testing-library/user-event'
14
+
15
+ class CustomError extends Error {
16
+ constructor(message: string) {
17
+ super(message)
18
+ this.name = 'CustomError'
19
+ }
20
+ }
12
21
 
13
22
  Object.defineProperty(window, 'matchMedia', {
14
23
  writable: true,
15
- value: jest.fn().mockImplementation((query: string) => ({
24
+ value: vi.fn().mockImplementation((query: string) => ({
16
25
  matches: false,
17
26
  media: query,
18
27
  onchange: null,
19
- addListener: jest.fn(), // deprecated
20
- removeListener: jest.fn(), // deprecated
21
- addEventListener: jest.fn(),
22
- removeEventListener: jest.fn(),
23
- dispatchEvent: jest.fn(),
28
+ addListener: vi.fn(), // deprecated
29
+ removeListener: vi.fn(), // deprecated
30
+ addEventListener: vi.fn(),
31
+ removeEventListener: vi.fn(),
32
+ dispatchEvent: vi.fn(),
24
33
  })),
25
34
  })
26
35
 
@@ -31,8 +40,8 @@ describe('ReactQueryDevtools', () => {
31
40
  })
32
41
  it('should be able to open and close devtools', async () => {
33
42
  const { queryClient } = createQueryClient()
34
- const onCloseClick = jest.fn()
35
- const onToggleClick = jest.fn()
43
+ const onCloseClick = vi.fn()
44
+ const onToggleClick = vi.fn()
36
45
 
37
46
  function Page() {
38
47
  const { data = 'default' } = useQuery({
@@ -861,4 +870,144 @@ describe('ReactQueryDevtools', () => {
861
870
  fireEvent.click(screen.getByRole('button', { name: /^close$/i }))
862
871
  expect(parentElement).toHaveStyle(parentPaddings)
863
872
  })
873
+
874
+ it('should simulate loading state', async () => {
875
+ const { queryClient } = createQueryClient()
876
+ let count = 0
877
+ function App() {
878
+ const { data, fetchStatus } = useQuery({
879
+ queryKey: ['key'],
880
+ queryFn: () => {
881
+ count++
882
+ return Promise.resolve('test')
883
+ },
884
+ })
885
+
886
+ return (
887
+ <div>
888
+ <h1>
889
+ {data ?? 'No data'}, {fetchStatus}
890
+ </h1>
891
+ </div>
892
+ )
893
+ }
894
+
895
+ renderWithClient(queryClient, <App />, {
896
+ initialIsOpen: true,
897
+ })
898
+
899
+ await screen.findByRole('heading', { name: /test/i })
900
+
901
+ const loadingButton = await screen.findByRole('button', {
902
+ name: 'Trigger loading',
903
+ })
904
+ fireEvent.click(loadingButton)
905
+
906
+ await waitFor(() => {
907
+ expect(screen.getByText('Restore loading')).toBeInTheDocument()
908
+ })
909
+
910
+ await waitFor(() => {
911
+ expect(screen.getByText('No data, fetching')).toBeInTheDocument()
912
+ })
913
+
914
+ fireEvent.click(screen.getByRole('button', { name: /restore loading/i }))
915
+
916
+ await waitFor(() => {
917
+ expect(screen.getByText('test, idle')).toBeInTheDocument()
918
+ })
919
+
920
+ expect(count).toBe(2)
921
+ })
922
+
923
+ it('should simulate error state', async () => {
924
+ const { queryClient } = createQueryClient()
925
+ function App() {
926
+ const { status, error } = useQuery({
927
+ queryKey: ['key'],
928
+ queryFn: () => {
929
+ return Promise.resolve('test')
930
+ },
931
+ })
932
+
933
+ return (
934
+ <div>
935
+ <h1>
936
+ {!!error ? 'Some error' : 'No error'}, {status}
937
+ </h1>
938
+ </div>
939
+ )
940
+ }
941
+
942
+ renderWithClient(queryClient, <App />, {
943
+ initialIsOpen: true,
944
+ })
945
+
946
+ const errorButton = await screen.findByRole('button', {
947
+ name: 'Trigger error',
948
+ })
949
+ fireEvent.click(errorButton)
950
+
951
+ await waitFor(() => {
952
+ expect(screen.getByText('Restore error')).toBeInTheDocument()
953
+ })
954
+
955
+ await waitFor(() => {
956
+ expect(screen.getByText('Some error, error')).toBeInTheDocument()
957
+ })
958
+
959
+ fireEvent.click(screen.getByRole('button', { name: /Restore error/i }))
960
+
961
+ await waitFor(() => {
962
+ expect(screen.getByText('No error, success')).toBeInTheDocument()
963
+ })
964
+ })
965
+
966
+ it('should can simulate a specific error', async () => {
967
+ const { queryClient } = createQueryClient()
968
+
969
+ function App() {
970
+ const { status, error } = useQuery({
971
+ queryKey: ['key'],
972
+ queryFn: () => {
973
+ return Promise.resolve('test')
974
+ },
975
+ })
976
+
977
+ return (
978
+ <div data-testid="test">
979
+ <h1>
980
+ {error instanceof CustomError
981
+ ? error.message.toString()
982
+ : 'No error'}
983
+ , {status}
984
+ </h1>
985
+ </div>
986
+ )
987
+ }
988
+
989
+ renderWithClient(queryClient, <App />, {
990
+ initialIsOpen: true,
991
+ errorTypes: [
992
+ {
993
+ name: 'error1',
994
+ initializer: () => new CustomError('error1'),
995
+ },
996
+ ],
997
+ })
998
+
999
+ const errorOption = await screen.findByLabelText('Trigger error:')
1000
+
1001
+ UserEvent.selectOptions(errorOption, 'error1')
1002
+
1003
+ await waitFor(() => {
1004
+ expect(screen.getByText('error1, error')).toBeInTheDocument()
1005
+ })
1006
+
1007
+ fireEvent.click(screen.getByRole('button', { name: /Restore error/i }))
1008
+
1009
+ await waitFor(() => {
1010
+ expect(screen.getByText('No error, success')).toBeInTheDocument()
1011
+ })
1012
+ })
864
1013
  })
package/src/devtools.tsx CHANGED
@@ -4,6 +4,8 @@ import type {
4
4
  QueryCache,
5
5
  QueryClient,
6
6
  QueryKey as QueryKeyType,
7
+ Query,
8
+ DefaultError,
7
9
  } from '@tanstack/react-query'
8
10
  import {
9
11
  useQueryClient,
@@ -39,6 +41,18 @@ import { ThemeProvider, defaultTheme as theme } from './theme'
39
41
  import { getQueryStatusLabel, getQueryStatusColor } from './utils'
40
42
  import Explorer from './Explorer'
41
43
  import Logo from './Logo'
44
+ import { useMemo } from 'react'
45
+
46
+ export interface DevToolsErrorType {
47
+ /**
48
+ * The name of the error.
49
+ */
50
+ name: string
51
+ /**
52
+ * How the error is initialized.
53
+ */
54
+ initializer: (query: Query) => DefaultError
55
+ }
42
56
 
43
57
  export interface DevtoolsOptions {
44
58
  /**
@@ -81,6 +95,10 @@ export interface DevtoolsOptions {
81
95
  * Custom instance of QueryClient
82
96
  */
83
97
  queryClient?: QueryClient
98
+ /**
99
+ * Use this so you can define custom errors that can be shown in the devtools.
100
+ */
101
+ errorTypes?: DevToolsErrorType[]
84
102
  }
85
103
 
86
104
  interface DevtoolsPanelOptions {
@@ -129,6 +147,10 @@ interface DevtoolsPanelOptions {
129
147
  * Custom instance of QueryClient
130
148
  */
131
149
  queryClient?: QueryClient
150
+ /**
151
+ * Use this so you can define custom errors that can be shown in the devtools.
152
+ */
153
+ errorTypes?: DevToolsErrorType[]
132
154
  }
133
155
 
134
156
  export function ReactQueryDevtools({
@@ -141,6 +163,7 @@ export function ReactQueryDevtools({
141
163
  queryClient,
142
164
  styleNonce,
143
165
  panelPosition: initialPanelPosition = 'bottom',
166
+ errorTypes = [],
144
167
  }: DevtoolsOptions): React.ReactElement | null {
145
168
  const rootRef = React.useRef<HTMLDivElement>(null)
146
169
  const panelRef = React.useRef<HTMLDivElement>(null)
@@ -349,6 +372,7 @@ export function ReactQueryDevtools({
349
372
  isOpen={isResolvedOpen}
350
373
  setIsOpen={setIsOpen}
351
374
  onDragStart={(e) => handleDragStart(panelRef.current, e)}
375
+ errorTypes={errorTypes}
352
376
  />
353
377
  </ThemeProvider>
354
378
  {!isResolvedOpen ? (
@@ -439,6 +463,7 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
439
463
  showCloseButton,
440
464
  position,
441
465
  closeButtonProps = {},
466
+ errorTypes = [],
442
467
  ...panelProps
443
468
  } = props
444
469
 
@@ -611,6 +636,8 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
611
636
  style={{
612
637
  display: 'flex',
613
638
  alignItems: 'center',
639
+ flexWrap: 'wrap',
640
+ gap: '0.5em',
614
641
  }}
615
642
  >
616
643
  <Input
@@ -623,7 +650,6 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
623
650
  }}
624
651
  style={{
625
652
  flex: '1',
626
- marginRight: '.5em',
627
653
  width: '100%',
628
654
  }}
629
655
  />
@@ -744,6 +770,7 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
744
770
  activeQueryHash={activeQueryHash}
745
771
  queryCache={queryCache}
746
772
  queryClient={client}
773
+ errorTypes={errorTypes}
747
774
  />
748
775
  ) : null}
749
776
 
@@ -779,10 +806,12 @@ const ActiveQuery = ({
779
806
  queryCache,
780
807
  activeQueryHash,
781
808
  queryClient,
809
+ errorTypes,
782
810
  }: {
783
811
  queryCache: QueryCache
784
812
  activeQueryHash: string
785
813
  queryClient: QueryClient
814
+ errorTypes: DevToolsErrorType[]
786
815
  }) => {
787
816
  const activeQuery = useSubscribeToQueryCache(queryCache, () =>
788
817
  queryCache.getAll().find((query) => query.queryHash === activeQueryHash),
@@ -816,10 +845,50 @@ const ActiveQuery = ({
816
845
  promise?.catch(noop)
817
846
  }
818
847
 
848
+ const currentErrorTypeName = useMemo(() => {
849
+ if (activeQuery && activeQueryState?.error) {
850
+ const errorType = errorTypes.find(
851
+ (type) =>
852
+ type.initializer(activeQuery).toString() ===
853
+ activeQueryState.error?.toString(),
854
+ )
855
+ return errorType?.name
856
+ }
857
+ return undefined
858
+ }, [activeQuery, activeQueryState?.error, errorTypes])
859
+
819
860
  if (!activeQuery || !activeQueryState) {
820
861
  return null
821
862
  }
822
863
 
864
+ const triggerError = (errorType?: DevToolsErrorType) => {
865
+ const error =
866
+ errorType?.initializer(activeQuery) ??
867
+ new Error('Unknown error from devtools')
868
+
869
+ const __previousQueryOptions = activeQuery.options
870
+
871
+ activeQuery.setState({
872
+ status: 'error',
873
+ error,
874
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
875
+ fetchMeta: {
876
+ ...activeQuery.state.fetchMeta,
877
+ __previousQueryOptions,
878
+ } as any,
879
+ })
880
+ }
881
+
882
+ const restoreQueryAfterLoadingOrError = () => {
883
+ activeQuery.fetch(
884
+ (activeQuery.state.fetchMeta as any).__previousQueryOptions,
885
+ {
886
+ // Make sure this fetch will cancel the previous one
887
+ cancelRefetch: true,
888
+ },
889
+ )
890
+ }
891
+
823
892
  return (
824
893
  <ActiveQueryPanel>
825
894
  <div
@@ -842,7 +911,7 @@ const ActiveQuery = ({
842
911
  style={{
843
912
  marginBottom: '.5em',
844
913
  display: 'flex',
845
- alignItems: 'stretch',
914
+ alignItems: 'flex-start',
846
915
  justifyContent: 'space-between',
847
916
  }}
848
917
  >
@@ -916,6 +985,10 @@ const ActiveQuery = ({
916
985
  <div
917
986
  style={{
918
987
  padding: '0.5em',
988
+ display: 'flex',
989
+ flexWrap: 'wrap',
990
+ gap: '0.5em',
991
+ alignItems: 'flex-end',
919
992
  }}
920
993
  >
921
994
  <Button
@@ -955,7 +1028,81 @@ const ActiveQuery = ({
955
1028
  }}
956
1029
  >
957
1030
  Remove
958
- </Button>
1031
+ </Button>{' '}
1032
+ <Button
1033
+ type="button"
1034
+ onClick={() => {
1035
+ if (activeQuery.state.data === undefined) {
1036
+ restoreQueryAfterLoadingOrError()
1037
+ } else {
1038
+ const __previousQueryOptions = activeQuery.options
1039
+ // Trigger a fetch in order to trigger suspense as well.
1040
+ activeQuery.fetch({
1041
+ ...__previousQueryOptions,
1042
+ queryFn: () => {
1043
+ return new Promise(() => {
1044
+ // Never resolve
1045
+ })
1046
+ },
1047
+ gcTime: -1,
1048
+ })
1049
+ activeQuery.setState({
1050
+ data: undefined,
1051
+ status: 'pending',
1052
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
1053
+ fetchMeta: {
1054
+ ...activeQuery.state.fetchMeta,
1055
+ __previousQueryOptions,
1056
+ } as any,
1057
+ })
1058
+ }
1059
+ }}
1060
+ style={{
1061
+ background: theme.paused,
1062
+ }}
1063
+ >
1064
+ {activeQuery.state.status === 'pending' ? 'Restore' : 'Trigger'}{' '}
1065
+ loading
1066
+ </Button>{' '}
1067
+ {errorTypes.length === 0 || activeQuery.state.status === 'error' ? (
1068
+ <Button
1069
+ type="button"
1070
+ onClick={() => {
1071
+ if (!activeQuery.state.error) {
1072
+ triggerError()
1073
+ } else {
1074
+ queryClient.resetQueries(activeQuery)
1075
+ }
1076
+ }}
1077
+ style={{
1078
+ background: theme.danger,
1079
+ }}
1080
+ >
1081
+ {activeQuery.state.status === 'error' ? 'Restore' : 'Trigger'} error
1082
+ </Button>
1083
+ ) : (
1084
+ <label>
1085
+ Trigger error:
1086
+ <Select
1087
+ value={currentErrorTypeName ?? ''}
1088
+ style={{ marginInlineStart: '.5em' }}
1089
+ onChange={(e) => {
1090
+ const errorType = errorTypes.find(
1091
+ (t) => t.name === e.target.value,
1092
+ )
1093
+
1094
+ triggerError(errorType)
1095
+ }}
1096
+ >
1097
+ <option key="" value="" />
1098
+ {errorTypes.map((errorType) => (
1099
+ <option key={errorType.name} value={errorType.name}>
1100
+ {errorType.name}
1101
+ </option>
1102
+ ))}
1103
+ </Select>
1104
+ </label>
1105
+ )}
959
1106
  </div>
960
1107
  <div
961
1108
  style={{
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ 'use client'
2
+
1
3
  import * as devtools from './devtools'
2
4
 
3
5
  export const ReactQueryDevtools: (typeof devtools)['ReactQueryDevtools'] =
@@ -50,7 +50,9 @@ export const Button = styled('button', (props, theme) => ({
50
50
  }))
51
51
 
52
52
  export const QueryKeys = styled('span', {
53
- display: 'inline-block',
53
+ display: 'flex',
54
+ flexWrap: 'wrap',
55
+ gap: '0.5em',
54
56
  fontSize: '0.9em',
55
57
  })
56
58