Skip to content

fix: If newData is deeply to the latest state, broadcast the latest state #1697

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ export const useSWRHandler = <Data = any, Error = any>(
// For local state, compare and assign.
if (!compare(stateRef.current.data, newData)) {
newState.data = newData
} else {
// data and newData are deeply equal
// it should be safe to broadcast the stale data
newState.data = stateRef.current.data
// At the end of this function, `brocastState` invokes the `onStateUpdate` function,
// which takes care of avoiding the re-render
}
Comment on lines 264 to 272
Copy link
Member

Choose a reason for hiding this comment

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

This is a good fix! And I just took a deeper look and it seems the problem is caused by here:

compare(stateRef.current.data, updatedData)

When they're not equal, we broadcast the new data even if the new data is not defined. So another way to fix it is adding:

isUndefined(updatedData) ||

to that line of code.

Copy link
Contributor Author

@icyJoseph icyJoseph Dec 8, 2021

Choose a reason for hiding this comment

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

Do you have any preference? I guess one if branch vs if-else is more terse?

Also, I commented heavily here, but perhaps I should revert to the original comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I may have misunderstood but

if (isUndefined(newData) || !compare(stateRef.current.data, newData)) {
          newState.data = newData
 }

Still requires the else block right (the test still fails otherwise)? We need to bind newState.data to stateRef.current.data.

Copy link
Member

Choose a reason for hiding this comment

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

Oh sorry I meant adding it here:

      setState(
        mergeObjects(
          {
            error: updatedError,
            isValidating: updatedIsValidating
          },
          // Since `setState` only shallowly compares states, we do a deep
          // comparison here.
+         isUndefined(newData) || compare(stateRef.current.data, updatedData)
            ? UNDEFINED
            : {
                data: updatedData
              }
        )
      )

I prefer this for now because it is more universal, so we have a way to call broadCastState() but skip updating the data, if it's undefined.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried that too, and it caused a couple other tests to failed, iirc.

Not on my laptop now, but I'll retry first thing in the morning if that's ok.

I also agree that placing the check there seems more correct.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, if that doesn’t work we can publish your original fix for sure 👍

Copy link
Contributor Author

@icyJoseph icyJoseph Dec 9, 2021

Choose a reason for hiding this comment

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

The test cases that stop working are:

● useSWR - local mutation › should be able to mutate data to undefined
● useSWR - local mutation › should be able to mutate data to undefined asynchronously

The test description kinda makes it clear why, because when mutating to undefined, with the proposed changed we'd be broadcasting the stale state instead.

Since the proposal didn't work out, I also tried:

 isUndefined(updatedData)
            ? { data: UNDEFINED }
            : compare(stateRef.current.data, updatedData)
            ? UNDEFINED
            : {
                data: updatedData
              }

But this breaks two tests 😅 the first one is introduced on this MR, it implicitly reintroduces the initial issue.

● useSWR - loading › should avoid extra rerenders is the data is the `same` 
● useSWR - configs › should read the config fallback from the context

I think it'd be an ok trade-off to patch this now, and investigate further.

Copy link
Member

Choose a reason for hiding this comment

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

Thank you, yeah that makes total sense!


// For global state, it's possible that the key has changed.
Expand Down
47 changes: 46 additions & 1 deletion test/use-swr-loading.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { act, screen, fireEvent } from '@testing-library/react'
import React from 'react'
import React, { useEffect } from 'react'
import useSWR from 'swr'
import {
createResponse,
Expand Down Expand Up @@ -77,6 +77,51 @@ describe('useSWR - loading', () => {
expect(dataLoaded).toEqual(true)
})

it('should avoid extra rerenders is the data is the `same`', async () => {
let renderCount = 0,
initialDataLoaded = false,
mutationDataLoaded = false

const key = createKey()
function Page() {
const { data, mutate } = useSWR(
key,
async () => {
const res = await createResponse({ greeting: 'hello' })
initialDataLoaded = true
return res
},
{ fallbackData: { greeting: 'hello' } }
)

useEffect(() => {
const timeout = setTimeout(
() =>
mutate(async () => {
const res = await createResponse({ greeting: 'hello' })
mutationDataLoaded = true
return res
}),
200
)

return () => clearTimeout(timeout)
}, [mutate])

renderCount++
return <div>{data?.greeting}</div>
}

renderWithConfig(<Page />)
screen.getByText('hello')

await act(() => sleep(1000)) // wait
// it doesn't re-render, but fetch was triggered
expect(initialDataLoaded).toEqual(true)
expect(mutationDataLoaded).toEqual(true)
expect(renderCount).toEqual(1)
})

it('should return enumerable object', async () => {
// If the returned object is enumerable, we can use the spread operator
// to deconstruct all the keys.
Expand Down