react-relay 0.0.0-main-1e923be9 → 0.0.0-main-d2d2d1f2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ReactRelayContext.js +1 -1
- package/hooks.js +1 -1
- package/index.js +1 -1
- package/legacy.js +1 -1
- package/lib/relay-hooks/react-cache/RelayReactCache.js +37 -0
- package/lib/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js +197 -0
- package/lib/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js +400 -0
- package/lib/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js +45 -0
- package/package.json +2 -2
- package/react-relay-hooks.js +1 -1
- package/react-relay-hooks.min.js +1 -1
- package/react-relay-legacy.js +1 -1
- package/react-relay-legacy.min.js +1 -1
- package/react-relay.js +1 -1
- package/react-relay.min.js +1 -1
- package/relay-hooks/FragmentResource.js.flow +1 -1
- package/relay-hooks/react-cache/RelayReactCache.js.flow +42 -0
- package/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js.flow +243 -0
- package/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js.flow +418 -0
- package/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js.flow +66 -0
@@ -0,0 +1,243 @@
|
|
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 strict-local
|
8
|
+
* @emails oncall+relay
|
9
|
+
* @format
|
10
|
+
*/
|
11
|
+
|
12
|
+
// flowlint ambiguous-object-type:error
|
13
|
+
|
14
|
+
'use strict';
|
15
|
+
|
16
|
+
import type {
|
17
|
+
FetchPolicy,
|
18
|
+
IEnvironment,
|
19
|
+
OperationDescriptor,
|
20
|
+
ReaderFragment,
|
21
|
+
RenderPolicy,
|
22
|
+
} from 'relay-runtime';
|
23
|
+
|
24
|
+
const {getCacheForType, getCacheSignal} = require('./RelayReactCache');
|
25
|
+
const invariant = require('invariant');
|
26
|
+
const {
|
27
|
+
__internal: {fetchQuery: fetchQueryInternal},
|
28
|
+
} = require('relay-runtime');
|
29
|
+
const warning = require('warning');
|
30
|
+
|
31
|
+
type QueryResult = {|
|
32
|
+
fragmentNode: ReaderFragment,
|
33
|
+
fragmentRef: mixed,
|
34
|
+
|};
|
35
|
+
|
36
|
+
// Note that the status of a cache entry will be 'resolved' when partial
|
37
|
+
// rendering is allowed, even if a fetch is ongoing. The pending status
|
38
|
+
// is specifically to indicate that we should suspend.
|
39
|
+
type QueryCacheEntry =
|
40
|
+
| {|status: 'resolved', result: QueryResult|}
|
41
|
+
| {|status: 'pending', promise: Promise<void>|}
|
42
|
+
| {|status: 'rejected', error: Error|};
|
43
|
+
|
44
|
+
type QueryCache = Map<string, QueryCacheEntry>;
|
45
|
+
|
46
|
+
const DEFAULT_FETCH_POLICY = 'store-or-network';
|
47
|
+
|
48
|
+
function createQueryCache(): QueryCache {
|
49
|
+
return new Map();
|
50
|
+
}
|
51
|
+
|
52
|
+
function getQueryCacheKey(
|
53
|
+
operation: OperationDescriptor,
|
54
|
+
fetchPolicy: FetchPolicy,
|
55
|
+
renderPolicy: RenderPolicy,
|
56
|
+
): string {
|
57
|
+
const cacheIdentifier = `${fetchPolicy}-${renderPolicy}-${operation.request.identifier}`;
|
58
|
+
return cacheIdentifier;
|
59
|
+
}
|
60
|
+
|
61
|
+
function constructQueryResult(operation: OperationDescriptor): QueryResult {
|
62
|
+
const rootFragmentRef = {
|
63
|
+
__id: operation.fragment.dataID,
|
64
|
+
__fragments: {
|
65
|
+
[operation.fragment.node.name]: operation.request.variables,
|
66
|
+
},
|
67
|
+
__fragmentOwner: operation.request,
|
68
|
+
};
|
69
|
+
return {
|
70
|
+
fragmentNode: operation.request.node.fragment,
|
71
|
+
fragmentRef: rootFragmentRef,
|
72
|
+
};
|
73
|
+
}
|
74
|
+
|
75
|
+
function getQueryResultOrFetchQuery_REACT_CACHE(
|
76
|
+
environment: IEnvironment,
|
77
|
+
queryOperationDescriptor: OperationDescriptor,
|
78
|
+
fetchPolicy: FetchPolicy = DEFAULT_FETCH_POLICY,
|
79
|
+
maybeRenderPolicy?: RenderPolicy,
|
80
|
+
): QueryResult {
|
81
|
+
const renderPolicy =
|
82
|
+
maybeRenderPolicy ?? environment.UNSTABLE_getDefaultRenderPolicy();
|
83
|
+
|
84
|
+
const cache = getCacheForType(createQueryCache);
|
85
|
+
|
86
|
+
const cacheKey = getQueryCacheKey(
|
87
|
+
queryOperationDescriptor,
|
88
|
+
fetchPolicy,
|
89
|
+
renderPolicy,
|
90
|
+
);
|
91
|
+
|
92
|
+
let entry = cache.get(cacheKey);
|
93
|
+
if (entry === undefined) {
|
94
|
+
// Initiate a query to fetch the data if needed:
|
95
|
+
entry = onCacheMiss(
|
96
|
+
environment,
|
97
|
+
queryOperationDescriptor,
|
98
|
+
fetchPolicy,
|
99
|
+
renderPolicy,
|
100
|
+
newCacheEntry => {
|
101
|
+
cache.set(cacheKey, newCacheEntry);
|
102
|
+
},
|
103
|
+
);
|
104
|
+
cache.set(cacheKey, entry);
|
105
|
+
|
106
|
+
// Since this is the first time rendering, retain the query. React will
|
107
|
+
// trigger the abort signal when this cache entry is no longer needed.
|
108
|
+
const retention = environment.retain(queryOperationDescriptor);
|
109
|
+
const abortSignal = getCacheSignal();
|
110
|
+
abortSignal.addEventListener(
|
111
|
+
'abort',
|
112
|
+
() => {
|
113
|
+
retention.dispose();
|
114
|
+
cache.delete(cacheKey);
|
115
|
+
},
|
116
|
+
{once: true},
|
117
|
+
);
|
118
|
+
}
|
119
|
+
|
120
|
+
switch (entry.status) {
|
121
|
+
case 'pending':
|
122
|
+
throw entry.promise;
|
123
|
+
case 'rejected':
|
124
|
+
throw entry.error;
|
125
|
+
case 'resolved':
|
126
|
+
return entry.result;
|
127
|
+
}
|
128
|
+
invariant(false, 'switch statement should be exhaustive');
|
129
|
+
}
|
130
|
+
|
131
|
+
function onCacheMiss(
|
132
|
+
environment: IEnvironment,
|
133
|
+
operation: OperationDescriptor,
|
134
|
+
fetchPolicy: FetchPolicy,
|
135
|
+
renderPolicy: RenderPolicy,
|
136
|
+
updateCache: QueryCacheEntry => void,
|
137
|
+
): QueryCacheEntry {
|
138
|
+
// NB: Besides checking if the data is available, calling `check` will write missing
|
139
|
+
// data to the store using any missing data handlers specified in the environment.
|
140
|
+
const queryAvailability = environment.check(operation);
|
141
|
+
const queryStatus = queryAvailability.status;
|
142
|
+
const hasFullQuery = queryStatus === 'available';
|
143
|
+
const canPartialRender =
|
144
|
+
hasFullQuery || (renderPolicy === 'partial' && queryStatus !== 'stale');
|
145
|
+
|
146
|
+
let shouldFetch;
|
147
|
+
let shouldRenderNow;
|
148
|
+
switch (fetchPolicy) {
|
149
|
+
case 'store-only': {
|
150
|
+
shouldFetch = false;
|
151
|
+
shouldRenderNow = true;
|
152
|
+
break;
|
153
|
+
}
|
154
|
+
case 'store-or-network': {
|
155
|
+
shouldFetch = !hasFullQuery;
|
156
|
+
shouldRenderNow = canPartialRender;
|
157
|
+
break;
|
158
|
+
}
|
159
|
+
case 'store-and-network': {
|
160
|
+
shouldFetch = true;
|
161
|
+
shouldRenderNow = canPartialRender;
|
162
|
+
break;
|
163
|
+
}
|
164
|
+
case 'network-only':
|
165
|
+
default: {
|
166
|
+
shouldFetch = true;
|
167
|
+
shouldRenderNow = false;
|
168
|
+
break;
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
const promise = shouldFetch
|
173
|
+
? executeOperationAndKeepUpToDate(environment, operation, updateCache)
|
174
|
+
: undefined;
|
175
|
+
if (shouldRenderNow) {
|
176
|
+
return {status: 'resolved', result: constructQueryResult(operation)};
|
177
|
+
} else {
|
178
|
+
invariant(
|
179
|
+
promise,
|
180
|
+
'Should either fetch or render (or both), otherwise we would suspend forever.',
|
181
|
+
);
|
182
|
+
return {status: 'pending', promise: promise};
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
function executeOperationAndKeepUpToDate(
|
187
|
+
environment: IEnvironment,
|
188
|
+
operation: OperationDescriptor,
|
189
|
+
updateCache: QueryCacheEntry => void,
|
190
|
+
): Promise<void> {
|
191
|
+
let resolvePromise;
|
192
|
+
const promise = new Promise(r => {
|
193
|
+
resolvePromise = r;
|
194
|
+
});
|
195
|
+
// $FlowExpectedError[prop-missing] Expando to annotate Promises.
|
196
|
+
promise.displayName = 'Relay(' + operation.request.node.operation.name + ')';
|
197
|
+
|
198
|
+
let isFirstPayload = true;
|
199
|
+
|
200
|
+
// FIXME We may still need to cancel network requests for live queries.
|
201
|
+
const fetchObservable = fetchQueryInternal(environment, operation);
|
202
|
+
fetchObservable.subscribe({
|
203
|
+
start: subscription => {},
|
204
|
+
error: error => {
|
205
|
+
if (isFirstPayload) {
|
206
|
+
updateCache({status: 'rejected', error});
|
207
|
+
} else {
|
208
|
+
// TODO:T92030819 Remove this warning and actually throw the network error
|
209
|
+
// To complete this task we need to have a way of precisely tracking suspendable points
|
210
|
+
warning(
|
211
|
+
false,
|
212
|
+
'getQueryResultOrFetchQuery: An incremental payload for query `%` returned an error: `%`:`%`.',
|
213
|
+
operation.request.node.operation.name,
|
214
|
+
error.message,
|
215
|
+
error.stack,
|
216
|
+
);
|
217
|
+
}
|
218
|
+
resolvePromise();
|
219
|
+
isFirstPayload = false;
|
220
|
+
},
|
221
|
+
next: response => {
|
222
|
+
// Stop suspending on the first payload because of streaming, defer, etc.
|
223
|
+
updateCache({
|
224
|
+
status: 'resolved',
|
225
|
+
result: constructQueryResult(operation),
|
226
|
+
});
|
227
|
+
resolvePromise();
|
228
|
+
isFirstPayload = false;
|
229
|
+
},
|
230
|
+
complete: () => {
|
231
|
+
updateCache({
|
232
|
+
status: 'resolved',
|
233
|
+
result: constructQueryResult(operation),
|
234
|
+
});
|
235
|
+
resolvePromise();
|
236
|
+
isFirstPayload = false;
|
237
|
+
},
|
238
|
+
});
|
239
|
+
|
240
|
+
return promise;
|
241
|
+
}
|
242
|
+
|
243
|
+
module.exports = getQueryResultOrFetchQuery_REACT_CACHE;
|
@@ -0,0 +1,418 @@
|
|
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 strict-local
|
8
|
+
* @emails oncall+relay
|
9
|
+
* @format
|
10
|
+
*/
|
11
|
+
|
12
|
+
// flowlint ambiguous-object-type:error
|
13
|
+
|
14
|
+
'use strict';
|
15
|
+
|
16
|
+
import type {
|
17
|
+
CacheConfig,
|
18
|
+
FetchPolicy,
|
19
|
+
IEnvironment,
|
20
|
+
ReaderFragment,
|
21
|
+
ReaderSelector,
|
22
|
+
SelectorData,
|
23
|
+
Snapshot,
|
24
|
+
} from 'relay-runtime';
|
25
|
+
import type {MissingClientEdgeRequestInfo} from 'relay-runtime/store/RelayStoreTypes';
|
26
|
+
|
27
|
+
const useRelayEnvironment = require('../useRelayEnvironment');
|
28
|
+
const getQueryResultOrFetchQuery = require('./getQueryResultOrFetchQuery_REACT_CACHE');
|
29
|
+
const invariant = require('invariant');
|
30
|
+
const {useDebugValue, useEffect, useMemo, useRef, useState} = require('react');
|
31
|
+
const {
|
32
|
+
areEqualSelectors,
|
33
|
+
createOperationDescriptor,
|
34
|
+
getPendingOperationsForFragment,
|
35
|
+
getSelector,
|
36
|
+
getVariablesFromFragment,
|
37
|
+
recycleNodesInto,
|
38
|
+
reportMissingRequiredFields,
|
39
|
+
} = require('relay-runtime');
|
40
|
+
|
41
|
+
type FragmentQueryOptions = {|
|
42
|
+
fetchPolicy?: FetchPolicy,
|
43
|
+
networkCacheConfig?: CacheConfig,
|
44
|
+
|};
|
45
|
+
|
46
|
+
type FragmentState = $ReadOnly<
|
47
|
+
| {|kind: 'bailout'|}
|
48
|
+
| {|kind: 'singular', snapshot: Snapshot, epoch: number|}
|
49
|
+
| {|kind: 'plural', snapshots: $ReadOnlyArray<Snapshot>, epoch: number|},
|
50
|
+
>;
|
51
|
+
|
52
|
+
type StateUpdater<T> = (T | (T => T)) => void;
|
53
|
+
|
54
|
+
function isMissingData(state: FragmentState): boolean {
|
55
|
+
if (state.kind === 'bailout') {
|
56
|
+
return false;
|
57
|
+
} else if (state.kind === 'singular') {
|
58
|
+
return state.snapshot.isMissingData;
|
59
|
+
} else {
|
60
|
+
return state.snapshots.some(s => s.isMissingData);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
function getMissingClientEdges(
|
65
|
+
state: FragmentState,
|
66
|
+
): $ReadOnlyArray<MissingClientEdgeRequestInfo> | null {
|
67
|
+
if (state.kind === 'bailout') {
|
68
|
+
return null;
|
69
|
+
} else if (state.kind === 'singular') {
|
70
|
+
return state.snapshot.missingClientEdges ?? null;
|
71
|
+
} else {
|
72
|
+
let edges = null;
|
73
|
+
for (const snapshot of state.snapshots) {
|
74
|
+
if (snapshot.missingClientEdges) {
|
75
|
+
edges = edges ?? [];
|
76
|
+
for (const edge of snapshot.missingClientEdges) {
|
77
|
+
edges.push(edge);
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}
|
81
|
+
return edges;
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
function reportMissingRequiredFieldsForState(
|
86
|
+
environment: IEnvironment,
|
87
|
+
state: FragmentState,
|
88
|
+
): void {
|
89
|
+
if (state.kind === 'singular') {
|
90
|
+
if (state.snapshot.missingRequiredFields != null) {
|
91
|
+
reportMissingRequiredFields(
|
92
|
+
environment,
|
93
|
+
state.snapshot.missingRequiredFields,
|
94
|
+
);
|
95
|
+
}
|
96
|
+
} else if (state.kind === 'plural') {
|
97
|
+
for (const snapshot of state.snapshots) {
|
98
|
+
if (snapshot.missingRequiredFields != null) {
|
99
|
+
reportMissingRequiredFields(
|
100
|
+
environment,
|
101
|
+
snapshot.missingRequiredFields,
|
102
|
+
);
|
103
|
+
}
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
function handleMissedUpdates(
|
109
|
+
environment: IEnvironment,
|
110
|
+
state: FragmentState,
|
111
|
+
setState: StateUpdater<FragmentState>,
|
112
|
+
): void {
|
113
|
+
if (state.kind === 'bailout') {
|
114
|
+
return;
|
115
|
+
}
|
116
|
+
const currentEpoch = environment.getStore().getEpoch();
|
117
|
+
if (currentEpoch === state.epoch) {
|
118
|
+
return;
|
119
|
+
}
|
120
|
+
// The store has updated since we rendered (without us being subscribed yet),
|
121
|
+
// so check for any updates to the data we're rendering:
|
122
|
+
if (state.kind === 'singular') {
|
123
|
+
const currentSnapshot = environment.lookup(state.snapshot.selector);
|
124
|
+
const updatedData = recycleNodesInto(
|
125
|
+
state.snapshot.data,
|
126
|
+
currentSnapshot.data,
|
127
|
+
);
|
128
|
+
if (updatedData !== state.snapshot.data) {
|
129
|
+
setState({
|
130
|
+
kind: 'singular',
|
131
|
+
snapshot: currentSnapshot,
|
132
|
+
epoch: currentEpoch,
|
133
|
+
});
|
134
|
+
}
|
135
|
+
} else {
|
136
|
+
let updates = null;
|
137
|
+
for (let index = 0; index < state.snapshots.length; index++) {
|
138
|
+
const snapshot = state.snapshots[index];
|
139
|
+
const currentSnapshot = environment.lookup(snapshot.selector);
|
140
|
+
const updatedData = recycleNodesInto(snapshot.data, currentSnapshot.data);
|
141
|
+
if (updatedData !== snapshot.data) {
|
142
|
+
updates =
|
143
|
+
updates === null ? new Array(state.snapshots.length) : updates;
|
144
|
+
updates[index] = snapshot;
|
145
|
+
}
|
146
|
+
}
|
147
|
+
if (updates !== null) {
|
148
|
+
const theUpdates = updates; // preserve flow refinement.
|
149
|
+
setState(existing => {
|
150
|
+
invariant(
|
151
|
+
existing.kind === 'plural',
|
152
|
+
'Cannot go from singular to plural or from bailout to plural.',
|
153
|
+
);
|
154
|
+
const updated = [...existing.snapshots];
|
155
|
+
for (let index = 0; index < theUpdates.length; index++) {
|
156
|
+
const updatedSnapshot = theUpdates[index];
|
157
|
+
if (updatedSnapshot) {
|
158
|
+
updated[index] = updatedSnapshot;
|
159
|
+
}
|
160
|
+
}
|
161
|
+
return {kind: 'plural', snapshots: updated, epoch: currentEpoch};
|
162
|
+
});
|
163
|
+
}
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
function handleMissingClientEdge(
|
168
|
+
environment: IEnvironment,
|
169
|
+
parentFragmentNode: ReaderFragment,
|
170
|
+
parentFragmentRef: mixed,
|
171
|
+
missingClientEdgeRequestInfo: MissingClientEdgeRequestInfo,
|
172
|
+
queryOptions?: FragmentQueryOptions,
|
173
|
+
): void {
|
174
|
+
const originalVariables = getVariablesFromFragment(
|
175
|
+
parentFragmentNode,
|
176
|
+
parentFragmentRef,
|
177
|
+
);
|
178
|
+
const variables = {
|
179
|
+
...originalVariables,
|
180
|
+
id: missingClientEdgeRequestInfo.clientEdgeDestinationID, // TODO should be a reserved name
|
181
|
+
};
|
182
|
+
const queryOperationDescriptor = createOperationDescriptor(
|
183
|
+
missingClientEdgeRequestInfo.request,
|
184
|
+
variables,
|
185
|
+
queryOptions?.networkCacheConfig,
|
186
|
+
);
|
187
|
+
// This may suspend. We don't need to do anything with the results; all we're
|
188
|
+
// doing here is started the query if needed and retaining and releasing it
|
189
|
+
// according to the component mount/suspense cycle; getQueryResultOrFetchQuery
|
190
|
+
// already handles this by itself.
|
191
|
+
getQueryResultOrFetchQuery(
|
192
|
+
environment,
|
193
|
+
queryOperationDescriptor,
|
194
|
+
queryOptions?.fetchPolicy,
|
195
|
+
);
|
196
|
+
}
|
197
|
+
|
198
|
+
function subscribeToSnapshot(
|
199
|
+
environment: IEnvironment,
|
200
|
+
state: FragmentState,
|
201
|
+
setState: StateUpdater<FragmentState>,
|
202
|
+
): () => void {
|
203
|
+
if (state.kind === 'bailout') {
|
204
|
+
return () => {};
|
205
|
+
} else if (state.kind === 'singular') {
|
206
|
+
const disposable = environment.subscribe(state.snapshot, latestSnapshot => {
|
207
|
+
setState({
|
208
|
+
kind: 'singular',
|
209
|
+
snapshot: latestSnapshot,
|
210
|
+
epoch: environment.getStore().getEpoch(),
|
211
|
+
});
|
212
|
+
});
|
213
|
+
return () => {
|
214
|
+
disposable.dispose();
|
215
|
+
};
|
216
|
+
} else {
|
217
|
+
const disposables = state.snapshots.map((snapshot, index) =>
|
218
|
+
environment.subscribe(snapshot, latestSnapshot => {
|
219
|
+
setState(existing => {
|
220
|
+
invariant(
|
221
|
+
existing.kind === 'plural',
|
222
|
+
'Cannot go from singular to plural or from bailout to plural.',
|
223
|
+
);
|
224
|
+
const updated = [...existing.snapshots];
|
225
|
+
updated[index] = latestSnapshot;
|
226
|
+
return {
|
227
|
+
kind: 'plural',
|
228
|
+
snapshots: updated,
|
229
|
+
epoch: environment.getStore().getEpoch(),
|
230
|
+
};
|
231
|
+
});
|
232
|
+
}),
|
233
|
+
);
|
234
|
+
return () => {
|
235
|
+
for (const d of disposables) {
|
236
|
+
d.dispose();
|
237
|
+
}
|
238
|
+
};
|
239
|
+
}
|
240
|
+
}
|
241
|
+
|
242
|
+
function getFragmentState(
|
243
|
+
environment: IEnvironment,
|
244
|
+
fragmentSelector: ?ReaderSelector,
|
245
|
+
): FragmentState {
|
246
|
+
if (fragmentSelector == null) {
|
247
|
+
return {kind: 'bailout'};
|
248
|
+
} else if (fragmentSelector.kind === 'PluralReaderSelector') {
|
249
|
+
return {
|
250
|
+
kind: 'plural',
|
251
|
+
snapshots: fragmentSelector.selectors.map(s => environment.lookup(s)),
|
252
|
+
epoch: environment.getStore().getEpoch(),
|
253
|
+
};
|
254
|
+
} else {
|
255
|
+
return {
|
256
|
+
kind: 'singular',
|
257
|
+
snapshot: environment.lookup(fragmentSelector),
|
258
|
+
epoch: environment.getStore().getEpoch(),
|
259
|
+
};
|
260
|
+
}
|
261
|
+
}
|
262
|
+
|
263
|
+
// fragmentNode cannot change during the lifetime of the component, though fragmentRef may change.
|
264
|
+
function useFragmentInternal_REACT_CACHE(
|
265
|
+
fragmentNode: ReaderFragment,
|
266
|
+
fragmentRef: mixed,
|
267
|
+
hookDisplayName: string,
|
268
|
+
queryOptions?: FragmentQueryOptions,
|
269
|
+
fragmentKey?: string,
|
270
|
+
): {|
|
271
|
+
data: ?SelectorData | Array<?SelectorData>,
|
272
|
+
disableStoreUpdates: () => void,
|
273
|
+
enableStoreUpdates: () => void,
|
274
|
+
|} {
|
275
|
+
const fragmentSelector = getSelector(fragmentNode, fragmentRef);
|
276
|
+
|
277
|
+
if (fragmentNode?.metadata?.plural === true) {
|
278
|
+
invariant(
|
279
|
+
Array.isArray(fragmentRef),
|
280
|
+
'Relay: Expected fragment pointer%s for fragment `%s` to be ' +
|
281
|
+
'an array, instead got `%s`. Remove `@relay(plural: true)` ' +
|
282
|
+
'from fragment `%s` to allow the prop to be an object.',
|
283
|
+
fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
|
284
|
+
fragmentNode.name,
|
285
|
+
typeof fragmentRef,
|
286
|
+
fragmentNode.name,
|
287
|
+
);
|
288
|
+
} else {
|
289
|
+
invariant(
|
290
|
+
!Array.isArray(fragmentRef),
|
291
|
+
'Relay: Expected fragment pointer%s for fragment `%s` not to be ' +
|
292
|
+
'an array, instead got `%s`. Add `@relay(plural: true)` ' +
|
293
|
+
'to fragment `%s` to allow the prop to be an array.',
|
294
|
+
fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
|
295
|
+
fragmentNode.name,
|
296
|
+
typeof fragmentRef,
|
297
|
+
fragmentNode.name,
|
298
|
+
);
|
299
|
+
}
|
300
|
+
invariant(
|
301
|
+
fragmentRef == null || fragmentSelector != null,
|
302
|
+
'Relay: Expected to receive an object where `...%s` was spread, ' +
|
303
|
+
'but the fragment reference was not found`. This is most ' +
|
304
|
+
'likely the result of:\n' +
|
305
|
+
"- Forgetting to spread `%s` in `%s`'s parent's fragment.\n" +
|
306
|
+
'- Conditionally fetching `%s` but unconditionally passing %s prop ' +
|
307
|
+
'to `%s`. If the parent fragment only fetches the fragment conditionally ' +
|
308
|
+
'- with e.g. `@include`, `@skip`, or inside a `... on SomeType { }` ' +
|
309
|
+
'spread - then the fragment reference will not exist. ' +
|
310
|
+
'In this case, pass `null` if the conditions for evaluating the ' +
|
311
|
+
'fragment are not met (e.g. if the `@include(if)` value is false.)',
|
312
|
+
fragmentNode.name,
|
313
|
+
fragmentNode.name,
|
314
|
+
hookDisplayName,
|
315
|
+
fragmentNode.name,
|
316
|
+
fragmentKey == null ? 'a fragment reference' : `the \`${fragmentKey}\``,
|
317
|
+
hookDisplayName,
|
318
|
+
);
|
319
|
+
|
320
|
+
const environment = useRelayEnvironment();
|
321
|
+
const [rawState, setState] = useState<FragmentState>(() =>
|
322
|
+
getFragmentState(environment, fragmentSelector),
|
323
|
+
);
|
324
|
+
|
325
|
+
const [previousFragmentSelector, setPreviousFragmentSelector] =
|
326
|
+
useState(fragmentSelector);
|
327
|
+
if (!areEqualSelectors(fragmentSelector, previousFragmentSelector)) {
|
328
|
+
setPreviousFragmentSelector(fragmentSelector);
|
329
|
+
setState(getFragmentState(environment, fragmentSelector));
|
330
|
+
}
|
331
|
+
|
332
|
+
let state;
|
333
|
+
if (fragmentRef == null) {
|
334
|
+
state = {kind: 'bailout'};
|
335
|
+
} else if (rawState.kind === 'plural' && rawState.snapshots.length === 0) {
|
336
|
+
state = {kind: 'bailout'};
|
337
|
+
} else {
|
338
|
+
state = rawState;
|
339
|
+
}
|
340
|
+
|
341
|
+
// Handle the queries for any missing client edges; this may suspend.
|
342
|
+
// FIXME handle client edges in parallel.
|
343
|
+
const missingClientEdges = getMissingClientEdges(state);
|
344
|
+
if (missingClientEdges?.length) {
|
345
|
+
for (const edge of missingClientEdges) {
|
346
|
+
handleMissingClientEdge(
|
347
|
+
environment,
|
348
|
+
fragmentNode,
|
349
|
+
fragmentRef,
|
350
|
+
edge,
|
351
|
+
queryOptions,
|
352
|
+
);
|
353
|
+
}
|
354
|
+
}
|
355
|
+
|
356
|
+
if (isMissingData(state)) {
|
357
|
+
// Suspend if an active operation bears on this fragment, either the
|
358
|
+
// fragment's owner or some other mutation etc. that could affect it:
|
359
|
+
invariant(fragmentSelector != null, 'refinement, see invariants above');
|
360
|
+
const fragmentOwner =
|
361
|
+
fragmentSelector.kind === 'PluralReaderSelector'
|
362
|
+
? fragmentSelector.selectors[0].owner
|
363
|
+
: fragmentSelector.owner;
|
364
|
+
const pendingOperationsResult = getPendingOperationsForFragment(
|
365
|
+
environment,
|
366
|
+
fragmentNode,
|
367
|
+
fragmentOwner,
|
368
|
+
);
|
369
|
+
if (pendingOperationsResult) {
|
370
|
+
throw pendingOperationsResult.promise;
|
371
|
+
}
|
372
|
+
// Report required fields only if we're not suspending, since that means
|
373
|
+
// they're missing even though we are out of options for possibly fetching them:
|
374
|
+
reportMissingRequiredFieldsForState(environment, state);
|
375
|
+
}
|
376
|
+
|
377
|
+
// Subscriptions:
|
378
|
+
const isMountedRef = useRef(false);
|
379
|
+
const isListeningForUpdatesRef = useRef(true);
|
380
|
+
function enableStoreUpdates() {
|
381
|
+
isListeningForUpdatesRef.current = true;
|
382
|
+
handleMissedUpdates(environment, state, setState);
|
383
|
+
}
|
384
|
+
function disableStoreUpdates() {
|
385
|
+
isListeningForUpdatesRef.current = false;
|
386
|
+
}
|
387
|
+
useEffect(() => {
|
388
|
+
const wasAlreadySubscribed = isMountedRef.current;
|
389
|
+
isMountedRef.current = true;
|
390
|
+
if (!wasAlreadySubscribed) {
|
391
|
+
handleMissedUpdates(environment, state, setState);
|
392
|
+
}
|
393
|
+
return subscribeToSnapshot(environment, state, setState);
|
394
|
+
}, [environment, state]);
|
395
|
+
|
396
|
+
const data = useMemo(
|
397
|
+
() =>
|
398
|
+
state.kind === 'bailout'
|
399
|
+
? {}
|
400
|
+
: state.kind === 'singular'
|
401
|
+
? state.snapshot.data
|
402
|
+
: state.snapshots.map(s => s.data),
|
403
|
+
[state],
|
404
|
+
);
|
405
|
+
|
406
|
+
if (__DEV__) {
|
407
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
408
|
+
useDebugValue({fragment: fragmentNode.name, data});
|
409
|
+
}
|
410
|
+
|
411
|
+
return {
|
412
|
+
data,
|
413
|
+
disableStoreUpdates,
|
414
|
+
enableStoreUpdates,
|
415
|
+
};
|
416
|
+
}
|
417
|
+
|
418
|
+
module.exports = useFragmentInternal_REACT_CACHE;
|