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

Adjust relative timing of useFragment and useQuery #11083

Merged
merged 10 commits into from
Jul 28, 2023

Conversation

phryneas
Copy link
Member

@phryneas phryneas commented Jul 19, 2023

Ensures that useQuery triggers rerenders faster or at the same time as useFragment.

Before this change, useQuery used a useState setter callback to trigger a
rerender instead of the useSyncExternalState forceRender callback.
Due to a bug in React (facebook/react#25191), these renders
are not batched together.
That could lead to situations where a child accessing a fragment that was evicted
from the cache would rerender (without data) before a parent useQuery component
had a chance to unmount the child.
After this change, useQuery and useFragment still don't rerender exactly at the same moment
in time - but it is guaranteed that useQuery will always rerender before useFragment will,
which should fix most problems.

I know, it's late for this, but this is the last moment in time we have to fix useFragment before it gets stable - and we should not release it and then immediately change it's timing.

Checklist:

  • If this PR contains changes to the library itself (not necessary for e.g. docs updates), please include a changeset (see CONTRIBUTING.md)
  • If this PR is a new feature, please reference an issue where a consensus about the design was reached (not necessary for small changes)
  • Make sure all of the significant new logic is covered by tests

@changeset-bot
Copy link

changeset-bot bot commented Jul 19, 2023

🦋 Changeset detected

Latest commit: d5db4c4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@apollo/client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@phryneas
Copy link
Member Author

/release:pr

@github-actions
Copy link
Contributor

A new release has been made for this PR. You can install it with npm i @apollo/client@0.0.0-pr-11083-20230719155342.

@github-actions
Copy link
Contributor

github-actions bot commented Jul 19, 2023

size-limit report 📦

Path Size
dist/apollo-client.min.cjs 37.16 KB (+0.07% 🔺)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/main.cjs" 43.73 KB (+0.09% 🔺)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/main.cjs" (production) 42.33 KB (+0.09% 🔺)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/index.js" 32.39 KB (0%)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/index.js" (production) 31.22 KB (0%)
import { ApolloProvider } from "dist/react/index.js" 1.21 KB (0%)
import { ApolloProvider } from "dist/react/index.js" (production) 1.2 KB (0%)
import { useQuery } from "dist/react/index.js" 4.29 KB (+0.67% 🔺)
import { useQuery } from "dist/react/index.js" (production) 4.1 KB (+0.58% 🔺)
import { useLazyQuery } from "dist/react/index.js" 4.6 KB (+0.47% 🔺)
import { useLazyQuery } from "dist/react/index.js" (production) 4.42 KB (+0.61% 🔺)
import { useMutation } from "dist/react/index.js" 2.53 KB (0%)
import { useMutation } from "dist/react/index.js" (production) 2.51 KB (0%)
import { useSubscription } from "dist/react/index.js" 2.24 KB (0%)
import { useSubscription } from "dist/react/index.js" (production) 2.2 KB (0%)
import { useSuspenseQuery } from "dist/react/index.js" 4.71 KB (0%)
import { useSuspenseQuery } from "dist/react/index.js" (production) 4.14 KB (0%)
import { useBackgroundQuery } from "dist/react/index.js" 4.2 KB (0%)
import { useBackgroundQuery } from "dist/react/index.js" (production) 3.63 KB (0%)
import { useReadQuery } from "dist/react/index.js" 2.69 KB (0%)
import { useReadQuery } from "dist/react/index.js" (production) 2.67 KB (0%)
import { useFragment } from "dist/react/index.js" 2.08 KB (+1.39% 🔺)
import { useFragment } from "dist/react/index.js" (production) 2.03 KB (+1.42% 🔺)

@phryneas phryneas linked an issue Jul 20, 2023 that may be closed by this pull request
Comment on lines 728 to 733
await waitFor(() => {
if (IS_REACT_18) {
expect(count).toBe(3)
} else {
expect(count).toBe(12)
if (testFailures.length > 0) {
throw testFailures[0];
}
expect(count).toBe(12)
});
Copy link
Member Author

@phryneas phryneas Jul 24, 2023

Choose a reason for hiding this comment

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

A lot of tests had "just 3 renders in React 18" because during the tests, some expect statement failed.
That would be swallowed by React and not propagate to jest - but interrupt the React renders for good. The test would just stop going, but not really test anything.

I've changed a lot of tests to this pattern of atestFailures array to collect errors during rendering, and then checking that inside the waitFor.
Checking it after the waitFor would be more optimal, but if anything interrupted the counting from happening, the test would just time out without ever reporting why. This way the test times out, but then errors with the first in-component error or failed expect.

There is a ton more of those tests with a render behaviour that seems to have gotten problematic with React 18 (and we will have to fix those!) - but so far I only fixed those tests that were somehow affected by the timing changes.

@phryneas phryneas changed the title [draft] use fragment timing Adjust relative timing of useFragment and useQuery Jul 24, 2023
@phryneas phryneas marked this pull request as ready for review July 24, 2023 15:11
@@ -68,7 +68,7 @@ export function useLazyQuery<TData = any, TVariables extends OperationVariables
if (!execOptionsRef.current) {
execOptionsRef.current = Object.create(null);
// Only the first time populating execOptionsRef.current matters here.
internalState.forceUpdate();
internalState.forceUpdateState();
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the one situation where we want to always call the useState setter update - at this point in time, the useSyncExternalStore result has not changed from the last render, and thus React would skip the rerender, which in some situations is necessary.

src/react/hooks/useQuery.ts Outdated Show resolved Hide resolved
@phryneas
Copy link
Member Author

/release:pr

@github-actions
Copy link
Contributor

A new release has been made for this PR. You can install it with npm i @apollo/client@0.0.0-pr-11083-20230725111454.

@phryneas
Copy link
Member Author

Just to add some context here, to better understand what's going on:

useQuery

  • InMemoryCache.evict
  • InMemoryCache.broadcastWatch
  • QueryInfo.setDiff
  • setTimeout(QueryInfo.notify)
  • listener registered via QueryInfo.setObservableQuery: reobserveCacheFirst(ObservableQuery)
  • ObservableQuery.reobserveAsConcast
  • concast -> observer.next
  • ObservableQuery.reportResult
  • iterateObserversSafely -> next
  • useQuery onNext

useFragment

  • InMemoryCache.evict
  • InMemoryCache.broadcastWatch
  • callback in useFragment

So it's probably impossible to get this behavior really "synchronous" if we don't want to stuff an ObservableQuery into useFragment just for the fun of it - and even then, stuff could really get out of sync.

@phryneas phryneas self-assigned this Jul 27, 2023
Copy link
Member

@alessbell alessbell left a comment

Choose a reason for hiding this comment

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

After talking through the details of this PR with Lenz, this seems like a solid approach to addressing the useFragment timing issue with useQuery given some larger constraints 🙌 🎉

One notable fact I learned in the course of our discussion: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified. Thanks for sharing your very thorough deep dive here, @phryneas!

Copy link
Member

@jerelmiller jerelmiller left a comment

Choose a reason for hiding this comment

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

This stuff all looks great! Thanks for digging into this so far. I've not nothing else that @alessbell hasn't already mentioned. Thanks!

.changeset/honest-ads-act.md Outdated Show resolved Hide resolved
@@ -11,8 +11,6 @@ import { mockSingleLink } from '../../../../testing';
import { graphql } from '../../graphql';
import { ChildProps } from '../../types';

const IS_REACT_18 = React.version.startsWith('18')
Copy link
Member

Choose a reason for hiding this comment

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

🎉 this PR is worth it for this change alone 🙌

src/react/hooks/__tests__/useFragment.test.tsx Outdated Show resolved Hide resolved
Co-authored-by: Jerel Miller <jerelmiller@gmail.com>
@netlify
Copy link

netlify bot commented Jul 28, 2023

Deploy Preview for apollo-client-docs ready!

Name Link
🔨 Latest commit d5db4c4
🔍 Latest deploy log https://app.netlify.com/sites/apollo-client-docs/deploys/64c36fa514f7e6000828a235
😎 Deploy Preview https://deploy-preview-11083--apollo-client-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@phryneas phryneas merged commit f766e83 into release-3.8 Jul 28, 2023
8 checks passed
@github-actions github-actions bot mentioned this pull request Jul 28, 2023
@jerelmiller jerelmiller deleted the pr/useFragment-timing branch July 28, 2023 07:43
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 28, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

useFragment returns data {} after entity was evicted
3 participants