Skip to content

Commit 82f584a

Browse files
committed
Add empty query detection and skipping
Adds EmptyChecker to detect queries with no server-fetchable fields due to @skip/@include directives or client-only fields. When enabled via the ENABLE_EMPTY_QUERY_CHECK feature flag, empty queries are skipped and do not result in network requests. Key changes: - New EmptyChecker module to traverse query AST and detect empty queries - Add isEmpty() method to IEnvironment, IMultiActorEnvironment interfaces - Skip execution in RelayModernEnvironment.execute() and executeWithSource() - Update loadQuery to check isEmpty() before fetching - Add 'execute.skipped' log event with reason: 'empty' - Add comprehensive tests for EmptyChecker and React hooks integration - Feature flag ENABLE_EMPTY_QUERY_CHECK (default: false) Test plan: - EmptyChecker-test.js: Tests all query AST node types and conditional logic - useLazyLoadQueryNode-empty-query-test.js: Tests useLazyLoadQuery and usePreloadedQuery + loadQuery patterns with empty queries
1 parent 0851da3 commit 82f584a

32 files changed

Lines changed: 3189 additions & 4 deletions

packages/react-relay/relay-hooks/__tests__/__generated__/useLazyLoadQueryNodeEmptyQueryTestPreloadedQuery.graphql.js

Lines changed: 113 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-relay/relay-hooks/__tests__/__generated__/useLazyLoadQueryNodeEmptyQueryTestSkipQuery.graphql.js

Lines changed: 113 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
* @format
9+
* @oncall relay
10+
*/
11+
12+
'use strict';
13+
14+
import type {LogEvent} from '../../../relay-runtime/store/RelayStoreTypes';
15+
16+
const RelayEnvironmentProvider = require('../RelayEnvironmentProvider');
17+
const useLazyLoadQuery = require('../useLazyLoadQuery');
18+
const usePreloadedQuery = require('../usePreloadedQuery');
19+
const React = require('react');
20+
const ReactTestRenderer = require('react-test-renderer');
21+
const {
22+
Environment,
23+
Network,
24+
RecordSource,
25+
Store,
26+
graphql,
27+
} = require('relay-runtime');
28+
const {loadQuery} = require('../loadQuery');
29+
const RelayFeatureFlags = require('relay-runtime/util/RelayFeatureFlags');
30+
31+
const {disallowConsoleErrors, disallowWarnings} = jest.requireActual(
32+
'relay-test-utils-internal',
33+
) as $FlowFixMe;
34+
35+
disallowWarnings();
36+
disallowConsoleErrors();
37+
38+
describe('useLazyLoadQuery with empty query', () => {
39+
let environment;
40+
let logs: Array<LogEvent>;
41+
let renderFn;
42+
let originalFlagValue;
43+
44+
beforeEach(() => {
45+
originalFlagValue = RelayFeatureFlags.ENABLE_EMPTY_QUERY_CHECK;
46+
RelayFeatureFlags.ENABLE_EMPTY_QUERY_CHECK = true;
47+
48+
logs = [];
49+
environment = new Environment({
50+
network: Network.create(() => {
51+
throw new Error('Network should not be called for empty queries');
52+
}),
53+
store: new Store(new RecordSource(), {gcReleaseBufferSize: 0}),
54+
log: event => {
55+
logs.push(event);
56+
},
57+
});
58+
});
59+
60+
afterEach(() => {
61+
RelayFeatureFlags.ENABLE_EMPTY_QUERY_CHECK = originalFlagValue;
62+
});
63+
64+
it('does not suspend when query is empty due to @skip', () => {
65+
const query = graphql`
66+
query useLazyLoadQueryNodeEmptyQueryTestSkipQuery($skip: Boolean!) {
67+
me @skip(if: $skip) {
68+
id
69+
name
70+
}
71+
}
72+
`;
73+
74+
function Renderer() {
75+
const data = useLazyLoadQuery(
76+
query,
77+
{skip: true},
78+
{fetchPolicy: 'network-only'},
79+
);
80+
return `Data: ${JSON.stringify(data)}`;
81+
}
82+
83+
let instance;
84+
ReactTestRenderer.act(() => {
85+
instance = ReactTestRenderer.create(
86+
<RelayEnvironmentProvider environment={environment}>
87+
<React.Suspense fallback="Loading...">{<Renderer />}</React.Suspense>
88+
</RelayEnvironmentProvider>,
89+
);
90+
});
91+
92+
// Should not suspend - render should complete immediately
93+
expect(instance?.toJSON()).toEqual('Data: {}');
94+
95+
// Should have logged the empty query skip
96+
expect(logs).toContainEqual(
97+
expect.objectContaining({
98+
name: 'execute.skipped',
99+
reason: 'empty',
100+
}),
101+
);
102+
});
103+
104+
it('does not make network request when using usePreloadedQuery with empty query', () => {
105+
const query = graphql`
106+
query useLazyLoadQueryNodeEmptyQueryTestPreloadedQuery($skip: Boolean!) {
107+
me @skip(if: $skip) {
108+
id
109+
name
110+
}
111+
}
112+
`;
113+
114+
const preloadedQuery = loadQuery(
115+
environment,
116+
query,
117+
{skip: true},
118+
{fetchPolicy: 'network-only'},
119+
);
120+
121+
function Renderer() {
122+
const data = usePreloadedQuery(query, preloadedQuery);
123+
return `Data: ${JSON.stringify(data)}`;
124+
}
125+
126+
let instance;
127+
ReactTestRenderer.act(() => {
128+
instance = ReactTestRenderer.create(
129+
<RelayEnvironmentProvider environment={environment}>
130+
<React.Suspense fallback="Loading...">{<Renderer />}</React.Suspense>
131+
</RelayEnvironmentProvider>,
132+
);
133+
});
134+
135+
// Should not suspend - render should complete immediately
136+
expect(instance?.toJSON()).toEqual('Data: {}');
137+
138+
// Should have logged the empty query skip
139+
expect(logs).toContainEqual(
140+
expect.objectContaining({
141+
name: 'execute.skipped',
142+
reason: 'empty',
143+
}),
144+
);
145+
146+
// Dispose the preloaded query
147+
preloadedQuery.dispose();
148+
});
149+
});

0 commit comments

Comments
 (0)