Skip to content

Back out "Remove loadQuery during render warning" #4930

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
52 changes: 51 additions & 1 deletion packages/react-relay/relay-hooks/__tests__/loadQuery-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ import type {
Variables,
} from 'relay-runtime';

const {loadQuery} = require('../loadQuery');
const {loadQuery, useTrackLoadQueryInRender} = require('../loadQuery');
// Need React require for OSS build
// eslint-disable-next-line no-unused-vars
const React = require('react');
const ReactTestRenderer = require('react-test-renderer');
const {
Network,
Observable,
Expand All @@ -40,6 +41,7 @@ const {
createMockEnvironment,
disallowConsoleErrors,
disallowWarnings,
expectWarningWillFire,
} = require('relay-test-utils-internal');

disallowWarnings();
Expand Down Expand Up @@ -945,4 +947,52 @@ describe('loadQuery', () => {
});
});
});

describe('warnings', () => {
let Container;
let LoadDuringRender;

beforeEach(() => {
Container = (props: {children: React.Node}) => {
// $FlowFixMe[react-rule-hook]
useTrackLoadQueryInRender();
return props.children;
};
LoadDuringRender = (props: {name?: ?string}) => {
loadQuery(environment, preloadableConcreteRequest, variables, {
fetchPolicy: 'store-or-network',
__nameForWarning: props.name,
});
return null;
};
});

it('warns if called during render', () => {
expectWarningWillFire(
'Relay: `loadQuery` should not be called inside a React render function.',
);

ReactTestRenderer.act(() => {
ReactTestRenderer.create(
<Container>
<LoadDuringRender />
</Container>,
);
});
});

it('uses provided name for warning', () => {
expectWarningWillFire(
'Relay: `refetch` should not be called inside a React render function.',
);

ReactTestRenderer.act(() => {
ReactTestRenderer.create(
<Container>
<LoadDuringRender name="refetch" />
</Container>,
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,16 @@ const {createMockEnvironment} = require('relay-test-utils');
const {
injectPromisePolyfill__DEPRECATED,
} = require('relay-test-utils-internal');
const warning = require('warning');
const {
disallowConsoleErrors,
disallowWarnings,
expectToWarn,
} = require('relay-test-utils-internal');

injectPromisePolyfill__DEPRECATED();
disallowWarnings();
disallowConsoleErrors();

jest.mock('warning');
injectPromisePolyfill__DEPRECATED();

const query = graphql`
query usePreloadedQueryTestQuery($id: ID!) {
Expand Down Expand Up @@ -110,11 +115,6 @@ class ErrorBoundary extends React.Component<$FlowFixMe, $FlowFixMe> {
}
}

beforeEach(() => {
// $FlowFixMe[prop-missing]
warning.mockClear();
});

afterAll(() => {
jest.clearAllMocks();
});
Expand Down Expand Up @@ -1041,15 +1041,20 @@ describe('usePreloadedQuery', () => {
return data.node?.name;
}
let renderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<RelayEnvironmentProvider environment={altEnvironment}>
<React.Suspense fallback="Fallback">
<Component prefetched={prefetched} />
</React.Suspense>
</RelayEnvironmentProvider>,
);
});
expectToWarn(
'usePreloadedQuery(): usePreloadedQuery was passed a preloaded query that was created with a different environment than the one that is currently in context. In the future, this will become a hard error.',
() => {
TestRenderer.act(() => {
renderer = TestRenderer.create(
<RelayEnvironmentProvider environment={altEnvironment}>
<React.Suspense fallback="Fallback">
<Component prefetched={prefetched} />
</React.Suspense>
</RelayEnvironmentProvider>,
);
});
},
);

expect(renderer?.toJSON()).toEqual('Fallback');
expect(altFetch).toHaveBeenCalledTimes(1);
Expand All @@ -1058,16 +1063,20 @@ describe('usePreloadedQuery', () => {
altDataSource.next(response);
}

TestRenderer.act(() => jest.runAllImmediates());
expectToWarn(
'usePreloadedQuery(): usePreloadedQuery was passed a preloaded query that was created with a different environment than the one that is currently in context. In the future, this will become a hard error.',
() => {
TestRenderer.act(() => jest.runAllImmediates());
},
);
expect(renderer?.toJSON()).toEqual('Zuck');
});
});

describe('when loadQuery is passed a preloadedQuery that was disposed', () => {
it('warns that the preloadedQuery has already been disposed', () => {
const expectWarningMessage = expect.stringMatching(
/^usePreloadedQuery\(\): Expected preloadedQuery to not be disposed/,
);
const expectWarningMessage =
'usePreloadedQuery(): Expected preloadedQuery to not be disposed yet. This is because disposing the query marks it for future garbage collection, and as such query results may no longer be present in the Relay store. In the future, this will become a hard error.';
const prefetched = loadQuery(environment, preloadableConcreteRequest, {
id: '1',
});
Expand All @@ -1091,19 +1100,11 @@ describe('usePreloadedQuery', () => {
};

render();
expect(warning).toBeCalledTimes(1);
expect(warning).toHaveBeenLastCalledWith(
true, // invariant holds
expectWarningMessage,
);

prefetched.dispose();
render();
expect(warning).toBeCalledTimes(2);
expect(warning).toHaveBeenLastCalledWith(
false, // invariant broken
expectWarningMessage,
);
expectToWarn(expectWarningMessage, () => {
render();
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const loadQuery = jest.fn().mockImplementation(() => {

jest.mock('../loadQuery', () => ({
loadQuery,
useTrackLoadQueryInRender: () => {},
}));

beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const loadQuery = jest.fn().mockImplementation(() => {

jest.mock('../loadQuery', () => ({
loadQuery,
useTrackLoadQueryInRender: () => {},
}));

describe.each([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type {
import type {OperationDescriptor, Variables} from 'relay-runtime';
import type {Query} from 'relay-runtime/util/RelayRuntimeTypes';

const {useTrackLoadQueryInRender} = require('../loadQuery');
const RelayEnvironmentProvider = require('../RelayEnvironmentProvider');
const useRefetchableFragmentInternal = require('../useRefetchableFragmentInternal');
const invariant = require('invariant');
Expand Down Expand Up @@ -368,6 +369,7 @@ describe.each([['New', useRefetchableFragmentInternal]])(
};

const ContextProvider = ({children}: {children: React.Node}) => {
useTrackLoadQueryInRender();
const [env, _setEnv] = useState(environment);
const relayContext = useMemo(() => ({environment: env}), [env]);

Expand Down Expand Up @@ -646,10 +648,11 @@ describe.each([['New', useRefetchableFragmentInternal]])(
refetch({id: '4'});
});

expect(warning).toHaveBeenCalledTimes(1);
// $FlowFixMe[prop-missing]
const triggeredWarnings = warning.mock.calls.filter(args => !args[0]);
expect(triggeredWarnings.length).toBe(1);
expect(
// $FlowFixMe[prop-missing]
warning.mock.calls[0][1].includes(
triggeredWarnings[0][1].includes(
'Relay: Unexpected call to `refetch` while using a null fragment ref',
),
).toEqual(true);
Expand Down
25 changes: 25 additions & 0 deletions packages/react-relay/relay-hooks/loadQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
import type {OperationAvailability} from 'relay-runtime/store/RelayStoreTypes';

const invariant = require('invariant');
const React = require('react');
const {
__internal: {fetchQueryDeduped},
Observable,
Expand All @@ -40,9 +41,21 @@ const {
getRequest,
getRequestIdentifier,
} = require('relay-runtime');
const warning = require('warning');

let RenderDispatcher = null;
let fetchKey = 100001;

hook useTrackLoadQueryInRender() {
if (RenderDispatcher === null) {
// Flow does not know of React internals (rightly so), but we need to
// ensure here that this function isn't called inside render.
RenderDispatcher =
// $FlowFixMe[prop-missing]
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?.H;
}
}

type QueryType<T> =
T extends Query<infer V, infer D, infer RR>
? {
Expand Down Expand Up @@ -75,6 +88,17 @@ function loadQuery<
options?: ?LoadQueryOptions,
environmentProviderOptions?: ?TEnvironmentProviderOptions,
): PreloadedQueryInner<TQuery, TEnvironmentProviderOptions> {
// This code ensures that we don't call loadQuery during render.
const CurrentDispatcher =
// $FlowFixMe[prop-missing]
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?.H;

warning(
RenderDispatcher == null || CurrentDispatcher !== RenderDispatcher,
'Relay: `%s` should not be called inside a React render function.',
options?.__nameForWarning ?? 'loadQuery',
);

// Every time you call loadQuery, we will generate a new fetchKey.
// This will ensure that every query reference that is created and
// passed to usePreloadedQuery is independently evaluated,
Expand Down Expand Up @@ -392,4 +416,5 @@ function loadQuery<

module.exports = {
loadQuery,
useTrackLoadQueryInRender,
};
3 changes: 3 additions & 0 deletions packages/react-relay/relay-hooks/useEntryPointLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
} from './EntryPointTypes.flow';

const loadEntryPoint = require('./loadEntryPoint');
const {useTrackLoadQueryInRender} = require('./loadQuery');
const useIsMountedRef = require('./useIsMountedRef');
const {useCallback, useEffect, useRef, useState} = require('react');

Expand Down Expand Up @@ -103,6 +104,8 @@ hook useLoadEntryPoint<
* entry point references.
*/

useTrackLoadQueryInRender();

const initialEntryPointReferenceInternal =
options?.TEST_ONLY__initialEntryPointData?.entryPointReference ??
initialNullEntryPointReferenceState;
Expand Down
5 changes: 5 additions & 0 deletions packages/react-relay/relay-hooks/useFragment.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import type {Fragment, FragmentType, GraphQLTaggedNode} from 'relay-runtime';

const {useTrackLoadQueryInRender} = require('./loadQuery');
const useFragmentInternal = require('./useFragmentInternal');
const useStaticFragmentNodeWarning = require('./useStaticFragmentNodeWarning');
const {useDebugValue} = require('react');
Expand Down Expand Up @@ -48,6 +49,10 @@ declare hook useFragment<TFragmentType: FragmentType, TData>(
): ?TData;

hook useFragment(fragment: GraphQLTaggedNode, key: mixed): mixed {
// We need to use this hook in order to be able to track if
// loadQuery was called during render
useTrackLoadQueryInRender();

const fragmentNode = getFragment(fragment);
useStaticFragmentNodeWarning(fragmentNode, 'first argument of useFragment()');
const data = useFragmentInternal(fragmentNode, key, 'useFragment()');
Expand Down
5 changes: 5 additions & 0 deletions packages/react-relay/relay-hooks/useLazyLoadQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
Variables,
} from 'relay-runtime';

const {useTrackLoadQueryInRender} = require('./loadQuery');
const useLazyLoadQueryNode = require('./useLazyLoadQueryNode');
const useMemoOperationDescriptor = require('./useMemoOperationDescriptor');
const useRelayEnvironment = require('./useRelayEnvironment');
Expand Down Expand Up @@ -50,6 +51,10 @@ hook useLazyLoadQuery<TVariables: Variables, TData>(
UNSTABLE_renderPolicy?: RenderPolicy,
},
): TData {
// We need to use this hook in order to be able to track if
// loadQuery was called during render
useTrackLoadQueryInRender();

const environment = useRelayEnvironment();

const query = useMemoOperationDescriptor(
Expand Down
5 changes: 5 additions & 0 deletions packages/react-relay/relay-hooks/usePreloadedQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
} from './EntryPointTypes.flow';
import type {Query, RenderPolicy, Variables} from 'relay-runtime';

const {useTrackLoadQueryInRender} = require('./loadQuery');
const useLazyLoadQueryNode = require('./useLazyLoadQueryNode');
const useMemoOperationDescriptor = require('./useMemoOperationDescriptor');
const useRelayEnvironment = require('./useRelayEnvironment');
Expand Down Expand Up @@ -66,6 +67,10 @@ hook usePreloadedQuery<
UNSTABLE_renderPolicy?: RenderPolicy,
},
): TData {
// We need to use this hook in order to be able to track if
// loadQuery was called during render
useTrackLoadQueryInRender();

const environment = useRelayEnvironment();
const {fetchKey, fetchPolicy, source, variables, networkCacheConfig} =
preloadedQuery;
Expand Down
3 changes: 2 additions & 1 deletion packages/react-relay/relay-hooks/useQueryLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type {
Variables,
} from 'relay-runtime';

const {loadQuery} = require('./loadQuery');
const {loadQuery, useTrackLoadQueryInRender} = require('./loadQuery');
const useIsMountedRef = require('./useIsMountedRef');
const useQueryLoader_EXPERIMENTAL = require('./useQueryLoader_EXPERIMENTAL');
const useRelayEnvironment = require('./useRelayEnvironment');
Expand Down Expand Up @@ -170,6 +170,7 @@ hook useQueryLoader_CURRENT<
initialQueryReference ?? initialNullQueryReferenceState;

const environment = useRelayEnvironment();
useTrackLoadQueryInRender();

const isMountedRef = useIsMountedRef();
const undisposedQueryReferencesRef = useRef<
Expand Down
Loading