Skip to content

Commit

Permalink
Add areEqualOwners to check for structural equality of fragment owner…
Browse files Browse the repository at this point in the history
…s. (#4500)

Summary:
There is a bug in the experimental hooks implementation.

useRefetchableFragment will return a new data object every time the refetch function is called, even if the data has not changed. This is because refetch creates a new fragment owner with createOperationDescriptor and refetch variables, and passes it to useFragmentInternal as part of the fragment key.

Later in useFragmentInternal, we call areEqualSelectors to check if the fragment selector has changed. The owner is part of the selector, but we only check if the owner is the same object. However, it's possible that the owner is the same but just a new instance of the same object.

Proposed fix:
* Add areEqualOwners to check that owners are equivalent. This would prevent an endless loop of refetch/setState between components using useRefetchableFragment and the refetch call in the useEffect.

Pull Request resolved: #4500

Test Plan:
* New unit test-case to useRefetchableFragmentNode-test.
* Updated tests for RelayModernSelector.

Reviewed By: voideanvalue

Differential Revision: D50524883

Pulled By: alunyov

fbshipit-source-id: 6d9df3abc80ab997a59bf961e7a721da6ad769e3
  • Loading branch information
alunyov authored and facebook-github-bot committed Oct 24, 2023
1 parent d35e27b commit 8011e22
Show file tree
Hide file tree
Showing 7 changed files with 417 additions and 10 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type {Query} from 'relay-runtime/util/RelayRuntimeTypes';

const {useTrackLoadQueryInRender} = require('../loadQuery');
const useRefetchableFragmentInternal_REACT_CACHE = require('../react-cache/useRefetchableFragmentInternal_REACT_CACHE');
const RelayEnvironmentProvider = require('../RelayEnvironmentProvider');
const useRefetchableFragmentNode_LEGACY = require('../useRefetchableFragmentNode');
const invariant = require('invariant');
const React = require('react');
Expand Down Expand Up @@ -1393,7 +1394,7 @@ describe.each([
]);
});

it('warns if data retured has different __typename', () => {
it('warns if data returned has different __typename', () => {
const warning = require('warning');
// $FlowFixMe[prop-missing]
warning.mockClear();
Expand Down Expand Up @@ -1985,7 +1986,7 @@ describe.each([
{force: true},
);

// Assert we suspend on intial refetch request
// Assert we suspend on initial refetch request
expectFragmentIsRefetching(renderer, {
refetchQuery: refetchQuery1,
refetchVariables: refetchVariables1,
Expand Down Expand Up @@ -2154,6 +2155,75 @@ describe.each([

expect(fetchSpy).toBeCalledTimes(4);
});

it('preserves referential equality after refetch if data & variables have not changed', async () => {
let refetchCount = 0;
const ComponentWithUseEffectRefetch = (props: {
fragmentKey: any,
}): null => {
const {fragmentData, refetch} = useRefetchableFragmentNode(
graphql`
fragment useRefetchableFragmentNodeTestIdentityTestFragment on User
@refetchable(
queryName: "useRefetchableFragmentNodeTestIdentityTestFragmentRefetchQuery"
) {
id
name
profile_picture(scale: $scale) {
uri
}
}
`,
props.fragmentKey,
);
if (refetchCount > 2) {
throw new Error('Detected refetch loop.');
}
useEffect(() => {
refetchCount++;
refetch(fragmentData.id);
}, [fragmentData, refetch]);

return null;
};
const variables = {id: '1', scale: 16};
const query = createOperationDescriptor(
gqlRefetchQuery,
variables,
{},
);
environment.commitPayload(query, {
node: {
__typename: 'User',
id: '1',
name: 'Alice',
profile_picture: null,
},
});
let renderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<ErrorBoundary fallback={({error}) => `Error: ${error.message}`}>
<React.Suspense fallback={'Loading'}>
<RelayEnvironmentProvider environment={environment}>
<ComponentWithUseEffectRefetch
fragmentKey={createFragmentRef(
'1',
query,
'useRefetchableFragmentNodeTestIdentityTestFragment',
)}
/>
</RelayEnvironmentProvider>
</React.Suspense>
</ErrorBoundary>,
// $FlowFixMe[prop-missing] - error revealed when flow-typing ReactTestRenderer
{unstable_isConcurrent: true},
);
jest.runAllImmediates();
});
expect(refetchCount).toBe(2);
expect(renderer?.toJSON()).toBe(null);
});
});

describe('fetchPolicy', () => {
Expand Down
Loading

0 comments on commit 8011e22

Please sign in to comment.