-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
fix(useQueries): make sure keepPreviousData is respected #2340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
40b3b01
2ff8feb
0cbf7c8
62ce4ff
513be09
6446a13
4fe6ff4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { waitFor } from '@testing-library/react' | ||
| import React from 'react' | ||
|
|
||
| import { queryKey, renderWithClient, sleep } from './utils' | ||
| import { queryKey, renderWithClient, setActTimeout, sleep } from './utils' | ||
| import { useQueries, QueryClient, UseQueryResult, QueryCache } from '../..' | ||
|
|
||
| describe('useQueries', () => { | ||
|
|
@@ -42,4 +43,196 @@ describe('useQueries', () => { | |
| expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }]) | ||
| expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) | ||
| }) | ||
|
|
||
| it('should keep previous data if amount of queries is the same', async () => { | ||
| const key1 = queryKey() | ||
| const key2 = queryKey() | ||
| const states: UseQueryResult[][] = [] | ||
|
|
||
| function Page() { | ||
| const [count, setCount] = React.useState(1) | ||
| const result = useQueries([ | ||
| { | ||
| queryKey: [key1, count], | ||
| keepPreviousData: true, | ||
| queryFn: async () => { | ||
| await sleep(5) | ||
| return count * 2 | ||
| }, | ||
| }, | ||
| { | ||
| queryKey: [key2, count], | ||
| keepPreviousData: true, | ||
| queryFn: async () => { | ||
| await sleep(10) | ||
| return count * 5 | ||
| }, | ||
| }, | ||
| ]) | ||
| states.push(result) | ||
|
|
||
| React.useEffect(() => { | ||
| setActTimeout(() => { | ||
| setCount(prev => prev + 1) | ||
| }, 20) | ||
| }, []) | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| renderWithClient(queryClient, <Page />) | ||
|
|
||
| await waitFor(() => expect(states.length).toBe(7)) | ||
|
|
||
| expect(states[0]).toMatchObject([ | ||
| { | ||
| status: 'loading', | ||
| data: undefined, | ||
| isPreviousData: false, | ||
| isFetching: true, | ||
| }, | ||
| { | ||
| status: 'loading', | ||
| data: undefined, | ||
| isPreviousData: false, | ||
| isFetching: true, | ||
| }, | ||
| ]) | ||
| expect(states[1]).toMatchObject([ | ||
| { status: 'success', data: 2, isPreviousData: false, isFetching: false }, | ||
| { | ||
| status: 'loading', | ||
| data: undefined, | ||
| isPreviousData: false, | ||
| isFetching: true, | ||
| }, | ||
| ]) | ||
| expect(states[2]).toMatchObject([ | ||
| { status: 'success', data: 2, isPreviousData: false, isFetching: false }, | ||
| { status: 'success', data: 5, isPreviousData: false, isFetching: false }, | ||
| ]) | ||
| expect(states[3]).toMatchObject([ | ||
| { status: 'success', data: 2, isPreviousData: true, isFetching: true }, | ||
| { status: 'success', data: 5, isPreviousData: true, isFetching: true }, | ||
| ]) | ||
| expect(states[4]).toMatchObject([ | ||
| { status: 'success', data: 2, isPreviousData: true, isFetching: true }, | ||
| { status: 'success', data: 5, isPreviousData: true, isFetching: true }, | ||
| ]) | ||
|
Comment on lines
+114
to
+121
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure why I'm getting two re-renders here with the same results, I think it's because each of the two queries transitions to |
||
| expect(states[5]).toMatchObject([ | ||
| { status: 'success', data: 4, isPreviousData: false, isFetching: false }, | ||
| { status: 'success', data: 5, isPreviousData: true, isFetching: true }, | ||
| ]) | ||
| expect(states[6]).toMatchObject([ | ||
| { status: 'success', data: 4, isPreviousData: false, isFetching: false }, | ||
| { status: 'success', data: 10, isPreviousData: false, isFetching: false }, | ||
| ]) | ||
| }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I created two more test cases, tell me what you think: it('should keep previous data if amount of queries is the same when has 3 elements', async () => {
const key1 = queryKey()
const key2 = queryKey()
const key3 = queryKey()
const states: UseQueryResult[][] = []
function Page() {
const [count, setCount] = React.useState(1)
const result = useQueries([
{
queryKey: [key1, count],
keepPreviousData: true,
queryFn: async () => {
await sleep(5)
return 1 * count
},
},
{
queryKey: [key2, count],
keepPreviousData: true,
queryFn: async () => {
await sleep(10)
return 2 * count
},
},
{
queryKey: [key3, count],
keepPreviousData: true,
queryFn: async () => {
await sleep(15)
return 3 * count
},
},
])
states.push(result)
React.useEffect(() => {
setActTimeout(() => {
setCount(prev => prev + 1)
}, 20)
}, [])
return null
}
renderWithClient(queryClient, <Page />)
await waitFor(() => expect(states.length).toBe(9))
expect(states[0]).toMatchObject([
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
])
expect(states[1]).toMatchObject([
{ status: 'success', data: 1, isPreviousData: false, isFetching: false },
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
])
expect(states[2]).toMatchObject([
{ status: 'success', data: 1, isPreviousData: false, isFetching: false },
{ status: 'success', data: 2, isPreviousData: false, isFetching: false },
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
])
expect(states[3]).toMatchObject([
{ status: 'success', data: 1, isPreviousData: false, isFetching: false },
{ status: 'success', data: 2, isPreviousData: false, isFetching: false },
{ status: 'success', data: 3, isPreviousData: false, isFetching: false },
])
expect(states[4]).toMatchObject([
{ status: 'success', data: 1, isPreviousData: true, isFetching: true },
{ status: 'success', data: 2, isPreviousData: true, isFetching: true },
{ status: 'success', data: 3, isPreviousData: true, isFetching: true },
])
expect(states[5]).toMatchObject([
{ status: 'success', data: 1, isPreviousData: true, isFetching: true },
{ status: 'success', data: 2, isPreviousData: true, isFetching: true },
{ status: 'success', data: 3, isPreviousData: true, isFetching: true },
])
expect(states[6]).toMatchObject([
{ status: 'success', data: 2, isPreviousData: false, isFetching: false },
{ status: 'success', data: 2, isPreviousData: true, isFetching: true },
{ status: 'success', data: 3, isPreviousData: true, isFetching: true },
])
expect(states[7]).toMatchObject([
{ status: 'success', data: 2, isPreviousData: false, isFetching: false },
{ status: 'success', data: 4, isPreviousData: false, isFetching: false },
{ status: 'success', data: 3, isPreviousData: true, isFetching: true },
])
expect(states[8]).toMatchObject([
{ status: 'success', data: 2, isPreviousData: false, isFetching: false },
{ status: 'success', data: 4, isPreviousData: false, isFetching: false },
{ status: 'success', data: 6, isPreviousData: false, isFetching: false },
])
})
it('should not keep previous data', async () => {
const key1 = queryKey()
const key2 = queryKey()
const key3 = queryKey()
const states: UseQueryResult[][] = []
function Page() {
const [count, setCount] = React.useState(1)
const result = useQueries([
{
queryKey: [key1, count],
queryFn: async () => {
await sleep(5)
return 1 * count
},
},
{
queryKey: [key2, count],
queryFn: async () => {
await sleep(10)
return 2 * count
},
},
{
queryKey: [key3, count],
queryFn: async () => {
await sleep(15)
return 3 * count
},
},
])
states.push(result)
React.useEffect(() => {
setActTimeout(() => {
setCount(prev => prev + 1)
}, 20)
}, [])
return null
}
renderWithClient(queryClient, <Page />)
await waitFor(() => expect(states.length).toBe(9))
expect(states[0]).toMatchObject([
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
])
expect(states[1]).toMatchObject([
{ status: 'success', data: 1, isPreviousData: false, isFetching: false },
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
])
expect(states[2]).toMatchObject([
{ status: 'success', data: 1, isPreviousData: false, isFetching: false },
{ status: 'success', data: 2, isPreviousData: false, isFetching: false },
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
])
expect(states[3]).toMatchObject([
{ status: 'success', data: 1, isPreviousData: false, isFetching: false },
{ status: 'success', data: 2, isPreviousData: false, isFetching: false },
{ status: 'success', data: 3, isPreviousData: false, isFetching: false },
])
expect(states[4]).toMatchObject([
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
])
expect(states[5]).toMatchObject([
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
])
expect(states[6]).toMatchObject([
{ status: 'success', data: 2, isPreviousData: false, isFetching: false },
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
])
expect(states[7]).toMatchObject([
{ status: 'success', data: 2, isPreviousData: false, isFetching: false },
{ status: 'success', data: 4, isPreviousData: false, isFetching: false },
{
status: 'loading',
data: undefined,
isPreviousData: false,
isFetching: true,
},
])
expect(states[8]).toMatchObject([
{ status: 'success', data: 2, isPreviousData: false, isFetching: false },
{ status: 'success', data: 4, isPreviousData: false, isFetching: false },
{ status: 'success', data: 6, isPreviousData: false, isFetching: false },
])
})
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
thanks, but your test has 3 static elements. My claim is that it might not work with dynamic elements, so 2 elements on the first render, followed by 3 elements on the next render. I added a test for this here: 62ce4ff and it seems to work okay. I'm still positive that if you remove one element in the middle of the list, you will keep the previous data from this item. not sure if this will be perceived as intended behaviour, which is why I've suggested a different solution here: #2340 (comment), which would limit keepPreviousData to arrays of the same length... |
||
|
|
||
| it('should keep previous data for variable amounts of useQueries', async () => { | ||
| const key = queryKey() | ||
| const states: UseQueryResult[][] = [] | ||
|
|
||
| function Page() { | ||
| const [count, setCount] = React.useState(2) | ||
| const result = useQueries( | ||
| Array.from({ length: count }, (_, i) => ({ | ||
| queryKey: [key, count, i + 1], | ||
| keepPreviousData: true, | ||
| queryFn: async () => { | ||
| await sleep(5 * (i + 1)) | ||
| return (i + 1) * count * 2 | ||
| }, | ||
| })) | ||
| ) | ||
|
|
||
| states.push(result) | ||
|
|
||
| React.useEffect(() => { | ||
| setActTimeout(() => { | ||
| setCount(prev => prev + 1) | ||
| }, 20) | ||
| }, []) | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| renderWithClient(queryClient, <Page />) | ||
|
|
||
| await waitFor(() => expect(states.length).toBe(8)) | ||
|
|
||
| expect(states[0]).toMatchObject([ | ||
| { | ||
| status: 'loading', | ||
| data: undefined, | ||
| isPreviousData: false, | ||
| isFetching: true, | ||
| }, | ||
| { | ||
| status: 'loading', | ||
| data: undefined, | ||
| isPreviousData: false, | ||
| isFetching: true, | ||
| }, | ||
| ]) | ||
| expect(states[1]).toMatchObject([ | ||
| { status: 'success', data: 4, isPreviousData: false, isFetching: false }, | ||
| { | ||
| status: 'loading', | ||
| data: undefined, | ||
| isPreviousData: false, | ||
| isFetching: true, | ||
| }, | ||
| ]) | ||
| expect(states[2]).toMatchObject([ | ||
| { status: 'success', data: 4, isPreviousData: false, isFetching: false }, | ||
| { status: 'success', data: 8, isPreviousData: false, isFetching: false }, | ||
| ]) | ||
|
|
||
| expect(states[3]).toMatchObject([ | ||
| { status: 'success', data: 4, isPreviousData: true, isFetching: true }, | ||
| { status: 'success', data: 8, isPreviousData: true, isFetching: true }, | ||
| { | ||
| status: 'loading', | ||
| data: undefined, | ||
| isPreviousData: false, | ||
| isFetching: true, | ||
| }, | ||
| ]) | ||
| expect(states[4]).toMatchObject([ | ||
| { status: 'success', data: 4, isPreviousData: true, isFetching: true }, | ||
| { status: 'success', data: 8, isPreviousData: true, isFetching: true }, | ||
| { | ||
| status: 'loading', | ||
| data: undefined, | ||
| isPreviousData: false, | ||
| isFetching: true, | ||
| }, | ||
| ]) | ||
| expect(states[5]).toMatchObject([ | ||
| { status: 'success', data: 6, isPreviousData: false, isFetching: false }, | ||
| { status: 'success', data: 8, isPreviousData: true, isFetching: true }, | ||
| { | ||
| status: 'loading', | ||
| data: undefined, | ||
| isPreviousData: false, | ||
| isFetching: true, | ||
| }, | ||
| ]) | ||
| expect(states[6]).toMatchObject([ | ||
| { status: 'success', data: 6, isPreviousData: false, isFetching: false }, | ||
| { status: 'success', data: 12, isPreviousData: false, isFetching: false }, | ||
| { | ||
| status: 'loading', | ||
| data: undefined, | ||
| isPreviousData: false, | ||
| isFetching: true, | ||
| }, | ||
| ]) | ||
| expect(states[7]).toMatchObject([ | ||
| { status: 'success', data: 6, isPreviousData: false, isFetching: false }, | ||
| { status: 'success', data: 12, isPreviousData: false, isFetching: false }, | ||
| { status: 'success', data: 18, isPreviousData: false, isFetching: false }, | ||
| ]) | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please let me know if this is a bad idea. Generally, I was thinking that if we have keepPreviousData on, we would like to keep the data of the query that lies at the index. This likely won't work if the lengths are completely different, and we might "keep" wrong data then. But by just going by the
queryHashlike before, we're never able to makekeepPreviousDatawork because we'll get a new Observer every time as the hash changes ...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One way to limit this would be to only do the index access if the length hasn’t changed. That would make keepPreviousData only work on equal lengths though and we should likely document that