The Wayback Machine - https://web.archive.org/web/20220509224839/https://github.com/tannerlinsley/react-query/pull/2340
Skip to content
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

fix(useQueries): make sure keepPreviousData is respected #2340

Merged

Conversation

Copy link
Collaborator

@TkDodo TkDodo commented Jun 3, 2021

fixes #2181

@vercel
Copy link

@vercel vercel bot commented Jun 3, 2021

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/tannerlinsley/react-query/8YFkmFhzi7x3fYDijjBt2Dd1JUVz
Preview: https://react-query-git-fork-tkdodo-feature-2181-u-c1a193-tannerlinsley.vercel.app

@TkDodo
Copy link
Author

@TkDodo TkDodo commented Jun 3, 2021

Hmm why is codesandbox not running on this PR and how can i trigger it? 🤔

@codesandbox
Copy link

@codesandbox codesandbox bot commented Jun 4, 2021

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 4fe6ff4:

Sandbox Source
tannerlinsley/react-query: basic Configuration
tannerlinsley/react-query: basic-typescript Configuration
useQueries-with-keepPreviousData Issue #2181

@TkDodo TkDodo requested review from tannerlinsley and boschni Jun 4, 2021
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 },
])
Copy link
Collaborator Author

@TkDodo TkDodo Jun 4, 2021

Choose a reason for hiding this comment

The 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 isPreviousData separately ... ?

)
let currentObserver = this.observersMap[defaultedOptions.queryHash!]
if (!currentObserver && defaultedOptions.keepPreviousData) {
currentObserver = this.observers[index]
Copy link
Collaborator Author

@TkDodo TkDodo Jun 4, 2021

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 queryHash like before, we're never able to make keepPreviousData work because we'll get a new Observer every time as the hash changes ...

Copy link
Collaborator Author

@TkDodo TkDodo Jun 26, 2021

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

@TkDodo
Copy link
Author

@TkDodo TkDodo commented Jun 20, 2021

@tannerlinsley @boschni could I get your feedback on this PR? I left comments in a self-review on parts where I'm not sure, thanks :)

@raoufswe
Copy link

@raoufswe raoufswe commented Jun 23, 2021

@TkDodo thanks for working on this, I have faced the same issue #2181, and would be great if we can get this merged.

Please let me know if I can help with anything.

{ status: 'success', data: 4, isPreviousData: false, isFetching: false },
{ status: 'success', data: 10, isPreviousData: false, isFetching: false },
])
})

Choose a reason for hiding this comment

The 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 },
    ])
  })

Copy link
Collaborator Author

@TkDodo TkDodo Jul 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when has 3 elements

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...

to make sure node10 resolves them in order
@TkDodo TkDodo merged commit 6bbb371 into tannerlinsley:master Aug 10, 2021
7 checks passed
@TkDodo TkDodo deleted the feature/2181-useQueries-keepPreviousData branch Aug 10, 2021
@tannerlinsley
Copy link

@tannerlinsley tannerlinsley commented Aug 10, 2021

🎉 This PR is included in version 3.19.4 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants