diff --git a/packages/react-relay/relay-hooks/__tests__/__generated__/useFragmentWithRequiredTestQuery.graphql.js b/packages/react-relay/relay-hooks/__tests__/__generated__/useFragmentWithRequiredTestQuery.graphql.js new file mode 100644 index 0000000000000..b712fb7ca1959 --- /dev/null +++ b/packages/react-relay/relay-hooks/__tests__/__generated__/useFragmentWithRequiredTestQuery.graphql.js @@ -0,0 +1,150 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { useFragmentWithRequiredTestUserFragment$fragmentType } from "./useFragmentWithRequiredTestUserFragment.graphql"; +export type useFragmentWithRequiredTestQuery$variables = {| + id: string, +|}; +export type useFragmentWithRequiredTestQuery$data = {| + +node: ?{| + +$fragmentSpreads: useFragmentWithRequiredTestUserFragment$fragmentType, + |}, +|}; +export type useFragmentWithRequiredTestQuery = {| + response: useFragmentWithRequiredTestQuery$data, + variables: useFragmentWithRequiredTestQuery$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "useFragmentWithRequiredTestQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "useFragmentWithRequiredTestUserFragment" + } + ], + "type": "User", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "useFragmentWithRequiredTestQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "d92c3958e46586a5b24ead47bb7f2a33", + "id": null, + "metadata": {}, + "name": "useFragmentWithRequiredTestQuery", + "operationKind": "query", + "text": "query useFragmentWithRequiredTestQuery(\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ... on User {\n ...useFragmentWithRequiredTestUserFragment\n }\n id\n }\n}\n\nfragment useFragmentWithRequiredTestUserFragment on User {\n name\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "66a4cfb191113d8dc82023073e6a8884"; +} + +module.exports = ((node/*: any*/)/*: Query< + useFragmentWithRequiredTestQuery$variables, + useFragmentWithRequiredTestQuery$data, +>*/); diff --git a/packages/react-relay/relay-hooks/__tests__/__generated__/useFragmentWithRequiredTestUserFragment.graphql.js b/packages/react-relay/relay-hooks/__tests__/__generated__/useFragmentWithRequiredTestUserFragment.graphql.js new file mode 100644 index 0000000000000..e4b543c29f6cf --- /dev/null +++ b/packages/react-relay/relay-hooks/__tests__/__generated__/useFragmentWithRequiredTestUserFragment.graphql.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<262ea8ff7a0a3ff34d7fa1f80f2cea04>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +declare export opaque type useFragmentWithRequiredTestUserFragment$fragmentType: FragmentType; +export type useFragmentWithRequiredTestUserFragment$data = ?{| + +name: string, + +$fragmentType: useFragmentWithRequiredTestUserFragment$fragmentType, +|}; +export type useFragmentWithRequiredTestUserFragment$key = { + +$data?: useFragmentWithRequiredTestUserFragment$data, + +$fragmentSpreads: useFragmentWithRequiredTestUserFragment$fragmentType, + ... +}; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "useFragmentWithRequiredTestUserFragment", + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + "action": "LOG", + "path": "name" + } + ], + "type": "User", + "abstractKey": null +}; + +if (__DEV__) { + (node/*: any*/).hash = "9e3297104a693133e2546618d76ce8d8"; +} + +module.exports = ((node/*: any*/)/*: Fragment< + useFragmentWithRequiredTestUserFragment$fragmentType, + useFragmentWithRequiredTestUserFragment$data, +>*/); diff --git a/packages/react-relay/relay-hooks/__tests__/useFragment-with-required-test.js b/packages/react-relay/relay-hooks/__tests__/useFragment-with-required-test.js new file mode 100644 index 0000000000000..54225264d10c5 --- /dev/null +++ b/packages/react-relay/relay-hooks/__tests__/useFragment-with-required-test.js @@ -0,0 +1,122 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + * @oncall relay + */ + +'use strict'; +import type {MutableRecordSource} from 'relay-runtime/store/RelayStoreTypes'; +import type {RequiredFieldLoggerEvent} from 'relay-runtime/store/RelayStoreTypes'; + +const useFragmentOriginal_REACT_CACHE = require('../react-cache/useFragment_REACT_CACHE'); +const RelayEnvironmentProvider = require('../RelayEnvironmentProvider'); +const useFragmentOriginal_LEGACY = require('../useFragment'); +const useLazyLoadQuery = require('../useLazyLoadQuery'); +const React = require('react'); +const TestRenderer = require('react-test-renderer'); +const {graphql} = require('relay-runtime'); +const RelayNetwork = require('relay-runtime/network/RelayNetwork'); +const LiveResolverStore = require('relay-runtime/store/experimental-live-resolvers/LiveResolverStore'); +const RelayModernEnvironment = require('relay-runtime/store/RelayModernEnvironment'); +const RelayRecordSource = require('relay-runtime/store/RelayRecordSource'); +const { + disallowConsoleErrors, + disallowWarnings, +} = require('relay-test-utils-internal'); + +disallowWarnings(); +disallowConsoleErrors(); + +describe.each([ + ['React Cache', useFragmentOriginal_REACT_CACHE], + ['Legacy', useFragmentOriginal_LEGACY], +])('useFragment (%s)', (_hookName, useFragmentOriginal) => { + test('@required(action: LOG) gets logged even if no data is "missing"', () => { + function InnerTestComponent({id}: {id: string}) { + const data = useLazyLoadQuery( + graphql` + query useFragmentWithRequiredTestQuery($id: ID!) { + node(id: $id) { + ... on User { + ...useFragmentWithRequiredTestUserFragment + } + } + } + `, + {id}, + {fetchPolicy: 'store-only'}, + ); + const user = useFragmentOriginal( + graphql` + fragment useFragmentWithRequiredTestUserFragment on User { + name @required(action: LOG) + } + `, + data.node, + ); + return `${user?.name ?? 'Unknown name'}`; + } + + function TestComponent({ + environment, + ...rest + }: { + environment: RelayModernEnvironment, + id: string, + }) { + return ( + + + + + + ); + } + const requiredFieldLogger = jest.fn< + $FlowFixMe | [RequiredFieldLoggerEvent], + void, + >(); + function createEnvironment(source: MutableRecordSource) { + return new RelayModernEnvironment({ + network: RelayNetwork.create(jest.fn()), + store: new LiveResolverStore(source), + requiredFieldLogger, + }); + } + + const source = RelayRecordSource.create({ + 'client:root': { + __id: 'client:root', + __typename: '__Root', + 'node(id:"1")': {__ref: '1'}, + }, + '1': { + __id: '1', + __typename: 'User', + name: null, + }, + }); + const environment = createEnvironment(source); + + const renderer = TestRenderer.create( + , + ); + + // Validate that the missing required field was logged. + expect(requiredFieldLogger.mock.calls).toEqual([ + [ + { + fieldPath: 'name', + kind: 'missing_field.log', + owner: 'useFragmentWithRequiredTestUserFragment', + }, + ], + ]); + expect(renderer.toJSON()).toEqual('Unknown name'); + }); +}); diff --git a/packages/react-relay/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js b/packages/react-relay/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js index 308de3587bdc5..2837bd65b9524 100644 --- a/packages/react-relay/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js +++ b/packages/react-relay/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js @@ -506,11 +506,12 @@ function useFragmentInternal_REACT_CACHE( throw pendingOperationsResult.promise; } } - // Report required fields only if we're not suspending, since that means - // they're missing even though we are out of options for possibly fetching them: - handlePotentialSnapshotErrorsForState(environment, state); } + // Report required fields only if we're not suspending, since that means + // they're missing even though we are out of options for possibly fetching them: + handlePotentialSnapshotErrorsForState(environment, state); + useEffect(() => { // Check for updates since the state was rendered let currentState = subscribedState;