From 356bc78ccb014895b3c51e7bfd700bd16f1b55a9 Mon Sep 17 00:00:00 2001 From: Andrey Lunyov Date: Thu, 24 Aug 2023 13:20:18 -0700 Subject: [PATCH] Docs v15.0.0 Reviewed By: voideanvalue Differential Revision: D48648509 fbshipit-source-id: 728c3f4e2ab79c69502a9d870d26da5073fc1a08 --- .../entrypoint-apis/entrypoint-container.md | 38 + .../entrypoint-apis/load-entrypoint.md | 77 ++ .../entrypoint-apis/use-entrypoint-loader.md | 99 ++ .../graphql/graphql-directives.md | 218 ++++ .../api-reference/hooks/load-query.md | 84 ++ .../hooks/relay-environment-provider.md | 78 ++ .../api-reference/hooks/use-client-query.md | 65 ++ .../api-reference/hooks/use-fragment.md | 69 ++ .../hooks/use-lazy-load-query.md | 77 ++ .../api-reference/hooks/use-mutation.md | 94 ++ .../hooks/use-pagination-fragment.md | 161 +++ .../hooks/use-preloaded-query.md | 84 ++ .../api-reference/hooks/use-query-loader.md | 95 ++ .../hooks/use-refetchable-fragment.md | 121 +++ .../hooks/use-relay-environment.md | 37 + .../api-reference/hooks/use-subscription.md | 66 ++ .../api-reference/legacy-apis/legacy-apis.md | 18 + .../relay-runtime/commit-mutation.md | 65 ++ .../relay-runtime/fetch-query.md | 111 ++ .../relay-runtime/request-subscription.md | 55 + .../api-reference/relay-runtime/store.md | 590 +++++++++++ .../api-reference/types/CacheConfig.md | 8 + .../api-reference/types/Disposable.md | 4 + .../types/GraphQLSubscriptionConfig.md | 17 + .../api-reference/types/MutationConfig.md | 31 + .../types/SelectorStoreUpdater.md | 6 + .../api-reference/types/UploadableMap.md | 3 + .../community/learning-resources.md | 34 + .../declarative-mutation-directives.md | 34 + .../debugging/disallowed-id-types-error.md | 43 + .../debugging/inconsistent-typename-error.md | 45 + .../debugging/relay-devtools.md | 73 ++ .../version-v15.0.0/debugging/why-null.md | 116 +++ .../version-v15.0.0/editor-support.md | 55 + .../error-reference/unknown-field.md | 36 + .../getting-started/installation-and-setup.md | 150 +++ .../getting-started/prerequisites.md | 49 + .../getting-started/step-by-step-guide.md | 314 ++++++ .../version-v15.0.0/glossary/glossary.md | 967 ++++++++++++++++++ .../guided-tour/introduction.md | 57 ++ .../list-data/advanced-pagination.md | 200 ++++ .../guided-tour/list-data/connections.md | 23 + .../guided-tour/list-data/pagination.md | 141 +++ .../list-data/refetching-connections.md | 210 ++++ .../list-data/rendering-connections.md | 112 ++ .../list-data/streaming-pagination.md | 87 ++ .../list-data/updating-connections.md | 603 +++++++++++ .../prefetching-queries.md | 10 + .../reading-fragments.md | 12 + .../reading-queries.md | 12 + .../retaining-queries.md | 51 + .../subscribing-to-queries.md | 12 + .../refetching/OssAvoidSuspenseNote.md | 3 + .../guided-tour/refetching/introduction.md | 17 + ...efetching-fragments-with-different-data.md | 171 ++++ .../refetching-queries-with-different-data.md | 344 +++++++ .../refetching/refreshing-fragments.md | 191 ++++ .../refetching/refreshing-queries.md | 357 +++++++ .../guided-tour/rendering/environment.md | 59 ++ .../guided-tour/rendering/error-states.md | 295 ++++++ .../guided-tour/rendering/fragments.md | 354 +++++++ .../guided-tour/rendering/loading-states.md | 257 +++++ .../guided-tour/rendering/queries.md | 261 +++++ .../guided-tour/rendering/variables.md | 233 +++++ .../availability-of-data.md | 18 + .../reusing-cached-data/fetch-policies.md | 56 + .../filling-in-missing-data.md | 102 ++ .../reusing-cached-data/introduction.md | 22 + .../reusing-cached-data/presence-of-data.md | 93 ++ .../rendering-partially-cached-data.md | 175 ++++ .../reusing-cached-data/staleness-of-data.md | 116 +++ .../updating-data/client-only-data.md | 115 +++ .../updating-data/graphql-mutations.md | 378 +++++++ .../updating-data/graphql-subscriptions.md | 279 +++++ .../imperatively-modifying-linked-fields.md | 521 ++++++++++ ...mperatively-modifying-store-data-legacy.md | 142 +++ .../imperatively-modifying-store-data.md | 275 +++++ .../guided-tour/updating-data/introduction.md | 24 + .../updating-data/local-data-updates.md | 71 ++ .../updating-data/typesafe-updaters-faq.md | 95 ++ .../version-v15.0.0/guided-tour/workflow.md | 39 + .../guides/client-schema-extensions.md | 207 ++++ .../version-v15.0.0/guides/compiler.md | 172 ++++ .../guides/graphql-server-specification.md | 447 ++++++++ .../version-v15.0.0/guides/network-layer.md | 74 ++ .../guides/persisted-queries.md | 326 ++++++ .../version-v15.0.0/guides/relay-resolvers.md | 249 +++++ .../guides/required-directive.md | 234 +++++ .../guides/testing-relay-components.md | 584 +++++++++++ .../testing-relay-with-preloaded-queries.md | 163 +++ .../version-v15.0.0/guides/type-emission.md | 414 ++++++++ .../versioned_docs/version-v15.0.0/home.md | 65 ++ .../relay-hooks-and-legacy-container-apis.md | 564 ++++++++++ .../suspense-compatibility.md | 36 + .../upgrading-to-relay-hooks.md | 38 + .../architecture-overview.md | 24 + .../compiler-architecture.md | 106 ++ .../runtime-architecture.md | 249 +++++ .../thinking-in-graphql.md | 309 ++++++ .../thinking-in-relay.md | 104 ++ .../principles-and-architecture/videos.md | 50 + .../version-v15.0.0/tutorial/arrays-lists.md | 126 +++ .../tutorial/connections-pagination.md | 488 +++++++++ .../version-v15.0.0/tutorial/fragments-1.md | 485 +++++++++ .../version-v15.0.0/tutorial/graphql.md | 172 ++++ .../tutorial/interfaces-polymorphism.md | 161 +++ .../version-v15.0.0/tutorial/intro.md | 56 + .../tutorial/mutations-updates.md | 619 +++++++++++ .../version-v15.0.0/tutorial/queries-1.md | 267 +++++ .../version-v15.0.0/tutorial/queries-2.md | 389 +++++++ .../tutorial/refetchable-fragments.md | 350 +++++++ .../version-v15.0.0-sidebars.json | 124 +++ website/versions.json | 1 + 113 files changed, 18533 insertions(+) create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/entrypoint-container.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/load-entrypoint.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/use-entrypoint-loader.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/graphql/graphql-directives.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/load-query.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/relay-environment-provider.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/use-client-query.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/use-fragment.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/use-lazy-load-query.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/use-mutation.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/use-pagination-fragment.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/use-preloaded-query.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/use-query-loader.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/use-refetchable-fragment.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/use-relay-environment.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/hooks/use-subscription.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/legacy-apis/legacy-apis.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/commit-mutation.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/fetch-query.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/request-subscription.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/store.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/types/CacheConfig.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/types/Disposable.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/types/GraphQLSubscriptionConfig.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/types/MutationConfig.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/types/SelectorStoreUpdater.md create mode 100644 website/versioned_docs/version-v15.0.0/api-reference/types/UploadableMap.md create mode 100644 website/versioned_docs/version-v15.0.0/community/learning-resources.md create mode 100644 website/versioned_docs/version-v15.0.0/debugging/declarative-mutation-directives.md create mode 100644 website/versioned_docs/version-v15.0.0/debugging/disallowed-id-types-error.md create mode 100644 website/versioned_docs/version-v15.0.0/debugging/inconsistent-typename-error.md create mode 100644 website/versioned_docs/version-v15.0.0/debugging/relay-devtools.md create mode 100644 website/versioned_docs/version-v15.0.0/debugging/why-null.md create mode 100644 website/versioned_docs/version-v15.0.0/editor-support.md create mode 100644 website/versioned_docs/version-v15.0.0/error-reference/unknown-field.md create mode 100644 website/versioned_docs/version-v15.0.0/getting-started/installation-and-setup.md create mode 100644 website/versioned_docs/version-v15.0.0/getting-started/prerequisites.md create mode 100644 website/versioned_docs/version-v15.0.0/getting-started/step-by-step-guide.md create mode 100644 website/versioned_docs/version-v15.0.0/glossary/glossary.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/introduction.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/list-data/advanced-pagination.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/list-data/connections.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/list-data/pagination.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/list-data/refetching-connections.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/list-data/rendering-connections.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/list-data/streaming-pagination.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/list-data/updating-connections.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/prefetching-queries.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/reading-fragments.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/reading-queries.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/retaining-queries.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/subscribing-to-queries.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/refetching/OssAvoidSuspenseNote.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/refetching/introduction.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/refetching/refetching-fragments-with-different-data.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/refetching/refetching-queries-with-different-data.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/refetching/refreshing-fragments.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/refetching/refreshing-queries.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/rendering/environment.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/rendering/error-states.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/rendering/fragments.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/rendering/loading-states.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/rendering/queries.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/rendering/variables.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/availability-of-data.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/fetch-policies.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/filling-in-missing-data.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/introduction.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/presence-of-data.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/rendering-partially-cached-data.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/staleness-of-data.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/updating-data/client-only-data.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/updating-data/graphql-mutations.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/updating-data/graphql-subscriptions.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-linked-fields.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-store-data-legacy.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-store-data.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/updating-data/introduction.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/updating-data/local-data-updates.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/updating-data/typesafe-updaters-faq.md create mode 100644 website/versioned_docs/version-v15.0.0/guided-tour/workflow.md create mode 100644 website/versioned_docs/version-v15.0.0/guides/client-schema-extensions.md create mode 100644 website/versioned_docs/version-v15.0.0/guides/compiler.md create mode 100644 website/versioned_docs/version-v15.0.0/guides/graphql-server-specification.md create mode 100644 website/versioned_docs/version-v15.0.0/guides/network-layer.md create mode 100644 website/versioned_docs/version-v15.0.0/guides/persisted-queries.md create mode 100644 website/versioned_docs/version-v15.0.0/guides/relay-resolvers.md create mode 100644 website/versioned_docs/version-v15.0.0/guides/required-directive.md create mode 100644 website/versioned_docs/version-v15.0.0/guides/testing-relay-components.md create mode 100644 website/versioned_docs/version-v15.0.0/guides/testing-relay-with-preloaded-queries.md create mode 100644 website/versioned_docs/version-v15.0.0/guides/type-emission.md create mode 100644 website/versioned_docs/version-v15.0.0/home.md create mode 100644 website/versioned_docs/version-v15.0.0/migration-and-compatibility/relay-hooks-and-legacy-container-apis.md create mode 100644 website/versioned_docs/version-v15.0.0/migration-and-compatibility/suspense-compatibility.md create mode 100644 website/versioned_docs/version-v15.0.0/migration-and-compatibility/upgrading-to-relay-hooks.md create mode 100644 website/versioned_docs/version-v15.0.0/principles-and-architecture/architecture-overview.md create mode 100644 website/versioned_docs/version-v15.0.0/principles-and-architecture/compiler-architecture.md create mode 100644 website/versioned_docs/version-v15.0.0/principles-and-architecture/runtime-architecture.md create mode 100644 website/versioned_docs/version-v15.0.0/principles-and-architecture/thinking-in-graphql.md create mode 100644 website/versioned_docs/version-v15.0.0/principles-and-architecture/thinking-in-relay.md create mode 100644 website/versioned_docs/version-v15.0.0/principles-and-architecture/videos.md create mode 100644 website/versioned_docs/version-v15.0.0/tutorial/arrays-lists.md create mode 100644 website/versioned_docs/version-v15.0.0/tutorial/connections-pagination.md create mode 100644 website/versioned_docs/version-v15.0.0/tutorial/fragments-1.md create mode 100644 website/versioned_docs/version-v15.0.0/tutorial/graphql.md create mode 100644 website/versioned_docs/version-v15.0.0/tutorial/interfaces-polymorphism.md create mode 100644 website/versioned_docs/version-v15.0.0/tutorial/intro.md create mode 100644 website/versioned_docs/version-v15.0.0/tutorial/mutations-updates.md create mode 100644 website/versioned_docs/version-v15.0.0/tutorial/queries-1.md create mode 100644 website/versioned_docs/version-v15.0.0/tutorial/queries-2.md create mode 100644 website/versioned_docs/version-v15.0.0/tutorial/refetchable-fragments.md create mode 100644 website/versioned_sidebars/version-v15.0.0-sidebars.json diff --git a/website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/entrypoint-container.md b/website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/entrypoint-container.md new file mode 100644 index 0000000000000..77c2a3e44a451 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/entrypoint-container.md @@ -0,0 +1,38 @@ +--- +id: entrypoint-container +title: EntryPointContainer +slug: /api-reference/entrypoint-container/ +description: API reference for EntryPointContainer, a React component used to render the root component of an entrypoint +keywords: + - entrypoint + - container + - root +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +## `EntryPointContainer` + + + +For more information, see the [Defining EntryPoints](../../guides/entrypoints/using-entrypoints/#defining-entrypoints) and [Consuming EntryPoints](../../guides/entrypoints/using-entrypoints/#-entrypoints) guides. + + + +```js +function EntryPointContainer({ + entryPointReference, + props, +}: { + +entryPointReference: PreloadedEntryPoint, + +props: TRuntimeProps, +}): ReactElement +``` + +A React component that renders a preloaded EntryPoint. + +* `entryPointReference`: the value returned from a call to `loadEntryPoint` or acquired from the `useEntryPointLoader` hook. +* `props`: additional runtime props that will be passed to the `Component` + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/load-entrypoint.md b/website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/load-entrypoint.md new file mode 100644 index 0000000000000..66c819c8b7d0e --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/load-entrypoint.md @@ -0,0 +1,77 @@ +--- +id: load-entrypoint +title: loadEntryPoint +slug: /api-reference/load-entrypoint/ +description: API reference for loadEntryPoint, which imperatively loads an entrypoint and data for its queries +keywords: + - entrypoint + - preload + - render-as-you-fetch +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +## `loadEntryPoint` + +This function is designed to be used with `EntryPointContainer` to implement the "render-as-you-fetch" pattern. + +EntryPoint references returned from `loadEntryPoint` will leak data to the Relay store (if they have associated queries) unless `.dispose()` is called on them once they are no longer referenced. As such, prefer using `useEntryPointLoader` when possible, which ensures that EntryPoint references are correctly disposed for you. See the [`useEntryPointLoader`](../use-entrypoint-loader) docs for a more complete example. + + + +For more information, see the [Loading EntryPoints](../../guides/entrypoints/using-entrypoints/#loading-entrypoints) guide. + + + +```js +const EntryPoint = require('MyComponent.entrypoint.js'); + +const {loadQuery} = require('react-relay'); + +// Generally, your component should access the environment from the React context, +// and pass that environment to this function. +const getEntrypointReference = environment => loadEntryPoint( + { getEnvironment: () => environment }, + EntryPoint, + {id: '4'}, +); + +// later: pass entryPointReference to EntryPointContainer +// Note that EntryPoint references should have .dispose() called on them, +// which is missing in this example. +``` + +### Arguments + +* `environmentProvider`: A provider for a Relay Environment instance on which to execute the request. If you're starting this request somewhere within a React component, you probably want to use the environment you obtain from using [`useRelayEnvironment`](../use-relay-environment/). +* `EntryPoint`: EntryPoint to load. +* `entryPointParams`: Parameters that will be passed to the EntryPoint's `getPreloadProps` method. + +### Flow Type Parameters + +* `TEntryPointParams`: Type parameter corresponding to the type of the first parameter of the `getPreloadProps` method of the EntryPoint. +* `TPreloadedQueries`: the type of the `queries` parameter to the EntryPoint component. +* `TPreloadedEntryPoints`: the type of the `entrypoints` parameter passed to the EntryPoint component. +* `TRuntimeProps`: the type of the `props` prop passed to `EntryPointContainer`. This object is passed down to the EntryPoint component, also as `props`. +* `TExtraProps`: if an EntryPoint's `getPreloadProps` method returns an object with an `extraProps` property, those extra props will be passed to the EntryPoint component as `extraProps`. +* `TEntryPointComponent`: the type of the EntryPoint. +* `TEntryPoint`: the type of the EntryPoint. + +### Return Value + +An EntryPoint reference with the following properties: + +* `dispose`: a method that will release any query references loaded by this EntryPoint (including indirectly, by way of other EntryPoints) from being retained by the store. This can cause the data referenced by these query reference to be garbage collected. + +The exact format of the return value is *unstable and highly likely to change*. We strongly recommend not using any other properties of the return value, as such code would be highly likely to break when upgrading to future versions of Relay. Instead, pass the result of `loadEntryPoint()` to `EntryPointContainer`. + +### Behavior + +* When `loadEntryPoint()` is called, each of an EntryPoint's associated queries (if it has any) will load their query data and query AST. Once both the query AST and the data are available, the data will be written to the store. This differs from the behavior of `prepareEntryPoint_DEPRECATED`, which would only write the data from an associated query to the store when that query was rendered with `usePreloadedQuery`. +* The EntryPoint reference's associated query references will be retained by the Relay store, preventing it the data from being garbage collected. Once you call `.dispose()` on the EntryPoint reference, the data from the associated queries is liable to be garbage collected. +* `loadEntryPoint` may throw an error if it is called during React's render phase. + + + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/use-entrypoint-loader.md b/website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/use-entrypoint-loader.md new file mode 100644 index 0000000000000..8b3b04495e537 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/entrypoint-apis/use-entrypoint-loader.md @@ -0,0 +1,99 @@ +--- +id: use-entrypoint-loader +title: useEntryPointLoader +slug: /api-reference/use-entrypoint-loader/ +description: API reference for useEntryPointLoader, a React hook used to load entrypoints in response to user events +keywords: + - render-as-you-fetch + - entrypoint + - preload +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +## `useEntryPointLoader` + +Hook used to make it easy to safely work with EntryPoints, while avoiding data leaking into the Relay store. It will keep an EntryPoint reference in state, and dispose of it when it is no longer accessible via state. + + + +For more information, see the [Loading EntryPoints](https://www.internalfb.com/intern/wiki/Relay/Guides/entry-points/#loading-entrypoints) guide. + + + +```js +const {useEntryPointLoader, EntryPointContainer} = require('react-relay'); + +const ComponentEntryPoint = require('Component.entrypoint'); + +function EntryPointRevealer(): React.MixedElement { + const environmentProvider = useMyEnvironmentProvider(); + const [ + entryPointReference, + loadEntryPoint, + disposeEntryPoint, + ] = useEntryPointLoader(environmentProvider, ComponentEntryPoint); + + return ( + <> + { + entryPointReference == null && ( + + ) + } + { + entryPointReference != null && ( + <> + + + + + + ) + } + + ); +} +``` + +### Arguments + +* `environmentProvider`: an object with a `getEnvironment` method that returns a relay environment. +* `EntryPoint`: the EntryPoint, usually acquired by importing a `.entrypoint.js` file. + +### Flow Type Parameters + +* `TEntryPointParams`: the type of the first argument to the `getPreloadProps` method of the EntryPoint. +* `TPreloadedQueries`: the type of the `queries` prop passed to the EntryPoint component. +* `TPreloadedEntryPoints`: the type of the `entryPoints` prop passed to the EntryPoint component. +* `TRuntimeProps`: the type of the `props` prop passed to `EntryPointContainer`. This object is passed down to the EntryPoint component, also as `props`. +* `TExtraProps`: if an EntryPoint's `getPreloadProps` method returns an object with an `extraProps` property, those extra props will be passed to the EntryPoint component as `extraProps` and have type `TExtraProps`. +* `TEntryPointComponent`: the type of the EntryPoint component. +* `TEntryPoint`: the type of the EntryPoint. + +### Return value + +A tuple containing the following values: + +* `entryPointReference`: the EntryPoint reference, or `null`. +* `loadEntryPoint`: a callback that, when executed, will load an EntryPoint, which will be accessible as `entryPointReference`. If a previous EntryPoint was loaded, it will dispose of it. It may throw an error if called during React's render phase. + * Parameters + * `params: TEntryPointParams`: the params passed to the EntryPoint's `getPreloadProps` method. +* `disposeEntryPoint`: a callback that, when executed, will set `entryPointReference` to `null` and call `.dispose()` on it. It has type `() => void`. It should not be called during React's render phase. + +### Behavior + +* When the `loadEntryPoint` callback is called, each of an EntryPoint's associated queries (if it has any) will load their query data and query AST. Once both the query AST and the data are available, the data will be written to the store. This differs from the behavior of `prepareEntryPoint_DEPRECATED`, which would only write the data from an associated query to the store when that query was rendered with `usePreloadedQuery`. +* The EntryPoint reference's associated query references will be retained by the Relay store, preventing it the data from being garbage collected. Once you call `.dispose()` on the EntryPoint reference, the data from the associated queries is liable to be garbage collected. +* The `loadEntryPoint` callback may throw an error if it is called during React's render phase. + + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/graphql/graphql-directives.md b/website/versioned_docs/version-v15.0.0/api-reference/graphql/graphql-directives.md new file mode 100644 index 0000000000000..4d5dd12edb272 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/graphql/graphql-directives.md @@ -0,0 +1,218 @@ +--- +id: graphql-directives +title: GraphQL Directives +slug: /api-reference/graphql-and-directives/ +description: API Reference for GraphQL directives +keywords: + - GraphQL + - Directive + - arguments + - argumentDefinitions + - connection + - relay + - inline + - provider +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly, OssOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +Relay uses directives to add additional information to GraphQL documents, which are used by the [Relay compiler](../../guides/compiler/) to generate the appropriate runtime artifacts. These directives only appear in your application code and are removed from requests sent to your GraphQL server. + + +**Note:** The Relay compiler will maintain any directives supported by your server (such as `@include` or `@skip`) so they remain part of the request to the GraphQL server and won't alter generated runtime artifacts. + + +**Note:** The Relay compiler will maintain any directives supported by your server (such as `@include` or `@skip`) so they remain part of the request to the GraphQL server and won't alter generated runtime artifacts. Additional directives are documented [here](https://www.internalfb.com/intern/wiki/GraphQL/APIs_and_References/Directives/#graphql-standard). + + +## `@arguments` + +`@arguments` is a directive used to pass arguments to a fragment that was defined using [`@argumentDefinitions`](#argumentdefinitions). For example: + +```graphql +query TodoListQuery($userID: ID) { + ...TodoList_list @arguments(count: $count, userID: $userID) # Pass arguments here +} +``` + +## `@argumentDefinitions` + +`@argumentDefinitions` is a directive used to specify arguments taken by a fragment. For example: + +```graphql +fragment TodoList_list on TodoList @argumentDefinitions( + count: {type: "Int", defaultValue: 10}, # Optional argument + userID: {type: "ID"}, # Required argument +) { + title + todoItems(userID: $userID, first: $count) { # Use fragment arguments here as variables + ...TodoItem_item + } +} +``` + +### Provided Variables +A provided variable is a special fragment variable whose value is supplied by a specified provider function at runtime. This simplifies supplying device attributes, user experiment flags, and other runtime constants to graphql fragments. + +To add a provided variable: +- add an argument with `provider: "[JSModule].relayprovider"` to `@argumentDefinitions` +- ensure that `[JSModule].relayprovider.js` exists and exports a `get()` function + - `get` should return the same value on every call for a given run. +```graphql +fragment TodoItem_item on TodoList +@argumentDefinitions( + include_timestamp: { + type: "Boolean!", + provider: "Todo_ShouldIncludeTimestamp.relayprovider" + }, +) { + timestamp @include(if: $include_timestamp) + text +} +``` + +```javascript +// Todo_ShouldIncludeTimestamp.relayprovider.js +export default { + get(): boolean { + // must always return true or false for a given run + return check('todo_should_include_timestamp'); + }, +}; +``` +Notes: + + + +- Even though fragments declare provided variables in `argumentDefinitions`, their parent cannot pass provided variables through `@arguments`. +- An argument definition cannot specify both a provider and a defaultValue. +- If the modified fragment is included in operations that use hack preloaders (`@preloadable(hackPreloader: true)`), you will need to manually add provided variables when calling `RelayPreloader::gen`. + - Hack's typechecker will fail with `The field __relay_internal__pv__[JsModule] is missing.` + - We strongly encourage switching to [Entrypoints](../../guides/entrypoints/using-entrypoints/) if possible. +- _Unstable / subject to change_ + - Relay transforms provided variables to operation root variables and renames them to `__relay_internal__pv__[JsModule]`. + - Only relevant if you are debugging a query that uses provided variables. + + + + + +- Even though fragments declare provided variables in `argumentDefinitions`, their parent cannot pass provided variables through `@arguments`. +- An argument definition cannot specify both a provider and a defaultValue. +- _Unstable / subject to change_ + - Relay transforms provided variables to operation root variables and renames them to `__relay_internal__pv__[JsModule]`. + - Only relevant if you are debugging a query that uses provided variables. + + + +## `@connection(key: String!, filters: [String])` + +With `usePaginationFragment`, Relay expects connection fields to be annotated with a `@connection` directive. For more detailed information and an example, check out the [docs on `usePaginationFragment`](../../guided-tour/list-data/rendering-connections). + +## `@refetchable(queryName: String!)` + +With `useRefetchableFragment` and `usePaginationFragment`, Relay expects a `@refetchable` directive. The `@refetchable` directive can only be added to fragments that are "refetchable", that is, on fragments that are declared on `Viewer` or `Query` types, or on a type that implements `Node` (i.e. a type that has an id). The `@refetchable` directive will autogenerate a query with the specified `queryName`. This will also generate Flow types for the query, available to import from the generated file: `.graphql.js`. For more detailed information and examples, check out the docs on [`useRefetchableFragment`](../use-refetchable-fragment/) or [`usePaginationFragment`](../use-pagination-fragment/). + +## `@relay(plural: Boolean)` + +When defining a fragment for use with a Fragment container, you can use the `@relay(plural: true)` directive to indicate that container expects the prop for that fragment to be a list of items instead of a single item. A query or parent that spreads a `@relay(plural: true)` fragment should do so within a plural field (ie a field backed by a [GraphQL list](http://graphql.org/learn/schema/#lists-and-non-null). For example: + +```javascript +// Plural fragment definition +graphql` + fragment TodoItems_items on TodoItem @relay(plural: true) { + id + text + } +`; + +// Plural fragment usage: note the parent type is a list of items (`TodoItem[]`) +fragment TodoApp_app on App { + items { + // parent type is a list here + ...TodoItem_items + } +} +``` + +## `@required` + +`@required` is a directive you can add to fields in your Relay queries to declare how null values should be handled at runtime. + +See also [the @required guide](../../guides/required-directive/). + +## `@inline` + +The hooks APIs that Relay exposes allow you to read data from the store only during the render phase. In order to read data from outside of the render phase (or from outside of React), Relay exposes the `@inline` directive. The data from a fragment annotated with `@inline` can be read using `readInlineData`. + +In the example below, the function `processItemData` is called from a React component. It requires an item object with a specific set of fields. All React components that use this function should spread the `processItemData_item` fragment to ensure all of the correct item data is loaded for this function. + +```javascript +import {graphql, readInlineData} from 'react-relay'; + +// non-React function called from React +function processItemData(itemRef) { + const item = readInlineData(graphql` + fragment processItemData_item on Item @inline { + title + price + creator { + name + } + } + `, itemRef); + sendToThirdPartyApi({ + title: item.title, + price: item.price, + creatorName: item.creator.name + }); +} +``` + +```javascript +export default function MyComponent({item}) { + function handleClick() { + processItemData(item); + } + + const data = useFragment( + graphql` + fragment MyComponent_item on Item { + ...processItemData_item + title + } + `, + item + ); + + return ( + + ); +} +``` + +## `@relay(mask: Boolean)` + + It is not recommended to use `@relay(mask: false)`. Please instead consider using the `@inline` fragment. + +`@relay(mask: false)` can be used to prevent data masking; when including a fragment and annotating it with `@relay(mask: false)`, its data will be available directly to the parent instead of being masked for a different container. + +Applied to a fragment definition, `@relay(mask: false)` changes the generated Flow types to be better usable when the fragment is included with the same directive. The Flow types will no longer be exact objects and no longer contain internal marker fields. + +This may be helpful to reduce redundant fragments when dealing with nested or recursive data within a single Component. + +Keep in mind that it is typically considered an **anti-pattern** to create a single fragment shared across many containers. Abusing this directive could result in over-fetching in your application. + +In the example below, the `user` prop will include the data for `id` and `name` fields wherever `...Component_internUser` is included, instead of Relay's normal behavior to mask those fields: + +```javascript +graphql` + fragment Component_internUser on InternUser @relay(mask: false) { + id + name + } +`; +``` + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/hooks/load-query.md b/website/versioned_docs/version-v15.0.0/api-reference/hooks/load-query.md new file mode 100644 index 0000000000000..33966e329d71e --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/hooks/load-query.md @@ -0,0 +1,84 @@ +--- +id: load-query +title: loadQuery +slug: /api-reference/load-query/ +description: API reference for loadQuery, which imperatively fetches data for a query, retains that query and returns a query reference +keywords: + - preload + - fetch + - query + - render-as-you-fetch + - retain + - query reference +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +## `loadQuery` + +This function is designed to be used with the `usePreloadedQuery()` hook to implement the "render-as-you-fetch". + +Query references returned from `loadQuery` will leak data into the Relay store if `.dispose()` is not called on them once they are no longer referenced. As such, prefer calling `useQueryLoader` when possible, which ensures that query references are disposed for you. + +See the [`usePreloadedQuery`](../use-preloaded-query) docs for a more complete example. + +```js +const MyEnvironment = require('MyEnvironment'); +const {loadQuery} = require('react-relay'); + +const query = graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + } + } +`; + +// Note: you should generally not call loadQuery at the top level. +// Instead, it should be called in response to an event (such a route navigation, +// click, etc.). +const queryReference = loadQuery( + MyEnvironment, + query, + {id: '4'}, + {fetchPolicy: 'store-or-network'}, +); + +// later: pass queryReference to usePreloadedQuery() +// Note that query reference should have .dispose() called on them, +// which is missing in this example. +``` + +### Arguments + +* `environment`: A Relay Environment instance on which to execute the request. If you're starting this request somewhere within a React component, you probably want to use the environment you obtain from using [`useRelayEnvironment`](#userelayenvironment). +* `query`: GraphQL query to fetch, specified using a `graphql` template literal, or a preloadable concrete request, which can be acquired by requiring the file `$Parameters.graphql`. Relay will only generate the `$Parameters` file if the query is annotated with `@preloadable`. +* `variables`: Object containing the variable values to fetch the query. These variables need to match GraphQL variables declared inside the query. +* `options`: *_[Optional]_* options object + * `fetchPolicy`: Determines if cached data should be used, and whether to send a network request based on the cached data that is currently available in the Relay store (for more details, see our [Fetch Policies](../../guided-tour/reusing-cached-data/fetch-policies) and [Garbage Collection](../../guided-tour/reusing-cached-data/availability-of-data) guides): + * "store-or-network": **(default)** *will* reuse locally cached data and will *only* send a network request if any data for the query is missing. If the query is fully cached, a network request will *not* be made. + * "store-and-network": *will* reuse locally cached data and will *always* send a network request, regardless of whether any data was missing from the local cache or not. + * "network-only": *will not* reuse locally cached data, and will *always* send a network request to fetch the query, ignoring any data that might be locally cached in Relay. + * `networkCacheConfig`: *_[Optional]_* Default value: `{force: true}`. Object containing cache config options for the *network layer*. Note that the network layer may contain an *additional* query response cache which will reuse network responses for identical queries. If you want to bypass this cache completely (which is the default behavior), pass `{force: true}` as the value for this option. +* `environmentProviderOptions`: *[Optional]* options object + * Options passed to an `environmentProvider` used in `prepareSurfaceEntryPoint.js`. + +### Return Value + +A query reference with the following properties: + +* `dispose`: a method that will release the query reference from being retained by the store. This can cause the data referenced by the query reference to be garbage collected. + +The exact format of the return value is *unstable and highly likely to change*. We strongly recommend not using any other properties of the return value, as such code would be highly likely to break when upgrading to future versions of Relay. Instead, pass the result of `loadQuery()` to `usePreloadedQuery()`. + +### Behavior + +* `loadQuery()` will fetch data if passed a query, or data and the query if passed a preloadable concrete request. Once both the query and data are available, the data from the query will be written to the store. This differs from the behavior of `preloadQuery_DEPRECATED`, which would only write data to the store if the query was passed to `usePreloadedQuery`. +* the query reference returned from `loadQuery` will be retained by the relay store, preventing it the data from being garbage collected. Once you call `.dispose()` on the query reference, it can be garbage collected. +* `loadQuery()` will throw an error if it is called during React's render phase. + + + + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/hooks/relay-environment-provider.md b/website/versioned_docs/version-v15.0.0/api-reference/hooks/relay-environment-provider.md new file mode 100644 index 0000000000000..882e457cace88 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/hooks/relay-environment-provider.md @@ -0,0 +1,78 @@ +--- +id: relay-environment-provider +title: RelayEnvironmentProvider +slug: /api-reference/relay-environment-provider/ +description: API reference for RelayEnvironmentProvider, which sets a Relay environment in React context +keywords: + - environment + - context +--- + +import DocsRating from '@site/src/core/DocsRating'; + +## `RelayEnvironmentProvider` + +This component is used to set a Relay environment in React Context. Usually, a *single* instance of this component should be rendered at the very root of the application, in order to set the Relay environment for the whole application: + +```js +const React = require('React'); +const { + Store, + RecordSource, + Environment, + Network, + Observable, +} = require("relay-runtime"); + +const {RelayEnvironmentProvider} = require('react-relay'); + +/** + * Custom fetch function to handle GraphQL requests for a Relay environment. + * + * This function is responsible for sending GraphQL requests over the network and returning + * the response data. It can be customized to integrate with different network libraries or + * to add authentication headers as needed. + * + * @param {RequestParameters} params - The GraphQL request parameters to send to the server. + * @param {Variables} variables - Variables used in the GraphQL query. + */ +function fetchFunction(params, variables) { + const response = fetch("http://my-graphql/api", { + method: "POST", + headers: [["Content-Type", "application/json"]], + body: JSON.stringify({ + query: params.text, + variables, + }), + }); + + return Observable.from(response.then((data) => data.json())); +}; + +/** + * Creates a new Relay environment instance for managing (fetching, storing) GraphQL data. + */ +function createEnvironment() { + const network = Network.create(fetchFunction); + const store = new Store(new RecordSource()); + return new Environment({ store, network }); +} + +const environment = createEnvironment(); + +function Root() { + return ( + + + + ); +} + +module.exports = Root; +``` + +### Props + +* `environment`: The Relay environment to set in React Context. Any Relay Hooks (like [`useLazyLoadQuery`](../use-lazy-load-query) or [`useFragment`](../use-fragment)) used in descendants of this provider component will use the Relay environment specified here + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-client-query.md b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-client-query.md new file mode 100644 index 0000000000000..8e1f3a1019674 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-client-query.md @@ -0,0 +1,65 @@ +--- +id: use-client-query +title: useClientQuery +slug: /api-reference/use-client-query/ +description: API reference for useClientQuery, a React hook used to render client only queries +keywords: + - query + - read + - client-query +--- + +import DocsRating from '@site/src/core/DocsRating'; + +`useClientQuery` hook is used to render queries that read _only_ client fields. + +The Relay Compiler fully supports [client-side extensions](../../guides/client-schema-extensions/) of the schema, which allows you to define local fields and types. + +```graphql +# example client extension of the `Query` type +extend type Query { + client_field: String +} +``` + +These client-only fields are not sent to the server, and should be updated +using APIs for local updates, for example `commitPayload`. + +```js +const React = require('React'); + +const {graphql, useClientQuery} = require('react-relay'); + +function ClientQueryComponent() { + const data = useClientQuery( + graphql` + query ClientQueryComponentQuery { + client_field + } + `, + {}, // variables + ); + + return ( +
{data.client_field}
+ ); +} +``` + + +### Arguments + +* `query`: GraphQL query specified using a `graphql` template literal. +* `variables`: Object containing the variable values to fetch the query. These variables need to match GraphQL variables declared inside the query. + +### Return Value + +* `data`: Object that contains data which has been read out from the Relay store; the object matches the shape of specified query. + * The Flow type for data will also match this shape, and contain types derived from the GraphQL Schema. For example, the type of `data` above is: `{| user: ?{| name: ?string |} |}`. + +### Behavior + +* This hooks works as [`useLazyLoadQuery`](../use-lazy-load-query) with `fetchPolicy: store-only`, it does not send the network request. + + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-fragment.md b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-fragment.md new file mode 100644 index 0000000000000..da33250e03959 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-fragment.md @@ -0,0 +1,69 @@ +--- +id: use-fragment +title: useFragment +slug: /api-reference/use-fragment/ +description: API reference for useFragment, a React hook used to read fragment data from the Relay store using a fragment reference +keywords: + - fragment + - read + - fragment reference +--- + +import DocsRating from '@site/src/core/DocsRating'; + +## `useFragment` + +```js +import type {UserComponent_user$key} from 'UserComponent_user.graphql'; + +const React = require('React'); + +const {graphql, useFragment} = require('react-relay'); + +type Props = { + user: UserComponent_user$key, +}; + +function UserComponent(props: Props) { + const data = useFragment( + graphql` + fragment UserComponent_user on User { + name + profile_picture(scale: 2) { + uri + } + } + `, + props.user, + ); + + return ( + <> +

{data.name}

+
+ +
+ + ); +} +``` + +### Arguments + +* `fragment`: GraphQL fragment specified using a `graphql` template literal. +* `fragmentReference`: The *fragment reference* is an opaque Relay object that Relay uses to read the data for the fragment from the store; more specifically, it contains information about which particular object instance the data should be read from. + * The type of the fragment reference can be imported from the generated Flow types, from the file `.graphql.js`, and can be used to declare the type of your `Props`. The name of the fragment reference type will be: `$key`. We use our [lint rule](https://github.com/relayjs/eslint-plugin-relay) to enforce that the type of the fragment reference prop is correctly declared. + +### Return Value + +* `data`: Object that contains data which has been read out from the Relay store; the object matches the shape of specified fragment. + * The Flow type for data will also match this shape, and contain types derived from the GraphQL Schema. For example, the type of `data` above is: `{ name: ?string, profile_picture: ?{ uri: ?string } }`. + +### Behavior + +* The component is automatically subscribed to updates to the fragment data: if the data for this particular `User` is updated anywhere in the app (e.g. via fetching new data, or mutating existing data), the component will automatically re-render with the latest updated data. +* The component will suspend if any data for that specific fragment is missing, and the data is currently being fetched by a parent query. + * For more details on Suspense, see our [Loading States with Suspense](../../guided-tour/rendering/loading-states) guide. + + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-lazy-load-query.md b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-lazy-load-query.md new file mode 100644 index 0000000000000..1a80e04b4a9a6 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-lazy-load-query.md @@ -0,0 +1,77 @@ +--- +id: use-lazy-load-query +title: useLazyLoadQuery +slug: /api-reference/use-lazy-load-query/ +description: API reference for useLazyLoadQuery, a React hook used to lazily fetch query data when a component renders +keywords: + - lazy fetching + - query + - fetch +--- + +import DocsRating from '@site/src/core/DocsRating'; + +## `useLazyLoadQuery` + +Hook used to fetch a GraphQL query during render. This hook can trigger multiple nested or waterfalling round trips if used without caution, and waits until render to start a data fetch (when it can usually start a lot sooner than render), thereby degrading performance. Instead, prefer [`usePreloadedQuery`](../use-preloaded-query). + +```js +const React = require('React'); + +const {graphql, useLazyLoadQuery} = require('react-relay'); + +function App() { + const data = useLazyLoadQuery( + graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + } + } + `, + {id: 4}, + {fetchPolicy: 'store-or-network'}, + ); + + return

{data.user?.name}

; +} +``` + +### Arguments + +* `query`: GraphQL query specified using a `graphql` template literal. +* `variables`: Object containing the variable values to fetch the query. These variables need to match GraphQL variables declared inside the query. +* `options`: _*[Optional]*_ options object + * `fetchPolicy`: Determines if cached data should be used, and when to send a network request based on the cached data that is currently available in the Relay store (for more details, see our [Fetch Policies](../../guided-tour/reusing-cached-data/fetch-policies) and [Garbage Collection](../../guided-tour/reusing-cached-data/presence-of-data) guides): + * "store-or-network": _*(default)*_ *will* reuse locally cached data and will *only* send a network request if any data for the query is missing. If the query is fully cached, a network request will *not* be made. + * "store-and-network": *will* reuse locally cached data and will *always* send a network request, regardless of whether any data was missing from the local cache or not. + * "network-only": *will* *not* reuse locally cached data, and will *always* send a network request to fetch the query, ignoring any data that might be locally cached in Relay. + * "store-only": *will* *only* reuse locally cached data, and will *never* send a network request to fetch the query. In this case, the responsibility of fetching the query falls to the caller, but this policy could also be used to read and operate on data that is entirely [local](../../guided-tour/updating-data/local-data-updates). + * `fetchKey`: A `fetchKey` can be passed to force a re-evaluation of the current query and variables when the component re-renders, even if the variables didn't change, or even if the component isn't remounted (similarly to how passing a different `key` to a React component will cause it to remount). If the `fetchKey` is different from the one used in the previous render, the current query will be re-evaluated against the store, and it might be refetched depending on the current `fetchPolicy` and the state of the cache. + * `networkCacheConfig`: *_[Optional] _* Default value: `{force: true}`. Object containing cache config options for the *network layer*. Note that the network layer may contain an *additional* query response cache which will reuse network responses for identical queries. If you want to bypass this cache completely (which is the default behavior), pass `{force: true}` as the value for this option. + +### Return Value + +* `data`: Object that contains data which has been read out from the Relay store; the object matches the shape of specified query. + * The Flow type for data will also match this shape, and contain types derived from the GraphQL Schema. For example, the type of `data` above is: `{| user: ?{| name: ?string |} |}`. + +### Behavior + +* It is expected for `useLazyLoadQuery` to have been rendered under a [`RelayEnvironmentProvider`](../relay-environment-provider), in order to access the correct Relay environment, otherwise an error will be thrown. +* Calling `useLazyLoadQuery` will fetch and render the data for this query, and it may [*_suspend_*](../../guided-tour/rendering/loading-states) while the network request is in flight, depending on the specified `fetchPolicy`, and whether cached data is available, or if it needs to send and wait for a network request. If `useLazyLoadQuery` causes the component to suspend, you'll need to make sure that there's a `Suspense` ancestor wrapping this component in order to show the appropriate loading state. + * For more details on Suspense, see our [Loading States with Suspense](../../guided-tour/rendering/loading-states/) guide. +* The component is automatically subscribed to updates to the query data: if the data for this query is updated anywhere in the app, the component will automatically re-render with the latest updated data. +* After a component using `useLazyLoadQuery` has committed, re-rendering/updating the component will not cause the query to be fetched again. + * If the component is re-rendered with *different query variables,* that will cause the query to be fetched again with the new variables, and potentially re-render with different data. + * If the component *unmounts and remounts*, that will cause the current query and variables to be refetched (depending on the `fetchPolicy` and the state of the cache). + +### Differences with `QueryRenderer` + +* `useLazyLoadQuery` no longer takes a Relay environment as a parameter, and thus no longer sets the environment in React Context, like `QueryRenderer` did. Instead, `useLazyLoadQuery` should be used as a descendant of a [`RelayEnvironmentProvider`](../relay-environment-provider), which now sets the Relay environment in Context. Usually, you should render a single `RelayEnvironmentProvider` at the very root of the application, to set a single Relay environment for the whole application. +* `useLazyLoadQuery` will use [Suspense](../../guided-tour/rendering/loading-states) to allow developers to render loading states using Suspense boundaries, and will throw errors if network errors occur, which can be caught and rendered with Error Boundaries. This as opposed to providing error objects or null props to the `QueryRenderer` render function to indicate errors or loading states. +* `useLazyLoadQuery` fully supports fetch policies in order to reuse data that is cached in the Relay store instead of solely relying on the network response cache. +* `useLazyLoadQuery` has better type safety guarantees for the data it returns, which was not possible with QueryRenderer since we couldn't parametrize the type of the data with a renderer api. + + + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-mutation.md b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-mutation.md new file mode 100644 index 0000000000000..ac87cd519cfff --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-mutation.md @@ -0,0 +1,94 @@ +--- +id: use-mutation +title: useMutation +slug: /api-reference/use-mutation/ +description: API reference for useMutation, a React hook used to execute a GraphQL mutation +keywords: + - mutation +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbUseMutationParameter from './fb/FbUseMutationParameter.md'; + +## `useMutation` + +Hook used to execute a mutation in a React component. + +```js +import type {FeedbackLikeMutation} from 'FeedbackLikeMutation.graphql'; +const React = require('React'); + +const {graphql, useMutation} = require('react-relay'); + +function LikeButton() { + const [commit, isInFlight] = useMutation(graphql` + mutation FeedbackLikeMutation($input: FeedbackLikeData!) { + feedback_like(data: $input) { + feedback { + id + viewer_does_like + like_count + } + } + } + `); + + if (isInFlight) { + return ; + } + + return ( + + + ); +} + +module.exports = FriendsList; +``` + +### Arguments + +* `fragment`: GraphQL fragment specified using a `graphql` template literal. + * This fragment must have an `@connection` directive on a connection field, otherwise using it will throw an error. + * This fragment must have a `@refetchable` directive, otherwise using it will throw an error. The `@refetchable` directive can only be added to fragments that are "refetchable", that is, on fragments that are declared on `Viewer` or `Query` types, or on a type that implements `Node` (i.e. a type that has an `id`). + * Note that you *do not* need to manually specify a pagination query yourself. The `@refetchable` directive will autogenerate a query with the specified `queryName`. This will also generate Flow types for the query, available to import from the generated file: `.graphql.js`. +* `fragmentReference`: The *fragment reference* is an opaque Relay object that Relay uses to read the data for the fragment from the store; more specifically, it contains information about which particular object instance the data should be read from. + * The type of the fragment reference can be imported from the generated Flow types, from the file `.graphql.js`, and can be used to declare the type of your `Props`. The name of the fragment reference type will be: `$key`. We use our [lint rule](https://github.com/relayjs/eslint-plugin-relay) to enforce that the type of the fragment reference prop is correctly declared. + +### Return Value + + + + + + + +Object containing the following properties: + +* `data`: Object that contains data which has been read out from the Relay store; the object matches the shape of specified fragment. + * The Flow type for data will also match this shape, and contain types derived from the GraphQL Schema. +* `isLoadingNext`: Boolean value which indicates if a pagination request for the *next* items in the connection is currently in flight, including any incremental data payloads. +* `isLoadingPrevious`: Boolean value which indicates if a pagination request for the *previous* items in the connection is currently in flight, including any incremental data payloads. +* `hasNext`: Boolean value which indicates if the end of the connection has been reached in the "forward" direction. It will be true if there are more items to query for available in that direction, or false otherwise. +* `hasPrevious`: Boolean value which indicates if the end of the connection has been reached in the "backward" direction. It will be true if there are more items to query for available in that direction, or false otherwise. +* `loadNext`: Function used to fetch more items in the connection in the "forward" direction. + * Arguments: + * `count`*:* Number that indicates how many items to query for in the pagination request. + * `options`: *_[Optional]_* options object + * `onComplete`: Function that will be called whenever the refetch request has completed, including any incremental data payloads. If an error occurs during the request, `onComplete` will be called with an `Error` object as the first parameter. + * Return Value: + * `disposable`: Object containing a `dispose` function. Calling `disposable.dispose()` will cancel the pagination request. + * Behavior: + * Calling `loadNext` *will not* cause the component to suspend. Instead, the `isLoadingNext` value will be set to true while the request is in flight, and the new items from the pagination request will be added to the connection, causing the component to re-render. + * Pagination requests initiated from calling `loadNext` will *always* use the same variables that were originally used to fetch the connection, *except* pagination variables (which need to change in order to perform pagination); changing variables other than the pagination variables during pagination doesn't make sense, since that'd mean we'd be querying for a different connection. +* `loadPrevious`: Function used to fetch more items in the connection in the "backward" direction. + * Arguments: + * `count`*:* Number that indicates how many items to query for in the pagination request. + * `options`: *_[Optional]_* options object + * `onComplete`: Function that will be called whenever the refetch request has completed, including any incremental data payloads. If an error occurs during the request, `onComplete` will be called with an `Error` object as the first parameter. + * Return Value: + * `disposable`: Object containing a `dispose` function. Calling `disposable.dispose()` will cancel the pagination request. + * Behavior: + * Calling `loadPrevious` *will not* cause the component to suspend. Instead, the `isLoadingPrevious` value will be set to true while the request is in flight, and the new items from the pagination request will be added to the connection, causing the component to re-render. + * Pagination requests initiated from calling `loadPrevious` will *always* use the same variables that were originally used to fetch the connection, *except* pagination variables (which need to change in order to perform pagination); changing variables other than the pagination variables during pagination doesn't make sense, since that'd mean we'd be querying for a different connection. +* `refetch`: Function used to refetch the connection fragment with a potentially new set of variables. + * Arguments: + * `variables`: Object containing the new set of variable values to be used to fetch the `@refetchable` query. + * These variables need to match GraphQL variables referenced inside the fragment. + * However, only the variables that are intended to change for the refetch request need to be specified; any variables referenced by the fragment that are omitted from this input will fall back to using the value specified in the original parent query. So for example, to refetch the fragment with the exact same variables as it was originally fetched, you can call `refetch({})`. + * Similarly, passing an `id` value for the `$id` variable is _*optional*_, unless the fragment wants to be refetched with a different `id`. When refetching a `@refetchable` fragment, Relay will already know the id of the rendered object. + * `options`: *_[Optional]_* options object + * `fetchPolicy`: Determines if cached data should be used, and when to send a network request based on cached data that is available. See the [Fetch Policies](../../guided-tour/reusing-cached-data/fetch-policies/) section for full specification. + * `onComplete`: Function that will be called whenever the refetch request has completed, including any incremental data payloads. + * Return value: + * `disposable`: Object containing a `dispose` function. Calling `disposable.dispose()` will cancel the refetch request. + * Behavior: + * Calling `refetch` with a new set of variables will fetch the fragment again *with the newly provided variables*. Note that the variables you need to provide are only the ones referenced inside the fragment. In this example, it means fetching the translated body of the currently rendered Comment, by passing a new value to the `lang` variable. + * Calling `refetch` will re-render your component and may cause it to *[suspend](../../guided-tour/rendering/loading-states)*, depending on the specified `fetchPolicy` and whether cached data is available or if it needs to send and wait for a network request. If refetch causes the component to suspend, you'll need to make sure that there's a `Suspense` boundary wrapping this component. + * For more details on Suspense, see our [Loading States with Suspense](../../guided-tour/rendering/loading-states/) guide. + + + +### Behavior + +* The component is automatically subscribed to updates to the fragment data: if the data for this particular `User` is updated anywhere in the app (e.g. via fetching new data, or mutating existing data), the component will automatically re-render with the latest updated data. +* The component will suspend if any data for that specific fragment is missing, and the data is currently being fetched by a parent query. + * For more details on Suspense, see our [Loading States with Suspense](../../guided-tour/rendering/loading-states/) guide. +* Note that pagination (`loadNext` or `loadPrevious`), *will not* cause the component to suspend. + +### Differences with `PaginationContainer` + +* A pagination query no longer needs to be specified in this api, since it will be automatically generated by Relay by using a `@refetchable` fragment. +* This api supports simultaneous bi-directional pagination out of the box. +* This api no longer requires passing a `getVariables` or `getFragmentVariables` configuration functions, like the `PaginationContainer` does. + * This implies that pagination no longer has a between `variables` and `fragmentVariables`, which were previously vaguely defined concepts. Pagination requests will always use the same variables that were originally used to fetch the connection, *except* pagination variables (which need to change in order to perform pagination); changing variables other than the pagination variables during pagination doesn't make sense, since that'd mean we'd be querying for a different connection. +* This api no longer takes additional configuration like `direction` or `getConnectionFromProps` function (like Pagination Container does). These values will be automatically determined by Relay. +* Refetching no longer has a distinction between `variables` and `fragmentVariables`, which were previously vaguely defined concepts. Refetching will always correctly refetch and render the fragment with the variables you provide (any variables omitted in the input will fallback to using the original values in the parent query). +* Refetching will unequivocally update the component, which was not always true when calling `refetchConnection` from `PaginationContainer` (it would depend on what you were querying for in the refetch query and if your fragment was defined on the right object type). + + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-preloaded-query.md b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-preloaded-query.md new file mode 100644 index 0000000000000..57c0abcdd6780 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-preloaded-query.md @@ -0,0 +1,84 @@ +--- +id: use-preloaded-query +title: usePreloadedQuery +slug: /api-reference/use-preloaded-query/ +description: API reference for usePreloadedQuery, a React hook used to read query data from the Relay store using a query reference +keywords: + - read + - query + - query reference +--- + +import DocsRating from '@site/src/core/DocsRating'; + +## `usePreloadedQuery` + +Hook used to access data fetched by an earlier call to [`loadQuery`](../load-query) or with the help of [`useQueryLoader`](../use-query-loader). This implements the "render-as-you-fetch" pattern: + +* Call the `loadQuery` callback returned from `useQueryLoader`. This will store a query reference in React state. + * You can also call the imported `loadQuery` directly, which returns a query reference. In that case, store the item in state or in a React ref, and call `dispose()` on the value when you are no longer using it. +* Then, in your render method, consume the query reference with `usePreloadedQuery()`. This call will suspend if the query is still pending, throw an error if it failed, and otherwise return the query results. +* This pattern is encouraged over `useLazyLoadQuery()` as it can allow fetching data earlier while not blocking rendering. + +For more information, see the [Rendering Queries](../../guided-tour/rendering/queries) guide. + +```js + +import type {AppQueryType} from 'AppQueryType.graphql'; + +const React = require('React'); + +const {graphql, useQueryLoader, usePreloadedQuery} = require('react-relay'); + +const AppQuery = graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + } + } +`; + +type Props = { + initialQueryRef: PreloadedQuery, +}; + +function NameLoader(props) { + const [queryReference, loadQuery] = useQueryLoader( + AppQuery, + props.initialQueryRef, /* e.g. provided by router */ + ); + + return (<> + + + {queryReference != null + ? + : null + } + + ); +} + +function NameDisplay({ queryReference }) { + const data = usePreloadedQuery(AppQuery, queryReference); + + return

{data.user?.name}

; +} +``` + +### Arguments + +* `query`: GraphQL query specified using a `graphql` template literal. +* `preloadedQueryReference`: A `PreloadedQuery` query reference, which can be acquired from [`useQueryLoader`](../use-query-loader) or by calling [`loadQuery()`](../load-query) . + +### Return Value + +* `data`: Object that contains data which has been read out from the Relay store; the object matches the shape of specified query. + * The Flow type for data will also match this shape, and contain types derived from the GraphQL Schema. For example, the type of `data` above is: `{ user: ?{ name: ?string } }`. + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-query-loader.md b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-query-loader.md new file mode 100644 index 0000000000000..4129e47f2b11a --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-query-loader.md @@ -0,0 +1,95 @@ +--- +id: use-query-loader +title: useQueryLoader +slug: /api-reference/use-query-loader/ +description: API reference for useQueryLoader, a React hook used to imperatively fetch data for a query in response to a user event +keywords: + - query + - fetch + - preload + - render-as-you-fetch +--- + +import DocsRating from '@site/src/core/DocsRating'; + +## `useQueryLoader` + +Hook used to make it easy to safely load and retain queries. It will keep a query reference stored in state, and dispose of it when the component is disposed or it is no longer accessible via state. + +This hook is designed to be used with [`usePreloadedQuery`](../use-preloaded-query) to implement the "render-as-you-fetch" pattern. For more information, see the [Fetching Queries for Render](../../guided-tour/rendering/queries/) guide. + +```js +import type {PreloadedQuery} from 'react-relay'; + +const {useQueryLoader, usePreloadedQuery} = require('react-relay'); + +const AppQuery = graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + } + } +`; + +function QueryFetcherExample() { + const [ + queryReference, + loadQuery, + disposeQuery, + ] = useQueryLoader( + AppQuery, + ); + + if (queryReference == null) { + return ( + + ); + } + + return ( + <> + + + + + + ); +} + +function NameDisplay({ queryReference }) { + const data = usePreloadedQuery(AppQuery, queryReference); + + return

{data.user?.name}

; +} +``` + +### Arguments + +* `query`: GraphQL query specified using a `graphql` template literal. +* `initialQueryRef`: _*[Optional]*_ An initial `PreloadedQuery` to be used as the initial value of the `queryReference` stored in state and returned by `useQueryLoader`. + +### Return value + +A tuple containing the following values: + +* `queryReference`: the query reference, or `null`. +* `loadQuery`: a callback that, when executed, will load a query, which will be accessible as `queryReference`. If a previous query was loaded, it will dispose of it. It will throw an error if called during React's render phase. + * Parameters + * `variables`: the variables with which the query is loaded. + * `options`: `LoadQueryOptions`. An optional options object, containing the following keys: + * `fetchPolicy`: _*[Optional]*_ Determines if cached data should be used, and when to send a network request based on the cached data that is currently available in the Relay store (for more details, see our [Fetch Policies](../../guided-tour/reusing-cached-data/fetch-policies) and [Garbage Collection](../../guided-tour/reusing-cached-data/presence-of-data) guides): + * "store-or-network": _*(default)*_ *will* reuse locally cached data and will *only* send a network request if any data for the query is missing. If the query is fully cached, a network request will *not* be made. + * "store-and-network": *will* reuse locally cached data and will *always* send a network request, regardless of whether any data was missing from the local cache or not. + * "network-only": *will* *not* reuse locally cached data, and will *always* send a network request to fetch the query, ignoring any data that might be locally cached in Relay. + * `networkCacheConfig`: *_[Optional]_* Default value: `{force: true}`. Object containing cache config options for the *network layer*. Note that the network layer may contain an *additional* query response cache which will reuse network responses for identical queries. If you want to bypass this cache completely (which is the default behavior), pass `{force: true}` as the value for this option. +* `disposeQuery`: a callback that, when executed, will set `queryReference` to `null` and call `.dispose()` on it. It has type `() => void`. It should not be called during React's render phase. + +### Behavior + +* The `loadQuery` callback will fetch data if passed a query, or data and the query if passed a preloadable concrete request. Once both the query and data are available, the data from the query will be written to the store. This differs from the behavior of `preloadQuery_DEPRECATED`, which would only write data to the store if the query was passed to `usePreloadedQuery`. +* This query reference will be retained by the Relay store, preventing the data from being garbage collected. Once `.dispose()` is called on the query reference, the data is liable to be garbage collected. +* The `loadQuery` callback will throw an error if it is called during React's render phase. + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-refetchable-fragment.md b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-refetchable-fragment.md new file mode 100644 index 0000000000000..f2d80974a672f --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-refetchable-fragment.md @@ -0,0 +1,121 @@ +--- +id: use-refetchable-fragment +title: useRefetchableFragment +slug: /api-reference/use-refetchable-fragment/ +description: API reference for useRefetchableFragment, a React hook used to refetch fragment data +keywords: + - refetch + - fragment +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly, OssOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbUseRefetchableFragmentApiReferenceCodeExample from './fb/FbUseRefetchableFragmentApiReferenceCodeExample.md'; +import FbUseRefetchableFragmentReturnValue from './fb/FbUseRefetchableFragmentReturnValue.md'; + +## `useRefetchableFragment` + +You can use `useRefetchableFragment` when you want to fetch and re-render a fragment with different data: + + + + + + + +```js +import type {CommentBody_comment$key} from 'CommentBody_comment.graphql'; + +const React = require('React'); + +const {graphql, useRefetchableFragment} = require('react-relay'); + + +type Props = { + comment: CommentBody_comment$key, +}; + +function CommentBody(props: Props) { + const [data, refetch] = useRefetchableFragment( + graphql` + fragment CommentBody_comment on Comment + @refetchable(queryName: "CommentBodyRefetchQuery") { + body(lang: $lang) { + text + } + } + `, + props.comment, + ); + + return ( + <> +

{data.body?.text}

+ + + ); +} + +module.exports = CommentBody; +``` + +
+ +### Arguments + +* `fragment`: GraphQL fragment specified using a `graphql` template literal. This fragment must have a `@refetchable` directive, otherwise using it will throw an error. The `@refetchable` directive can only be added to fragments that are "refetchable", that is, on fragments that are declared on `Viewer` or `Query` types, or on a type that implements `Node` (i.e. a type that has an `id`). + * Note that you *do not* need to manually specify a refetch query yourself. The `@refetchable` directive will autogenerate a query with the specified `queryName`. This will also generate Flow types for the query, available to import from the generated file: `.graphql.js`. +* `fragmentReference`: The *fragment reference* is an opaque Relay object that Relay uses to read the data for the fragment from the store; more specifically, it contains information about which particular object instance the data should be read from. + * The type of the fragment reference can be imported from the generated Flow types, from the file `.graphql.js`, and can be used to declare the type of your `Props`. The name of the fragment reference type will be: `$key`. We use our [lint rule](https://github.com/relayjs/eslint-plugin-relay) to enforce that the type of the fragment reference prop is correctly declared. + +### Return Value + + + + + + + +Tuple containing the following values + +* [0] `data`: Object that contains data which has been read out from the Relay store; the object matches the shape of specified fragment. + * The Flow type for data will also match this shape, and contain types derived from the GraphQL Schema. +* [1] `refetch`: Function used to refetch the fragment with a potentially new set of variables. + * Arguments: + * `variables`: Object containing the new set of variable values to be used to fetch the `@refetchable` query. + * These variables need to match GraphQL variables referenced inside the fragment. + * However, only the variables that are intended to change for the refetch request need to be specified; any variables referenced by the fragment that are omitted from this input will fall back to using the value specified in the original parent query. So for example, to refetch the fragment with the exact same variables as it was originally fetched, you can call `refetch({})`. + * Similarly, passing an `id` value for the `$id` variable is _*optional*_, unless the fragment wants to be refetched with a different `id`. When refetching a `@refetchable` fragment, Relay will already know the id of the rendered object. + * `options`: *_[Optional]_* options object + * `fetchPolicy`: Determines if cached data should be used, and when to send a network request based on cached data that is available. See the [Fetch Policies](../../guided-tour/reusing-cached-data/fetch-policies/) section for full specification. + * `onComplete`: Function that will be called whenever the refetch request has completed, including any incremental data payloads. + * Return value: + * `disposable`: Object containing a `dispose` function. Calling `disposable.dispose()` will cancel the refetch request. + * Behavior: + * Calling `refetch` with a new set of variables will fetch the fragment again *with the newly provided variables*. Note that the variables you need to provide are only the ones referenced inside the fragment. In this example, it means fetching the translated body of the currently rendered Comment, by passing a new value to the `lang` variable. + * Calling `refetch` will re-render your component and may cause it to _*[suspend](../../guided-tour/rendering/loading-states)*_, depending on the specified `fetchPolicy` and whether cached data is available or if it needs to send and wait for a network request. If refetch causes the component to suspend, you'll need to make sure that there's a `Suspense` boundary wrapping this component. + * For more details on Suspense, see our [Loading States with Suspense](../../guided-tour/rendering/loading-states/) guide. + + + +### Behavior + +* The component is automatically subscribed to updates to the fragment data: if the data for this particular `User` is updated anywhere in the app (e.g. via fetching new data, or mutating existing data), the component will automatically re-render with the latest updated data. +* The component will suspend if any data for that specific fragment is missing, and the data is currently being fetched by a parent query. + * For more details on Suspense, see our [Loading States with Suspense](../../guided-tour/rendering/loading-states/) guide. + +### Differences with `RefetchContainer` + +* A refetch query no longer needs to be specified in this api, since it will be automatically generated by Relay by using a `@refetchable` fragment. +* Refetching no longer has a distinction between `refetchVariables` and `renderVariables`, which were previously vaguely defined concepts. Refetching will always correctly refetch and render the fragment with the variables you provide (any variables omitted in the input will fallback to using the original values from the parent query). +* Refetching will unequivocally update the component, which was not always true when calling refetch from `RefetchContainer` (it would depend on what you were querying for in the refetch query and if your fragment was defined on the right object type). + + + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-relay-environment.md b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-relay-environment.md new file mode 100644 index 0000000000000..098764d7ed419 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-relay-environment.md @@ -0,0 +1,37 @@ +--- +id: use-relay-environment +title: useRelayEnvironment +slug: /api-reference/use-relay-environment/ +description: API reference for useRelayEnvironment, a React hook used to access the Relay environment from context +keywords: + - environment + - context +--- + +import DocsRating from '@site/src/core/DocsRating'; + +## `useRelayEnvironment` + +Hook used to access a Relay environment that was set by a [`RelayEnvironmentProvider`](../relay-environment-provider): + +```js +const React = require('React'); + +const {useRelayEnvironment} = require('react-relay'); + +function MyComponent() { + const environment = useRelayEnvironment(); + + const handler = useCallback(() => { + // For example, can be used to pass the environment to functions + // that require a Relay environment. + commitMutation(environment, ...); + }, [environment]) + + return (...); +} + +module.exports = MyComponent; +``` + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-subscription.md b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-subscription.md new file mode 100644 index 0000000000000..01a4822232488 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/hooks/use-subscription.md @@ -0,0 +1,66 @@ +--- +id: use-subscription +title: useSubscription +slug: /api-reference/use-subscription/ +description: API reference for useSubscription, a React hook used to subscribe and unsubscribe from a subscription +keywords: + - subscription +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import GraphQLSubscriptionConfig from '../types/GraphQLSubscriptionConfig.md'; + +## `useSubscription` + +Hook used to subscribe and unsubscribe to a subscription. + +```js +import {graphql, useSubscription} from 'react-relay'; +import {useMemo} from 'react'; + +const subscription = graphql` + subscription UserDataSubscription($input: InputData!) { + # ... + } +`; + +function UserComponent({ id }) { + // IMPORTANT: your config should be memoized. + // Otherwise, useSubscription will re-render too frequently. + const config = useMemo(() => ({ + variables: {id}, + subscription, + }), [id, subscription]); + + useSubscription(config); + + return (/* ... */); +} +``` + +### Arguments + +* `config`: a config of type [`GraphQLSubscriptionConfig`](#type-graphqlsubscriptionconfigtsubscriptionpayload) passed to [`requestSubscription`](../request-subscription/) +* `requestSubscriptionFn`: `?(IEnvironment, GraphQLSubscriptionConfig) => Disposable`. An optional function with the same signature as [`requestSubscription`](../request-subscription/), which will be called in its stead. Defaults to `requestSubscription`. + + + +### Behavior + +* This is only a thin wrapper around the `requestSubscription` API. It will: + * Subscribe when the component is mounted with the given config + * Unsubscribe when the component is unmounted + * Unsubscribe and resubscribe with new values if the environment, config or `requestSubscriptionFn` changes. +* If you have the need to do something more complicated, such as imperatively requesting a subscription, please use the [`requestSubscription`](../request-subscription/) API directly. +* See the [GraphQL Subscriptions Guide](../../guided-tour/updating-data/graphql-subscriptions/) for a more detailed explanation of how to work with subscriptions. + + + +:::note +`useSubscription` doesn't automatically add `client_subscription_id`. You may need to provide an arbitrary `client_subscription_id` to `config.variables.input` +::: + + + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/legacy-apis/legacy-apis.md b/website/versioned_docs/version-v15.0.0/api-reference/legacy-apis/legacy-apis.md new file mode 100644 index 0000000000000..0c06eb626c42b --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/legacy-apis/legacy-apis.md @@ -0,0 +1,18 @@ +--- +id: legacy-apis +title: Legacy APIs +slug: /api-reference/legacy-apis/ +description: API reference for legacy APIs +keywords: + - QueryRenderer + - Container +--- + +API references for our previous legacy APIs are available in our previous docs website: + +- [`QueryRenderer`](https://relay.dev/docs/en/v10.1.3/query-renderer) +- [`Fragment Container`](https://relay.dev/docs/en/v10.1.3/fragment-container) +- [`Refetch Container`](https://relay.dev/docs/en/v10.1.3/refetch-container) +- [`Pagination Container`](https://relay.dev/docs/en/v10.1.3/pagination-container) +- [`Mutations`](https://relay.dev/docs/en/v10.1.3/mutations) +- [`Subscriptions`](https://relay.dev/docs/en/v10.1.3/subscriptions) diff --git a/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/commit-mutation.md b/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/commit-mutation.md new file mode 100644 index 0000000000000..9a08ff5a717f0 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/commit-mutation.md @@ -0,0 +1,65 @@ +--- +id: commit-mutation +title: commitMutation +slug: /api-reference/commit-mutation/ +description: API reference for commitMutation, which imperatively executes a mutation +keywords: + - mutation +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import MutationConfig from '../types/MutationConfig.md'; +import Disposable from '../types/Disposable.md'; + +## `commitMutation` + +Imperatively execute a mutation. + +See also the [`useMutation`](../use-mutation/) API and [Guide to Updating Data](../../guided-tour/updating-data/). + +```js +import type {FeedbackLikeMutation} from 'FeedbackLikeMutation.graphql'; +const React = require('React'); + +const {graphql, commitMutation} = require('react-relay'); + +function likeFeedback(environment: IEnvironment): Disposable { + return commitMutation(environment, { + mutation: graphql` + mutation FeedbackLikeMutation($input: FeedbackLikeData!) { + feedback_like(data: $input) { + feedback { + id + viewer_does_like + like_count + } + } + } + `, + variables: { + input: { + id: '123', + }, + }, + }); +} +``` + +### Arguments + +* `environment`: `IEnvironment`. A Relay environment. +* `config`: [`MutationConfig`](#type-mutationconfigtmutationconfig-mutationparameters). + + + + +### Return Value + +* A [`Disposable`](#interface-disposable) which: + * If called while before the request completes, will cancel revert any optimistic updates and prevent the `onComplete` and `onError` callbacks from being executed. It will not necessarily cancel any network request. Will cause the `onUnsubscribe` callback to be called. + * If called after the initial request completes, will do nothing. + + + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/fetch-query.md b/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/fetch-query.md new file mode 100644 index 0000000000000..3f977530ddbad --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/fetch-query.md @@ -0,0 +1,111 @@ +--- +id: fetch-query +title: fetchQuery +slug: /api-reference/fetch-query/ +description: API reference for fetchQuery, which imperatively fetches data for a query and returns an observable +keywords: + - observable + - query + - fetch +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +## `fetchQuery` + +If you want to fetch a query outside of React, you can use the `fetchQuery` function from `react-relay`: + +```js +// You should prefer passing an environment that was returned from useRelayEnvironment() +const MyEnvironment = require('MyEnvironment'); +const {fetchQuery} = require('react-relay'); + +fetchQuery( + environment, + graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + } + } + `, + {id: 4}, +) +.subscribe({ + start: () => {...}, + complete: () => {...}, + error: (error) => {...}, + next: (data) => {...} +}); +``` + +### Arguments + +* `environment`: A Relay Environment instance to execute the request on. If you're starting this request somewhere within a React component, you probably want to use the environment you obtain from using [`useRelayEnvironment`](../use-relay-environment/). +* `query`: GraphQL query to fetch, specified using a `graphql` template literal. +* `variables`: Object containing the variable values to fetch the query. These variables need to match GraphQL variables declared inside the query. +* `options`: *_[Optional]_* options object + * `networkCacheConfig`: *_[Optional]_ *Object containing cache config options + * `force`: Boolean value. If true, will bypass the network response cache. Defaults to true. + +### Flow Type Parameters + +* `TQuery`: Type parameter that should correspond to the Flow type for the specified query. This type is available to import from the the auto-generated file: `.graphql.js`. It will ensure that the type of the data provided by the observable matches the shape of the query, and enforces that the `variables` passed as input to `fetchQuery` match the type of the variables expected by the query. + +### Return Value + +* `observable`: Returns an observable instance. To start the request, `subscribe` or `toPromise` must be called on the observable. Exposes the following methods: + * `subscribe`: Function that can be called to subscribe to the observable for the network request. Keep in mind that this subscribes you only to the fetching of the query, not to any subsequent changes to the data within the Relay Store. + * Arguments: + * `observer`: Object that specifies observer functions for different events occurring on the network request observable. May specify the following event handlers as keys in the observer object: + * `start`: Function that will be called when the network requests starts. It will receive a single `subscription` argument, which represents the subscription on the network observable. + * `complete`: Function that will be called if and when the network request completes successfully. + * `next`: Function that will be called every time a payload is received from the network. It will receive a single `data` argument, which represents a snapshot of the query data read from the Relay store at the moment a payload was received from the server. + * `error`: Function that will be called if an error occurs during the network request. It will receive a single `error` argument, containing the error that occurred. + * `unsubscribe`: Function that will be called whenever the subscription is unsubscribed. It will receive a single `subscription` argument, which represents the subscription on the network observable. + * Return Value: + * `subscription`: Object representing a subscription to the observable. Calling `subscription.unsubscribe()` will cancel the network request. + * `toPromise`: + * Return Value: + * `promise`: Returns a promise that will resolve when the first network response is received from the server. If the request fails, the promise will reject. Cannot be cancelled. + + + +> The `next` function may be called multiple times when using Relay's [Incremental Data Delivery](../../guides/incremental-data-delivery/) capabilities to receive multiple payloads from the server. + + + +### Behavior + +* `fetchQuery` will automatically save the fetched data to the in-memory Relay store, and notify any components subscribed to the relevant data. +* `fetchQuery` will **NOT** retain the data for the query, meaning that it is not guaranteed that the data will remain saved in the Relay store at any point after the request completes. If you wish to make sure that the data is retained outside of the scope of the request, you need to call `environment.retain()` directly on the query to ensure it doesn't get deleted. See our section on [Controlling Relay's GC Policy](../../guided-tour/reusing-cached-data/availability-of-data) for more details. +* `fetchQuery` will automatically de-dupe identical network requests (same query and variables) that are in flight at the same time, and that were initiated with `fetchQuery`. + + +### Behavior with `.toPromise()` + +If desired, you can convert the request into a Promise using `**.toPromise()**`. Note that toPromise will start the query and return a Promise that will resolve when the *first* piece of data returns from the server and *cancel further processing*. That means any deferred or 3D data in the query may not be processed. **We generally recommend against using toPromise() for this reason.** + +```js +const {fetchQuery} = require('react-relay'); + +fetchQuery( + environment, + graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + } + } + `, + {id: 4}, +) +.toPromise() // NOTE: don't use, this can cause data to be missing! +.then(data => {...}) +.catch(error => {...}; +``` + +* `toPromise` Returns a promise that will resolve when the first network response is received from the server. If the request fails, the promise will reject. Cannot be cancelled. + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/request-subscription.md b/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/request-subscription.md new file mode 100644 index 0000000000000..407d1b2a18b0f --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/request-subscription.md @@ -0,0 +1,55 @@ +--- +id: request-subscription +title: requestSubscription +slug: /api-reference/request-subscription/ +description: API reference for requestSubscription, which imperatively establishes a GraphQL subscription +keywords: + - subscription +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import GraphQLSubscriptionConfig from '../types/GraphQLSubscriptionConfig.md'; +import Disposable from '../types/Disposable.md'; + +## `requestSubscription` + +Imperative API for establishing a GraphQL Subscription. +See also the [`useSubscription`](../use-subscription/) API and the [Guide to Updating Data](../../guided-tour/updating-data/). + +```js +import {graphql, requestSubscription} from 'react-relay'; + +const subscription = graphql` + subscription UserDataSubscription($input: InputData!) { + # ... + } +`; + +function createSubscription(environment: IEnvironment): Disposable { + return requestSubscription(environment, { + subscription, + variables: {input: {userId: '4'}}, + }); +} +``` + +### Arguments + +* `environment`: A Relay Environment +* `config`: `GraphQLSubscriptionConfig` + + + +### Return Type + +* A [`Disposable`](#interface-disposable) that clears the subscription. + + + +### Behavior + +* Imperatively establish a subscription. +* See the [GraphQL Subscriptions Guide](../../guided-tour/updating-data/graphql-subscriptions/) for a more detailed explanation of how to work with subscriptions. + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/store.md b/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/store.md new file mode 100644 index 0000000000000..c5cae4a3ceaaa --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/relay-runtime/store.md @@ -0,0 +1,590 @@ +--- +id: store +title: Store +slug: /api-reference/store/ +description: API reference for the Relay store +keywords: + - store +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +The Relay Store can be used to programmatically update client-side data inside [`updater` functions](../../guided-tour/updating-data/graphql-mutations/). The following is a reference of the Relay Store interface. + +Table of Contents: + +- [RecordSourceSelectorProxy](#recordsourceselectorproxy) +- [RecordProxy](#recordproxy) +- [ConnectionHandler](#connectionhandler) + +## RecordSourceSelectorProxy + +The `RecordSourceSelectorProxy` is the type of the `store` that [`updater` functions](../../guided-tour/updating-data/graphql-mutations/) receive as an argument. The following is the `RecordSourceSelectorProxy` interface: + +```javascript +interface RecordSourceSelectorProxy { + create(dataID: string, typeName: string): RecordProxy; + delete(dataID: string): void; + get(dataID: string): ?RecordProxy; + getRoot(): RecordProxy; + getRootField(fieldName: string): ?RecordProxy; + getPluralRootField(fieldName: string): ?Array; + invalidateStore(): void; +} +``` + +### `create(dataID: string, typeName: string): RecordProxy` + +Creates a new record in the store given a `dataID` and the `typeName` as defined by the GraphQL schema. Returns a [`RecordProxy`](#recordproxy) which serves as an interface to mutate the newly created record. + +#### Example + +```javascript +const record = store.create(dataID, 'Todo'); +``` + +### `delete(dataID: string): void` + +Deletes a record from the store given its `dataID`. + +#### Example + +```javascript +store.delete(dataID); +``` + +### `get(dataID: string): ?RecordProxy` + +Retrieves a record from the store given its `dataID`. Returns a [`RecordProxy`](#recordproxy) which serves as an interface to mutate the record. + +#### Example + +```javascript +const record = store.get(dataID); +``` + +### `getRoot(): RecordProxy` + +Returns the [`RecordProxy`](#recordproxy) representing the root of the GraphQL document. + +#### Example + +Given the GraphQL document: + +```graphql +viewer { + id +} +``` + +Usage: + +```javascript +// Represents root query +const root = store.getRoot(); +``` + +### `getRootField(fieldName: string): ?RecordProxy` + +Retrieves a root field from the store given the `fieldName`, as defined by the GraphQL document. Returns a [`RecordProxy`](#recordproxy) which serves as an interface to mutate the record. + +#### Example + +Given the GraphQL document: + +```graphql +viewer { + id +} +``` + +Usage: + +```javascript +const viewer = store.getRootField('viewer'); +``` + +### `getPluralRootField(fieldName: string): ?Array` + +Retrieves a root field that represents a collection from the store given the `fieldName`, as defined by the GraphQL document. Returns an array of [`RecordProxies`](#recordproxy). + +#### Example + +Given the GraphQL document: + +```graphql +nodes(first: 10) { + # ... +} +``` + +Usage: + +```javascript +const nodes = store.getPluralRootField('nodes'); +``` + +### `invalidateStore(): void` + +Globally invalidates the Relay store. This will cause any data that was written to the store before invalidation occurred to be considered stale, and will be considered to require refetch the next time a query is checked with `environment.check()`. + +#### Example + +```javascript +store.invalidateStore(); +``` + +After global invalidation, any query that is checked before refetching it will be considered stale: + +```javascript +environment.check(query) === 'stale' +``` + +## RecordProxy + +The `RecordProxy` serves as an interface to mutate records: + +```javascript +interface RecordProxy { + copyFieldsFrom(sourceRecord: RecordProxy): void; + getDataID(): string; + getLinkedRecord(name: string, arguments?: ?Object): ?RecordProxy; + getLinkedRecords(name: string, arguments?: ?Object): ?Array; + getOrCreateLinkedRecord( + name: string, + typeName: string, + arguments?: ?Object, + ): RecordProxy; + getType(): string; + getValue(name: string, arguments?: ?Object): mixed; + setLinkedRecord( + record: RecordProxy, + name: string, + arguments?: ?Object, + ): RecordProxy; + setLinkedRecords( + records: Array, + name: string, + arguments?: ?Object, + ): RecordProxy; + setValue(value: mixed, name: string, arguments?: ?Object): RecordProxy; + invalidateRecord(): void; +} +``` + +### `getDataID(): string` + +Returns the `dataID` of the current record. + +#### Example + +```javascript +const id = record.getDataID(); +``` + +### `getType(): string` + +Gets the type of the current record, as defined by the GraphQL schema. + +#### Example + +```javascript +const type = user.getType(); // User +``` + +### `getValue(name: string, arguments?: ?Object): mixed` + +Gets the value of a field in the current record given the field name. + +#### Example + +Given the GraphQL document: + +```graphql +viewer { + id + name +} +``` + +Usage: + +```javascript +const name = viewer.getValue('name'); +``` + +Optionally, if the field takes arguments, you can pass a bag of `variables`. + +#### Example + +Given the GraphQL document: + +```graphql +viewer { + id + name(arg: $arg) +} +``` + +Usage: + +```javascript +const name = viewer.getValue('name', {arg: 'value'}); +``` + +### `getLinkedRecord(name: string, arguments?: ?Object): ?RecordProxy` + +Retrieves a record associated with the current record given the field name, as defined by the GraphQL document. Returns a `RecordProxy`. + +#### Example + +Given the GraphQL document: + +```graphql +rootField { + viewer { + id + name + } +} +``` + +Usage: + +```javascript +const rootField = store.getRootField('rootField'); +const viewer = rootField.getLinkedRecord('viewer'); +``` + +Optionally, if the linked record takes arguments, you can pass a bag of `variables` as well. + +#### Example + +Given the GraphQL document: + +```graphql +rootField { + viewer(arg: $arg) { + id + } +} +``` + +Usage: + +```javascript +const rootField = store.getRootField('rootField'); +const viewer = rootField.getLinkedRecord('viewer', {arg: 'value'}); +``` + +### `getLinkedRecords(name: string, arguments?: ?Object): ?Array` + +Retrieves the set of records associated with the current record given the field name, as defined by the GraphQL document. Returns an array of `RecordProxies`. + +#### Example + +Given the GraphQL document: + +```graphql +rootField { + nodes { + # ... + } +} +``` + +Usage: + +```javascript +const rootField = store.getRootField('rootField'); +const nodes = rootField.getLinkedRecords('nodes'); +``` + +Optionally, if the linked record takes arguments, you can pass a bag of `variables` as well. + +#### Example + +Given the GraphQL document: + +```graphql +rootField { + nodes(first: $count) { + # ... + } +} +``` + +Usage: + +```javascript +const rootField = store.getRootField('rootField'); +const nodes = rootField.getLinkedRecords('nodes', {count: 10}); +``` + +### `getOrCreateLinkedRecord(name: string, typeName: string, arguments?: ?Object)` + +Retrieves a record associated with the current record given the field name, as defined by the GraphQL document. If the linked record does not exist, it will be created given the type name. Returns a `RecordProxy`. + +#### Example + +Given the GraphQL document: + +```graphql +rootField { + viewer { + id + } +} +``` + +Usage: + +```javascript +const rootField = store.getRootField('rootField'); +const newViewer = rootField.getOrCreateLinkedRecord('viewer', 'User'); // Will create if it doesn't exist +``` + +Optionally, if the linked record takes arguments, you can pass a bag of `variables` as well. + +### `setValue(value: mixed, name: string, arguments?: ?Object): RecordProxy` + +Mutates the current record by setting a new value on the specified field. Returns the mutated record. + +Given the GraphQL document: + +```graphql +viewer { + id + name +} +``` + +Usage: + +```javascript +viewer.setValue('New Name', 'name'); +``` + +Optionally, if the field takes arguments, you can pass a bag of `variables`. + +```javascript +viewer.setValue('New Name', 'name', {arg: 'value'}); +``` + +### `copyFieldsFrom(sourceRecord: RecordProxy): void` + +Mutates the current record by copying the fields over from the passed in record `sourceRecord`. + +#### Example + +```javascript +const record = store.get(id1); +const otherRecord = store.get(id2); +record.copyFieldsFrom(otherRecord); // Mutates `record` +``` + +### `setLinkedRecord(record: RecordProxy, name: string, arguments?: ?Object)` + +Mutates the current record by setting a new linked record on the given field name. + +#### Example + +Given the GraphQL document: + +```graphql +rootField { + viewer { + id + } +} +``` + +Usage: + +```javascript +const rootField = store.getRootField('rootField'); +const newViewer = store.create(/* ... */); +rootField.setLinkedRecord(newViewer, 'viewer'); +``` + +Optionally, if the linked record takes arguments, you can pass a bag of `variables` as well. + +### `setLinkedRecords(records: Array, name: string, variables?: ?Object)` + +Mutates the current record by setting a new set of linked records on the given field name. + +#### Example + +Given the GraphQL document: + +```graphql +rootField { + nodes { + # ... + } +} +``` + +Usage: + +```javascript +const rootField = store.getRootField('rootField'); +const newNode = store.create(/* ... */); +const newNodes = [...rootField.getLinkedRecords('nodes'), newNode]; +rootField.setLinkedRecords(newNodes, 'nodes'); +``` + +Optionally, if the linked record takes arguments, you can pass a bag of `variables` as well. + +### `invalidateRecord(): void` + +Invalidates the record. This will cause any query that references this record to be considered stale until the next time it is refetched, and will be considered to require a refetch the next time such a query is checked with `environment.check()`. + +#### Example + +```javascript +const record = store.get('4'); +record.invalidateRecord(); +``` + +After invalidating a record, any query that references the invalidated record and that is checked before refetching it will be considered stale: + +```javascript +environment.check(query) === 'stale' +``` + +## ConnectionHandler + +`ConnectionHandler` is a utility module exposed by `relay-runtime` that aids in the manipulation of connections. `ConnectionHandler` exposes the following interface: + +```javascript +interface ConnectionHandler { + getConnection( + record: RecordProxy, + key: string, + filters?: ?Object, + ): ?RecordProxy, + createEdge( + store: RecordSourceProxy, + connection: RecordProxy, + node: RecordProxy, + edgeType: string, + ): RecordProxy, + insertEdgeBefore( + connection: RecordProxy, + newEdge: RecordProxy, + cursor?: ?string, + ): void, + insertEdgeAfter( + connection: RecordProxy, + newEdge: RecordProxy, + cursor?: ?string, + ): void, + deleteNode(connection: RecordProxy, nodeID: string): void +} +``` + +### `getConnection(record: RecordProxy, key: string, filters?: ?Object)` + +Given a record and a connection key, and optionally a set of filters, `getConnection` retrieves a [`RecordProxy`](#recordproxy) that represents a connection that was annotated with a `@connection` directive. + +First, let's take a look at a plain connection: + +```graphql +fragment FriendsFragment on User { + friends(first: 10) { + edges { + node { + id + } + } + } +} +``` + +Accessing a plain connection field like this is the same as other regular fields: + +```javascript +// The `friends` connection record can be accessed with: +const user = store.get(userID); +const friends = user && user.getLinkedRecord('friends'); + +// Access fields on the connection: +const edges = friends && friends.getLinkedRecords('edges'); +``` + +When using [usePaginationFragment](../use-pagination-fragment/), we usually annotate the actual connection field with `@connection` to tell Relay which part needs to be paginated: + +```graphql +fragment FriendsFragment on User { + friends(first: 10, orderby: "firstname") @connection( + key: "FriendsFragment_friends", + ) { + edges { + node { + id + } + } + } +} +``` + +For connections like the above, `ConnectionHandler` helps us find the record: + +```javascript +import {ConnectionHandler} from 'relay-runtime'; + +// The `friends` connection record can be accessed with: +const user = store.get(userID); +const friends = ConnectionHandler.getConnection( + user, // parent record + 'FriendsFragment_friends', // connection key + {orderby: 'firstname'} // 'filters' that is used to identify the connection +); +// Access fields on the connection: +const edges = friends.getLinkedRecords('edges'); +``` + +### Edge creation and insertion + +#### `createEdge(store: RecordSourceProxy, connection: RecordProxy, node: RecordProxy, edgeType: string)` + +Creates an edge given a [`store`](#recordsourceselectorproxy), a connection, the edge node, and the edge type. + +#### `insertEdgeBefore(connection: RecordProxy, newEdge: RecordProxy, cursor?: ?string)` + +Given a connection, inserts the edge at the beginning of the connection, or before the specified `cursor`. + +#### `insertEdgeAfter(connection: RecordProxy, newEdge: RecordProxy, cursor?: ?string)` + +Given a connection, inserts the edge at the end of the connection, or after the specified `cursor`. + +#### Example + +```javascript +const user = store.get(userID); +const friends = ConnectionHandler.getConnection(user, 'FriendsFragment_friends'); +const newFriend = store.get(newFriendId); +const edge = ConnectionHandler.createEdge(store, friends, newFriend, 'UserEdge'); + +// No cursor provided, append the edge at the end. +ConnectionHandler.insertEdgeAfter(friends, edge); + +// No cursor provided, insert the edge at the front: +ConnectionHandler.insertEdgeBefore(friends, edge); +``` + +### `deleteNode(connection: RecordProxy, nodeID: string): void` + +Given a connection, deletes any edges whose node id matches the given id. + +#### Example + +```javascript +const user = store.get(userID); +const friends = ConnectionHandler.getConnection(user, 'FriendsFragment_friends'); +ConnectionHandler.deleteNode(friends, idToDelete); +``` + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/types/CacheConfig.md b/website/versioned_docs/version-v15.0.0/api-reference/types/CacheConfig.md new file mode 100644 index 0000000000000..e7690fbdf7259 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/types/CacheConfig.md @@ -0,0 +1,8 @@ +#### Type `CacheConfig` + +* An object with the following fields: + * `force`: *_[Optional]_* A boolean. If true, causes a query to be issued unconditionally, regardless of the state of any configured response cache. + * `poll`: *_[Optional]_* A number. Causes a query to live-update by polling at the specified interval, in milliseconds. (This value will be passed to `setTimeout`). + * `liveConfigId`: *_[Optional]_* A string. Causes a query to live-update by calling GraphQLLiveQuery; it represents a configuration of gateway when doing live query. + * `metadata`: *_[Optional]_* An object. User-supplied metadata. + * `transactionId`: *_[Optional]_* A string. A user-supplied value, intended for use as a unique id for a given instance of executing an operation. diff --git a/website/versioned_docs/version-v15.0.0/api-reference/types/Disposable.md b/website/versioned_docs/version-v15.0.0/api-reference/types/Disposable.md new file mode 100644 index 0000000000000..76dcc19d99490 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/types/Disposable.md @@ -0,0 +1,4 @@ +#### Interface `Disposable` + +* An object with the following key: + * `dispose`: `() => void`. Disposes of the resource. diff --git a/website/versioned_docs/version-v15.0.0/api-reference/types/GraphQLSubscriptionConfig.md b/website/versioned_docs/version-v15.0.0/api-reference/types/GraphQLSubscriptionConfig.md new file mode 100644 index 0000000000000..901d88b94322c --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/types/GraphQLSubscriptionConfig.md @@ -0,0 +1,17 @@ +import SelectorStoreUpdater from './SelectorStoreUpdater.md'; +import CacheConfig from './CacheConfig.md'; + +#### Type `GraphQLSubscriptionConfig` + +* An object with the following fields: + * `cacheConfig`: *_[Optional]_* [`CacheConfig`](#type-cacheconfig) + * `subscription`: `GraphQLTaggedNode`. A GraphQL subscription specified using a `graphql` template literal + * `variables`: The variables to pass to the subscription + * `onCompleted`: *_[Optional]_* `() => void`. An optional callback that is executed when the subscription is established + * `onError`: *_[Optional]_* `(Error) => {}`. An optional callback that is executed when an error occurs + * `onNext`: *_[Optional]_* `(TSubscriptionPayload) => {}`. An optional callback that is executed when new data is received + * `updater`: *_[Optional]_* [`SelectorStoreUpdater`](#type-selectorstoreupdater). + + + + diff --git a/website/versioned_docs/version-v15.0.0/api-reference/types/MutationConfig.md b/website/versioned_docs/version-v15.0.0/api-reference/types/MutationConfig.md new file mode 100644 index 0000000000000..4a517246ae310 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/types/MutationConfig.md @@ -0,0 +1,31 @@ +import CacheConfig from './CacheConfig.md'; +import SelectorStoreUpdater from './SelectorStoreUpdater.md'; +import UploadableMap from './UploadableMap.md'; + +#### Type `MutationConfig` + +* An object with the following fields: + * `cacheConfig`: *_[Optional]_* [`CacheConfig`](#type-cacheconfig) + * `mutation`: `GraphQLTaggedNode`. A mutation specified using a GraphQL literal + * `onError`: *_[Optional]_* `(Error) => void`. An optional callback executed if the mutation results in an error. + * `onCompleted`: *_[Optional]_* `($ElementType) => void`. An optional callback that is executed when the mutation completes. + * The value passed to `onCompleted` is the the mutation fragment, as read out from the store, **after** updaters and declarative mutation directives are applied. This means that data from within unmasked fragments will not be read, and records that were deleted (e.g. by `@deleteRecord`) may also be null. + * `onUnsubscribe`: *_[Optional]_* `() => void`. An optional callback that is executed when the mutation the mutation is unsubscribed, which occurs when the returned `Disposable` is disposed. + * `optimisticResponse`: *_[Optional]_* An object whose type matches the raw response type of the mutation. Make sure you decorate your mutation with `@raw_response_type` if you are using this field. + * `optimisticUpdater`: *_[Optional]_* [`SelectorStoreUpdater`](#type-selectorstoreupdater). A callback that is executed when `commitMutation` is called, after the `optimisticResponse` has been normalized into the store. + * `updater`: *_[Optional]_* [`SelectorStoreUpdater`](#type-selectorstoreupdater). A callback that is executed when a payload is received, after the payload has been written into the store. + * `uploadables`: *_[Optional]_* [`UploadableMap`](#type-uploadablemap). An optional uploadable map. + * `variables`: `$ElementType`. The variables to pass to the mutation. + + + + + + + +#### Type `MutationParameters` + +* An object with the following fields: + * `response`: An object + * `variables`: An object + * `rawResponse`: An optional object diff --git a/website/versioned_docs/version-v15.0.0/api-reference/types/SelectorStoreUpdater.md b/website/versioned_docs/version-v15.0.0/api-reference/types/SelectorStoreUpdater.md new file mode 100644 index 0000000000000..02f93a2aebd71 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/types/SelectorStoreUpdater.md @@ -0,0 +1,6 @@ +import useBaseUrl from '@docusaurus/useBaseUrl'; + +#### Type `SelectorStoreUpdater` + +* A function with signature `(store: RecordSourceSelectorProxy, data) => void` +* This interface allows you to *imperatively* write and read data directly to and from the Relay store. This means that you have full control over how to update the store in response to the subscription payload: you can *create entirely new records*, or *update or delete existing ones*. The full API for reading and writing to the Relay store is available here. diff --git a/website/versioned_docs/version-v15.0.0/api-reference/types/UploadableMap.md b/website/versioned_docs/version-v15.0.0/api-reference/types/UploadableMap.md new file mode 100644 index 0000000000000..0050b91169e39 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/api-reference/types/UploadableMap.md @@ -0,0 +1,3 @@ +#### Type `UploadableMap` + +* An object whose values are [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) or [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). diff --git a/website/versioned_docs/version-v15.0.0/community/learning-resources.md b/website/versioned_docs/version-v15.0.0/community/learning-resources.md new file mode 100644 index 0000000000000..a2dd71c5a3a05 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/community/learning-resources.md @@ -0,0 +1,34 @@ +--- +id: learning-resources +title: Community Learning Resources +slug: /community-learning-resources/ +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly, OssOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +## Relay example projects + +These projects serve as an example of how to use Relay in real world applications. + +- [github.com/relayjs/relay-examples](https://github.com/relayjs/relay-examples) + +## Guides and articles: + +- [How to use @argumentsDefinitions to define local variables to your fragments](https://medium.com/entria/relay-modern-argumentdefinitions-d53769dbb95d) (by Entria) +- [Deep Dive of Updater Relay Store function. How to update your store properly after a mutation or subscription](https://medium.com/entria/wrangling-the-client-store-with-the-relay-modern-updater-function-5c32149a71ac) (by Entria) +- [Optimistic Update: how to update your UI before server responds](https://medium.com/entria/relay-modern-optimistic-update-a09ba22d83c9) (by Entria) +- [Relay Network Deep Dive - how to incrementally improve your network layer to manage complex data fetching requirements](https://medium.com/entria/relay-modern-network-deep-dive-ec187629dfd3) (by Entria) +- [Relay Modern with TypeScript - how to configure Relay Modern to make it with TypeScript](https://medium.com/@sibelius/relay-modern-migration-to-typescript-c26ab0ee749c) (by @sibelius) +- [Collection of random thoughts and discoveries around Relay](https://mrtnzlml.com/docs/relay) + + + +## Relay Modern articles + +Note: you can find many more resources by looking at the Relay Modern Documentation. + + + + diff --git a/website/versioned_docs/version-v15.0.0/debugging/declarative-mutation-directives.md b/website/versioned_docs/version-v15.0.0/debugging/declarative-mutation-directives.md new file mode 100644 index 0000000000000..cfe156a170312 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/debugging/declarative-mutation-directives.md @@ -0,0 +1,34 @@ +--- +id: declarative-mutation-directives +title: Debugging Declarative Mutation Directives +slug: /debugging/declarative-mutation-directives/ +description: Debugging declarative mutation directives +keywords: +- debugging +- troubleshooting +- declarative mutation directive +- deleteRecord +- handlerProvider +- appendEdge +- prependEdge +- appendNode +- prependNode +--- + +import FbEnvHandlerExample from './fb/FbEnvHandlerExample.md'; + +If you see an error similar to: + +``` +RelayFBHandlerProvider: No handler defined for `deleteRecord`. [Caught in: An uncaught error was thrown inside `RelayObservable`.] +``` + +or + +``` +RelayModernEnvironment: Expected a handler to be provided for handle `deleteRecord`. +``` + +This probably means that you are using a Relay environment to which a `handlerProvider` is passed. However, the handler provider does not know how to accept the handles `"deleteRecord"`, `"appendEdge"` or `"prependEdge"`. If this is the case, you should return `MutationHandlers.DeleteRecordHandler`, `MutationHandlers.AppendEdgeHandler`, or `MutationHandlers.PrependEdgeHandler` respectively (these can be imported from `relay-runtime`). + + diff --git a/website/versioned_docs/version-v15.0.0/debugging/disallowed-id-types-error.md b/website/versioned_docs/version-v15.0.0/debugging/disallowed-id-types-error.md new file mode 100644 index 0000000000000..a3febedc1c864 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/debugging/disallowed-id-types-error.md @@ -0,0 +1,43 @@ +--- +id: disallowed-id-types-error +title: Disallowed Types for `id` Fields +slug: /debugging/disallowed-id-types-error +description: Disallowed types for `id` fields +keywords: +- debugging +- troubleshooting +- disallowed +- id +- Object Identification +--- + +import DocsRating from '@site/src/core/DocsRating'; + +If you see an error from the compiler that reads something like: + +``` +Disallowed type `String` of field `id` on parent type `Foo` cannot be used by Relay to identify entities +``` + +This means that your GraphQL schema defines an object with a field named `id` that doesn't have a valid type. Valid types for this field are `ID` or `ID!` unless configured otherwise. While there may be a valid reason in your application to have that field defined that way, the Relay compiler and runtime will mishandle that field and cause refresh or data updating issues. + +## Disallowing `id` fields without type `ID` + +Recall that Relay uses [Object Identification](../../guides/graphql-server-specification/#object-identification) to know which GraphQL objects to refetch. In the usual case, those GraphQL objects implement the [`Node` interface](https://graphql.org/learn/global-object-identification/#node-interface) and thus come with an `id` field with type `ID`. However, there are types in your GraphQL model that may not require unique identification. For that reason, Relay (by default) does not restrict object definitions, allowing `id` fields with non-`ID` types (e.g. `String`) to be defined. + +This poses a bit of difficulty to both Relay's compiler and runtime. In short, the runtime and compiler only properly handle `id` fields as defined by the `Node` interface. Any selections made with non-`Node` `id` fields will likely exhibit undesirable and unintended Relay behavior on your components (e.g. components not re-rendering on re-fetched data). + +### The significance of the `ID` type + +[Global Object Identification in GraphQL](https://graphql.org/learn/global-object-identification/)) is what underlies Relay's Object Identification. The `id` field supplied by the `Node` interface is specified to be a unique identifier that can be used for storage or caching. + +## Fix: Define your `id` fields as `ID` + +To ensure Relay correctly manages objects fetched to your application, here are two recommended courses of action: + +* Ensure all fields named `id` are typed with `ID` +* Rename any fields named `id` (with a type that isn't `ID`) to a different name (e.g. `special_purpose_id`) + +If your application truly requires that `id` field's definition to remain as-is and you are aware of the problems that may arise, you can add your GraphQL type and the type of its `id` field to the allowlist in `nonNodeIdFields` of the [Relay Compiler's Configuration](https://github.com/facebook/relay/tree/main/packages/relay-compiler). Note that this will only bypass the error for that combination of object type and `id` field type. + + diff --git a/website/versioned_docs/version-v15.0.0/debugging/inconsistent-typename-error.md b/website/versioned_docs/version-v15.0.0/debugging/inconsistent-typename-error.md new file mode 100644 index 0000000000000..6944b91f4c2dd --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/debugging/inconsistent-typename-error.md @@ -0,0 +1,45 @@ +--- +id: inconsistent-typename-error +title: Inconsistent Typename Error +slug: /debugging/inconsistent-typename-error/ +description: Debugging inconsistent typename errors in Relay +keywords: +- debugging +- troubleshooting +- inconsistent typename +- RelayResponseNormalizer +- globally unique id +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly, OssOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +## Inconsistent `__typename` error + +The GraphQL server likely violated the globally unique ID requirement by returning the same ID for different objects. + +If you're seeing an error like: + +> `RelayResponseNormalizer: Invalid record '543'. Expected __typename to be consistent, but the record was assigned conflicting types Foo and Bar. The GraphQL server likely violated the globally unique ID requirement by returning the same ID for different objects.` + +the server implementation of one of the types is not spec compliant. We require the `id` field to be globally unique. This is a problem because Relay stores objects in a normalized key-value store and one of the object just overwrote the other. This means your app is broken in some subtle or less subtle way. + +## Common cause + +The most common reason for this error is that 2 objects backed by an ID are using the plain ID as the id field, such as a `User` and `MessagingParticipant`. + +Less common reasons could be using array indices or auto-increment IDs from some database that might not be unique to this type. + +## Fix: Make your type spec compliant + +The best way to fix this is to make your type spec compliant. For the case of 2 different types backed by the same ID, a common solution is to prefix the ID of the less widely used type with a unique string and then base64 encode the result. This can be accomplished fairly easily by implementing a `NodeTokenResolver` using the helper trait `NodeTokenResolverWithPrefix`. When the `NodeTokenResolver` is registered is allows you to load your type using `node(id: $yourID)` GraphQL call and your type can return an encoded ID. + + + +### Example + +See [D17996531](https://www.internalfb.com/diff/D17996531) for an example on how to fix this. It created a `FusionPlatformComponentsCategoryNodeResolver` added the trait `TGraphQLNodeMixin` to the conflicting class so that it generates the base64 encoded ID. + + + + diff --git a/website/versioned_docs/version-v15.0.0/debugging/relay-devtools.md b/website/versioned_docs/version-v15.0.0/debugging/relay-devtools.md new file mode 100644 index 0000000000000..de8ceda919dd3 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/debugging/relay-devtools.md @@ -0,0 +1,73 @@ +--- +id: relay-devtools +title: Relay DevTools +slug: /debugging/relay-devtools/ +description: Debugging guide for the Relay DevTools +keywords: +- debugging +- troubleshooting +- extension +- devtools +- browser +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly, OssOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +## Installation + + + +### Internal version (preferred) + +The internal version of devtools has the latest updates and the process of installation will be much faster. + +1. Before downloading the new Devtools, make sure you've deleted all older versions of the extension. +2. Join [Relay Support](https://fb.workplace.com/groups/relay.support) group and you will automatically be added to the `cpe_relay_devtools_extension` gatekeeper. +3. Wait 20-30 minutes, and it should be downloaded on your Chrome browser +4. Or run `sudo soloctl -i` on your machine to get the extension immediately + +### Internal Version for Edgium users + +1. On `C:\Users\\AppData\Local\Google\Chrome\User Data\\Extensions` search for files manifest.json with Relay Developer Tools on it +2. Get the path to this folder e.g `...\Extensions\\\` +3. On edge://extensions/ click load upacked (you might need to Allow extensions for other stores). + +### External version + +The external version of devtools is less prone to bugs but does not always contain the latest updates and you have to download the extension from the chrome store. +Follow this link and install it from the [chrome store](https://chrome.google.com/webstore/detail/relay-developer-tools/ncedobpgnmkhcmnnkcimnobpfepidadl). + + + + + +Follow this link and install it from the [chrome store](https://chrome.google.com/webstore/detail/relay-developer-tools/ncedobpgnmkhcmnnkcimnobpfepidadl). + + + +--- + +## How to Navigate the Relay DevTools Extension + +You should have a new tab called Relay in your Chrome DevTools. In this tab, you will be able to select between 2 panels: the **network panel** and the **store panel**. + +### Network Panel + +The network panel allows users to view individual requests in an active environment. Users can scroll through these requests, search for the requests and view the details of each request. The details of each request includes the status, the variables, and the responses for the request. + +### Store Panel + +The store panel allows users to view individual records from the store data in an active environment. Users can scroll through these records, search for the records, and view the details of each request. Users can also copy the the store data in JSON format to the clipboard. The details of each record includes the ID, the typename, and all of the data in the record. If one of the fields in the data is a reference to another record, users can click on the reference, which will take them to that record. + +--- + +## Multiple Environments + +As you switch through the store and network panels, you'll notice that there is also a dropdown menu on the left side of the developer tools. This dropdown allows users to switch between environments. The requests in the network tab and the store data are grouped by environment, so users can easily shuffle between active environments. + +## Give Feedback + +Look in the top-right corner of the extension panel! + + diff --git a/website/versioned_docs/version-v15.0.0/debugging/why-null.md b/website/versioned_docs/version-v15.0.0/debugging/why-null.md new file mode 100644 index 0000000000000..0f2a1c9c453e3 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/debugging/why-null.md @@ -0,0 +1,116 @@ +--- +id: why-null +title: "Why Is My Field Null?" +slug: /debugging/why-null/ +description: Get help figuring out why a given field is unexpectedly null. +keywords: +- "null" +- missing +- optional +- nullthrows +--- + +import {FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import DocsRating from '@site/src/core/DocsRating'; + +There are a number of reasons that a field read by Relay can be null and some of them are obscure or unintuitive. When debugging an unexpectedly null value, it can be helpful to understand both the common cases and edge cases that can cause a field to be read as null. This document enumerates the cases that can lead to null or missing values with tips for figuring determining which case you are in. + +## Server Returned Null + +The simplest reason a field might be null is that the server explicitly returned null. This can happen in two cases: + +1. The server’s field resolver returned an explicit null +2. The field resolver throws. In this case GraphQL will return null for that field. *This is true even if the server resolver’s return type is non-nullable.* The one exceptions is fields annotated as non-null. In that case server should *never* return null. If an exception is encountered the entire parent object will be nulled out. + + + +:::note +At Meta, non-nullable fields are implemented using [`KillsParentOnException`](https://www.internalfb.com/intern/wiki/Graphql-for-hack-developers/fields/return-type/#non-nullable-fields). +::: + + + +**🕵️‍♀️ How to tell:** Inspect the server’s response using Relay Dev tools, or in your browser’s dev tools’s network tab, to see if the field is null. + +## Graph Relationship Change + +If a different query/mutation/subscription observes a relationship change in the graph, you may end up trying to read fields off of an object which your query never fetched. + +Imagine you have a query that reads your best friend’s name: + +```graphql +query MyQuery { + me { + best_friend { + # id: 1 + name + } + } +} +``` + +After you get your query response, *who* your best friend is *changes* on the server. Then a *different* query/mutation/subscription fetches a different set of fields off of `best_friend`. + +```graphql +query OtherQuery { + me { + best_friend { + # new id: 2 + # Note: name is not fetched here + age + } + } +} +``` + +Because the Relay store is normalized, we will update the `me` record to indicate that `best_friend` linked field now points to the user with ID 2, and the only information we know about that user is their age. + +This will trigger a rerender of `MyQuery`. However, when we try to read the `name` field off of the user with ID 2, we won’t find it, since the only thing we know about the user with ID 2 is their `age`. Note that a relationship “change” in this case, could also mean a relationship that is new. For example, if you start with no best friend but a subsequent response returns *some* best friend, but does not fetch all fields your component needs. + +**Note**: In theory, Relay *could* refetch your query when this state is encountered, but some queries are not safe to re-issue arbitrarily, and more generally, UI state changing in a way that’s not tied to a direct user action can lead to confusion. For this reason, we have chosen not to perform refetches in this scenario. + +**🕵️‍♀️ How to tell:** You can place a breakpoint/`console.log` at the finale return statement of `readWithIdentifier` in `FragmentResource` ([code pointer](https://github.com/facebook/relay/blob/2b9876fcbf0845cd23728d4d720712525ff424c4/packages/react-relay/relay-hooks/FragmentResource.js#L475). This is the point in Relay at which we know that we are missing data, but there is not query in flight to get it. + +## Inconsistent Server Response + +This is a **rare edge case**, *but* if the server does not correctly implement the [field stability](https://graphql.org/learn/global-object-identification/#field-stability) semantics of the id field, it’s possible that a field could be present in one part of the response, but *explicitly null* in another. + +```graphql +{ + me { + id: 1 + name: "Alice" + } + me_elsewhere_in_the_graph { + id: 1 # Note this is the same as the `me` field above... + name: null + } +} +``` + +In this case, Relay first learns that user 1’s `name` is Alice, but later in the query finds that user 1’s `name` has now `null`. Because Relay stores data in a normalized store, user 1 can only have one value for `name` and Relay will end in a state where user 1’s `name` is `null`. + +**🕵️‍♀️ How to tell:** Relay is smart enough to detect when this has happened, and will [log an error to the console](https://github.com/facebook/relay/blob/2b9876fcbf0845cd23728d4d720712525ff424c4/packages/relay-runtime/store/RelayResponseNormalizer.js#L505) in dev that looks like: “RelayResponseNormalizer: Invalid record. The record contains two instances of the same id: 1 with conflicting field, name and its values: Alice and null.". Additionally, you can manually inspect the query response. + +Note that if the unstable field is a linked field (edge to another object), this type of bug can cause a Graph Relationship Change (described above) to occur *within a single response*. For example, if a user with the same `id` appears in two places in the response, but their `best_friend` is different in those two locations. + +**🕵️‍♀️ How to tell:** Relay is also smart enough to detect this case, and will show a [similar console warning](https://github.com/facebook/relay/blob/2b9876fcbf0845cd23728d4d720712525ff424c4/packages/relay-runtime/store/RelayResponseNormalizer.js#L844) in dev. + + + +:::note +Because these errors existing in the code base and can cause noisy console outout, these warnings are [disabled](https://www.internalfb.com/code/www/[5b26a6bd37e8]/html/shared/core/WarningFilter.js?lines=559) in dev mode. +::: + + + + +## Client-side Deletion or Incomplete Update + +Imperative store updates, or optimistic updates could have deleted the record or field. If an imperative store update, or optimistic update, writes a new record to the store, it may not supply a value for a field which you expected to be able to read. This is a fundamental problem, since an updater cannot statically know all the data that might be accessed off of a new object. + +**🕵️‍♀️ How to tell:** Due to React and Relay’s batching, it’s not always possible to associate a component update with the store update that triggered it. Here, your best bet is to set a breakpoint in your component for when your value is null, and then use the Relay Dev Tools to look at the last few updates. + +This can happen due to a newly created object which did not supply a specific field or, as mentioned above, an update which causes a new or changed relationship in the graph. In this case, use the “How do tell” tip from that section. + + diff --git a/website/versioned_docs/version-v15.0.0/editor-support.md b/website/versioned_docs/version-v15.0.0/editor-support.md new file mode 100644 index 0000000000000..0c4e7810f7c79 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/editor-support.md @@ -0,0 +1,55 @@ +--- +id: editor-support +title: Editor Support +slug: /editor-support/ +keywords: +- LSP +- Language Server Protocol +- VS Code +- VSCode +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +*TL;DR: We have a [VS Code Extension](https://marketplace.visualstudio.com/items?itemName=meta.relay)* + +--- + +The Relay compiler has a rich understanding of the GraphQL embedded in your code. We want to use that understanding to imporve the developer experience of writing apps with Relay. So, starting in [v14.0.0](https://github.com/facebook/relay/releases/tag/v14.0.0), the new Rust Relay compiler can provide language features directly in your code editor. This means: + +#### Relay compiler errors surface as red squiggles directly in your editor + + + +#### Autocomplete throughout your GraphQL tagged template literals + + + +#### Hover to see type information and documentation about Relay-specific features + + + +#### `@deprecated` fields are rendered using ~~strikethrough~~ + + + +#### Click-to-definition for fragments, fields and types + + + +#### Quick fix suggestions for common errors + + + +## Language Server + +The editor support is implemented using the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) which means it can be used by a variety of editors, but in tandem with this release, [Terence Bezman](https://twitter.com/b_ez_man) from [Coinbase](https://www.coinbase.com/) has contributed an official VS Code extension. + +[**Find it here!**](https://marketplace.visualstudio.com/items?itemName=meta.relay) + +## Why Have a Relay-Specific Editor Extension? + +The GraphQL foundation has an official language server and [VS Code extension](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql) which provides editor support for GraphQL generically. This can provide a good baseline experience, but for Relay users, getting this information directly from the Relay compiler offers a number of benefits: + +* Relay compiler errors can surface directly in the editor as “problems”, often with suggested quick fixes +* Hover information is aware Relay-specific features and directives and can link out to relevant documentation diff --git a/website/versioned_docs/version-v15.0.0/error-reference/unknown-field.md b/website/versioned_docs/version-v15.0.0/error-reference/unknown-field.md new file mode 100644 index 0000000000000..e8e7099bf5f23 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/error-reference/unknown-field.md @@ -0,0 +1,36 @@ +--- +id: unknown-field +title: "Why Is My Field Not Found?" +slug: /error-reference/unknown-field/ +description: Get help figuring out why a given field is not found. +keywords: +- no such field on type +- missing +- field +- type +- compilation +- error +--- + +import {FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import DocsRating from '@site/src/core/DocsRating'; + +## [ERROR] Error in the project `some_project`: ✖︎ The type `Some_Type` has no field `some_unknown_field`. + +### Field name typo + +In case of a missing field in a type, the Relay compiler tries to find and suggest a field replacement. For example: ```Error in the project `some_project`: ✖︎ The type `UserInfo` has no field `mail`. Did you mean `email`?``` + + + +### The field exists in another schema + +Relay Compiler uses schemas in order to resolve types and their fields. The type's schema is determined by the file path and the mapping from file path to schema, which is configured in the "schema" or "schemaDir" properties of your Relay compiler config. If you expect this field to exist, make sure you're using the right schema. + +:::note +At meta there are various project config files that are listed [here](https://www.internalfb.com/intern/wiki/Relay-team/Rust_compiler_resources/#where-are-the-project-co). +::: + + + + diff --git a/website/versioned_docs/version-v15.0.0/getting-started/installation-and-setup.md b/website/versioned_docs/version-v15.0.0/getting-started/installation-and-setup.md new file mode 100644 index 0000000000000..57a685e534981 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/getting-started/installation-and-setup.md @@ -0,0 +1,150 @@ +--- +id: installation-and-setup +title: Installing in a Project +slug: /getting-started/installation-and-setup/ +description: Relay installation and setup guide +keywords: +- installation +- setup +- compiler +- babel-plugin-relay +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +# Installation + +In many situations, the easiest way to install Relay is with the `create-relay-app` package written by Tobias Tengler. Contrary to what the name suggests, this package *installs* Relay on your existing app. + +It currently supports apps built on Next, Vite, and Create React App. If you aren't on one of those platforms, or if it doesn't work for you for some reason, proceed to the manual steps below. + +To run it, make sure you have a clean working directory and run: + +``` +npm create @tobiastengler/relay-app +``` + +(or use `yarn` or `pnpm` instead of `npm` as you prefer). + +When it's done it will print some "Next Steps" for you to follow. + +More details about this script can be found at its [GitHub repository](https://github.com/tobias-tengler/create-relay-app). + +* * * + +# Manual Installation + +Install React and Relay using `yarn` or `npm`: + +```sh +yarn add react react-dom react-relay +``` + +## Set up the compiler + +Relay's ahead-of-time compilation requires the [Relay Compiler](../../guides/compiler/), which you can install via `yarn` or `npm`: + +```sh +yarn add --dev relay-compiler +``` + +This installs the bin script `relay-compiler` in your node_modules folder. It's recommended to run this from a `yarn`/`npm` script by adding a script to your `package.json` file: + +```js +"scripts": { + "relay": "relay-compiler" +} +``` + +## Compiler configuration + +Create the configuration file: + +```javascript +// relay.config.js +module.exports = { + // ... + // Configuration options accepted by the `relay-compiler` command-line tool and `babel-plugin-relay`. + src: "./src", + language: "javascript", // "javascript" | "typescript" | "flow" + schema: "./data/schema.graphql", + excludes: ["**/node_modules/**", "**/__mocks__/**", "**/__generated__/**"], +} +``` + +This configuration also can be specified in `"relay"` section of the `package.json` file. +For more details, and configuration options see: [Relay Compiler Configuration](https://github.com/facebook/relay/tree/main/packages/relay-compiler) + + +## Set up babel-plugin-relay + +Relay requires a Babel plugin to convert GraphQL to runtime artifacts: + +```sh +yarn add --dev babel-plugin-relay graphql +``` + +Add `"relay"` to the list of plugins your `.babelrc` file: + +```javascript +{ + "plugins": [ + "relay" + ] +} +``` + +Please note that the `"relay"` plugin should run before other plugins or +presets to ensure the `graphql` template literals are correctly transformed. See +Babel's [documentation on this topic](https://babeljs.io/docs/plugins/#pluginpreset-ordering). + +Alternatively, instead of using `babel-plugin-relay`, you can use Relay with [babel-plugin-macros](https://github.com/kentcdodds/babel-plugin-macros). After installing `babel-plugin-macros` and adding it to your Babel config: + +```javascript +const graphql = require('babel-plugin-relay/macro'); +``` + +## Running the compiler + +After making edits to your application files, just run the `relay` script to generate new compiled artifacts: + +```sh +yarn run relay +``` + +Alternatively, you can pass the `--watch` option to watch for file changes in your source code and automatically re-generate the compiled artifacts (**Note:** Requires [watchman](https://facebook.github.io/watchman) to be installed): + +```sh +yarn run relay --watch +``` + +For more details, check out our [Relay Compiler docs](../../guides/compiler/). + +## JavaScript environment requirements + +The Relay packages distributed on NPM use the widely-supported ES5 +version of JavaScript to support as many browser environments as possible. + +However, Relay expects modern JavaScript global types (`Map`, `Set`, +`Promise`, `Object.assign`) to be defined. If you support older browsers and +devices which may not yet provide these natively, consider including a global +polyfill in your bundled application, such as [core-js][] or +[@babel/polyfill](https://babeljs.io/docs/usage/polyfill/). + +A polyfilled environment for Relay using [core-js][] to support older browsers +might look like: + +```javascript +require('core-js/es6/map'); +require('core-js/es6/set'); +require('core-js/es6/promise'); +require('core-js/es6/object'); + +require('./myRelayApplication'); +``` + +[core-js]: https://github.com/zloirock/core-js + + + diff --git a/website/versioned_docs/version-v15.0.0/getting-started/prerequisites.md b/website/versioned_docs/version-v15.0.0/getting-started/prerequisites.md new file mode 100644 index 0000000000000..2c8cdd98a88c6 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/getting-started/prerequisites.md @@ -0,0 +1,49 @@ +--- +id: prerequisites +title: Prerequisites +slug: /getting-started/prerequisites/ +description: Prerequisites for setting up Relay +keywords: +- prerequisites +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + + +Before getting started with Relay, bear in mind that we assume that the following infrastructure has already been set up, as well as some level of familiarity with the topics below. + +## JavaScript + +Relay is a framework built in JavaScript, so we assume familiarity with the JavaScript language. + +## React + +Relay is a framework for data management with the primary supported binding for React applications, so we assume that you are already familiar with [React](https://reactjs.org/). + +## GraphQL + +We also assume basic understanding of [GraphQL](http://graphql.org/learn/). In order to start using Relay, you will also need: + +### A GraphQL Schema + +A description of your data model with an associated set of resolve methods that know how to fetch any data your application could ever need. + +GraphQL is designed to support a wide range of data access patterns. In order to understand the structure of an application's data, Relay requires that you follow certain conventions when defining your schema. These are documented in the [GraphQL Server Specification](../../guides/graphql-server-specification). + +- **[graphql-js](https://github.com/graphql/graphql-js)** on [npm](https://www.npmjs.com/package/graphql) + + General-purpose tools for building a GraphQL schema using JavaScript + +- **[graphql-relay-js](https://github.com/graphql/graphql-relay-js)** on [npm](https://www.npmjs.com/package/graphql-relay) + + JavaScript helpers for defining connections between data, and mutations, in a way that smoothly integrates with Relay. + +### A GraphQL Server + +Any server can be taught to load a schema and speak GraphQL. Our [examples](https://github.com/relayjs/relay-examples) use Express. + +- **[express-graphql](https://github.com/graphql/express-graphql)** on [npm](https://www.npmjs.com/package/express-graphql) + + + diff --git a/website/versioned_docs/version-v15.0.0/getting-started/step-by-step-guide.md b/website/versioned_docs/version-v15.0.0/getting-started/step-by-step-guide.md new file mode 100644 index 0000000000000..33b519abb5284 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/getting-started/step-by-step-guide.md @@ -0,0 +1,314 @@ +--- +id: step-by-step-guide +title: Server Setup Example +slug: /getting-started/step-by-step-guide/ +description: Step-by-step guide for setting up Relay +keywords: +- setup +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + + +Relay is a framework for managing and declaratively fetching GraphQL data. It allows developers to declare *what* data each component needs via GraphQL, and then aggregate these dependencies and efficiently fetch the data in fewer round trips. In this guide we'll introduce the key concepts for using Relay in a React app one at a time. + +## Step 1: Create React App + +For this example we'll start with a standard install of [Create React App](https://create-react-app.dev). Create React App makes it easy to get a fully configured React app up and running and also supports configuring Relay. To get started, create a new app with: + +```bash +# NPM +npx create-react-app your-app-name + +# Yarn +yarn create react-app your-app-name +``` + +At this point we should be able to change to the app's directory and run it: + +```bash +# NPM +cd your-app-name +npm start + +# Yarn +cd your-app-name +yarn start +``` + +For troubleshooting and more setup options, see the [Create React App documentation](https://create-react-app.dev/docs/getting-started). + +## Step 2: Fetch GraphQL (without Relay) + +If you're exploring using GraphQL with Relay, we highly recommend starting with a basic approach and using as few libraries as possible. GraphQL servers can generally be accessed using plain-old HTTP requests, so we can use the [`fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to request some data from our server. For this guide we'll use GitHub's GraphQL API as the server, but if you already have a GraphQL server feel free to use that instead. + +### 2.1: GitHub GraphQL Authentication + +To start we'll need an authentication token for the GitHub API (if you're using your own GraphQL endpoint, you can skip this step): + +* Open [github.com/settings/tokens](https://github.com/settings/tokens). +* Ensure that at least the `repo` scope is selected. +* Generate a token +* Create a file `./your-app-name/.env.local` and add the following contents, replacing `` with your authentication token: + +``` +# your-app-name/.env.local +REACT_APP_GITHUB_AUTH_TOKEN= +``` + +### 2.2: A fetchGraphQL Helper + +Next let's update the home screen of our app to show the name of the Relay repository. We'll start with a common approach to fetching data in React, calling our fetch function after the component mounts (note: later we'll see some limitations of this approach and a better alternative that works with React Concurrent Mode and Suspense). First we'll create a helper function to load data from the server. Again, this example will use the GitHub API, but feel free to replace it with the appropriate URL and authentication mechanism for your own GraphQL server: + +```javascript +// your-app-name/src/fetchGraphQL.js +async function fetchGraphQL(text, variables) { + const REACT_APP_GITHUB_AUTH_TOKEN = process.env.REACT_APP_GITHUB_AUTH_TOKEN; + + // Fetch data from GitHub's GraphQL API: + const response = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + Authorization: `bearer ${REACT_APP_GITHUB_AUTH_TOKEN}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: text, + variables, + }), + }); + + // Get the response as JSON + return await response.json(); +} + +export default fetchGraphQL; +``` + +### 2.3: Fetching GraphQL From React + +Now we can use our `fetchGraphQL` function to fetch some data in our app. Open `src/App.js` and edit it as follows: + +```javascript +// your-app-name/src/App.js +import React from 'react'; +import './App.css'; +import fetchGraphQL from './fetchGraphQL'; + +const { useState, useEffect } = React; + +function App() { + // We'll load the name of a repository, initially setting it to null + const [name, setName] = useState(null); + + // When the component mounts we'll fetch a repository name + useEffect(() => { + let isMounted = true; + fetchGraphQL(` + query RepositoryNameQuery { + # feel free to change owner/name here + repository(owner: "facebook" name: "relay") { + name + } + } + `).then(response => { + // Avoid updating state if the component unmounted before the fetch completes + if (!isMounted) { + return; + } + const data = response.data; + setName(data.repository.name); + }).catch(error => { + console.error(error); + }); + + return () => { + isMounted = false; + }; + }, []); + + // Render "Loading" until the query completes + return ( +
+
+

+ {name != null ? `Repository: ${name}` : "Loading"} +

+
+
+ ); +} + +export default App; +``` + +## Step 3: When To Use Relay + +At this point we can fetch data with GraphQL and render it with React. This is a reasonable solution that can be sufficient for many apps. However, this approach doesn't necessarily scale. As our app grows in size and complexity, or the number of people working on the app grows, a simple approach like this can become limiting. Relay provides a number of features designed to help keep applications fast and reliable even as they grow in size and complexity: collocating data dependencies in components with GraphQL fragments, data consistency, mutations, etc. Check out [Thinking in GraphQL](../../principles-and-architecture/thinking-in-graphql/) and [Thinking in Relay](../../principles-and-architecture/thinking-in-relay/) for an overview of how Relay makes it easier to work with GraphQL. + + +## Step 4: Adding Relay To Our Project + +In this guide we'll demonstrate installing the *experimental* release of Relay Hooks, a new, hooks-based Relay API that supports React Concurrent Mode and Suspense. + +First we'll add the necessary packages. Note that Relay is comprised of three key pieces: a compiler (which is used at build time), a core runtime (that is React agnostic), and a React integration layer. + +```bash +# NPM Users +npm install --save relay-runtime react-relay +npm install --save-dev relay-compiler babel-plugin-relay + +# Yarn Users +yarn add relay-runtime react-relay +yarn add --dev relay-compiler babel-plugin-relay +``` + +### 4.1 Configure Relay Compiler + +Next let's configure Relay compiler. We'll need a copy of the schema as a `.graphql` file. If you're using the GitHub GraphQL API, you can download a copy directly from the Relay example app: + +```bash +cd your-app-name +curl https://raw.githubusercontent.com/relayjs/relay-examples/main/issue-tracker/schema/schema.graphql > schema.graphql +``` +*Note:* On Windows, the `.graphql` file has to be explicitly saved with UTF-8 encoding, not the default UTF-16. See this [issue](https://github.com/prisma-labs/get-graphql-schema/issues/30) for more details. + +If you're using your own API we suggest using the [`get-graphql-schema`](https://www.npmjs.com/package/get-graphql-schema) utility to download your schema into a `.graphql` file. + +Now that we have a schema we can modify `package.json` to run the compiler first whenever we build or start our app: + +```json +// your-app-name/package.json +{ + ... + "scripts": { + ... + "start": "yarn run relay && react-scripts start", + "build": "yarn run relay && react-scripts build", + "relay": "yarn run relay-compiler" + ... + }, + "relay": { + "src": "./src/", + "schema": "./schema.graphql", + "language": "javascript" + } + ... +} +``` + +At this point, you should be able to run the following successfully: + +```bash +cd your-app-name +yarn start +``` + +If it works, your app will open at [localhost:3000](http://localhost:3000). Now when we write GraphQL in our app, Relay will detect it and generate code to represent our queries in `your-app-name/src/__generated__/`. We recommend checking in these generated files to source control. + +### 4.2 Configure Relay Runtime + +Now that the compiler is configured we can set up the runtime - we have to tell Relay how to connect to our GraphQL server. We'll reuse the `fetchGraphQL` utility we built above. Assuming you haven't modified it (or at least that it still takes `text` and `variables` as arguments), we can now define a Relay `Environment`. An `Environment` encapsulates how to talk to our server (a Relay `Network`) with a cache of data retrieved from that server. We'll create a new file, `src/RelayEnvironment.js` and add the following: + +```javascript +// your-app-name/src/RelayEnvironment.js +import {Environment, Network, RecordSource, Store} from 'relay-runtime'; +import fetchGraphQL from './fetchGraphQL'; + +// Relay passes a "params" object with the query name and text. So we define a helper function +// to call our fetchGraphQL utility with params.text. +async function fetchRelay(params, variables) { + console.log(`fetching query ${params.name} with ${JSON.stringify(variables)}`); + return fetchGraphQL(params.text, variables); +} + +// Export a singleton instance of Relay Environment configured with our network function: +export default new Environment({ + network: Network.create(fetchRelay), + store: new Store(new RecordSource()), +}); +``` + +## Step 5: Fetching a Query With Relay + +Now that Relay is installed and configured we can change `App.js` to use it instead. We'll prepare our data as the app starts, and wait for it to be ready in ``. Replace the contents of `src/App.js` with the following: + +```javascript +import React from 'react'; +import './App.css'; +import graphql from 'babel-plugin-relay/macro'; +import { + RelayEnvironmentProvider, + loadQuery, + usePreloadedQuery, +} from 'react-relay/hooks'; +import RelayEnvironment from './RelayEnvironment'; + +const { Suspense } = React; + +// Define a query +const RepositoryNameQuery = graphql` + query AppRepositoryNameQuery { + repository(owner: "facebook", name: "relay") { + name + } + } +`; + +// Immediately load the query as our app starts. For a real app, we'd move this +// into our routing configuration, preloading data as we transition to new routes. +const preloadedQuery = loadQuery(RelayEnvironment, RepositoryNameQuery, { + /* query variables */ +}); + +// Inner component that reads the preloaded query results via `usePreloadedQuery()`. +// This works as follows: +// - If the query has completed, it returns the results of the query. +// - If the query is still pending, it "suspends" (indicates to React that the +// component isn't ready to render yet). This will show the nearest +// fallback. +// - If the query failed, it throws the failure error. For simplicity we aren't +// handling the failure case here. +function App(props) { + const data = usePreloadedQuery(RepositoryNameQuery, props.preloadedQuery); + + return ( +
+
+

{data.repository.name}

+
+
+ ); +} + +// The above component needs to know how to access the Relay environment, and we +// need to specify a fallback in case it suspends: +// - tells child components how to talk to the current +// Relay Environment instance +// - specifies a fallback in case a child suspends. +function AppRoot(props) { + return ( + + + + + + ); +} + +export default AppRoot; +``` + +Note that you'll have to restart the app - `yarn start` - so that Relay compiler can see the new query and generate code for it. See the [Relay Compiler setup docs](../installation-and-setup/#set-up-relay-compiler) for how to run Relay Compiler in watch mode, to regenerate code as you modify queries. + +## Step 6: Explore! + +At this point we have an app configured to use Relay. We recommend checking out the following for information and ideas about where to go next: + +* The [Guided Tour](../../guided-tour/) describes how to implement many common use-cases. +* The [API Reference](../../api-reference/use-fragment/) has full details on the Relay Hooks APIs. +* The [Example App](https://github.com/relayjs/relay-examples/tree/main/issue-tracker) is a more sophisticated version of what we've started building here. It includes routing integration and uses React Concurrent Mode and Suspense for smooth transitions between pages. + + + diff --git a/website/versioned_docs/version-v15.0.0/glossary/glossary.md b/website/versioned_docs/version-v15.0.0/glossary/glossary.md new file mode 100644 index 0000000000000..07c3b7816cd26 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/glossary/glossary.md @@ -0,0 +1,967 @@ +--- +id: glossary +title: Glossary +slug: /glossary/ +description: Relay terms glossary +keywords: +- glossary +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly, OssOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +## 3D + +Data-Driven Dependencies. Facebook's way of including the code to render a particular component if and only if it will actually be rendered. Canonical use cases are + +* **Fields that are typically null**, and which are only rendered when not null. +* **Unions**. For example, the core news feed item has many different variants, each of which is a separate React component. Which one we render depends on the data (i.e. is "data-driven"). On a given feed, it is likely that most variants will not be rendered, and need not be downloaded. +* **Component can have different rendering strategies, depending on the data.** + + + +See the [@match](#match) directive, [@module](#module) directive and [the 3D guide](../guides/data-driven-dependencies). + + + + + +See the [@match](#match) directive and the [@module](#module) directive. + + + +## Abstract Type + +GraphQL unions and interfaces are abstract types. See [interface](#interface-graphql). + +## Abstract Type Refinement + +See [type refinement](#type-refinement). If type refinement is a way of conditionally including fields if a type implements a particular concrete type (such as `... on User { name }`), abstract type refinement refers to conditionally including fields if a type implements a particular abstract type (i.e. interface). So, `... on Actor { field }`. + +## @arguments + +A [directive](#directive) that modifies a [fragment spread](#fragment-spread) and is used to pass arguments (defined with [`@argumentDefinitions`](#argumentdefinitions)) to that fragment. + +```graphql +...Story_story @arguments(storyId: "1234") +``` + +## @argumentDefinitions + +A directive that modifies a fragment definition and defines the names of the local arguments that the fragment can take, as well as their type. + +```graphql +fragment Store_story on Story + @argumentDefinitions(storyId: {type: "ID!"}) { + # etc +} +``` + +If a variable is used in a fragment but not included in an `@argumentDefinitions` directive, Relay will require that the fragment is only spread in queries which declare these variables, or in fragments which ultimately are spread in such a query. + +Compare with [variables](#variables) and see the [relevant section](../guided-tour/rendering/variables) in the guided tour. + +## Artifact + +Files that are generated by the Relay compiler, typically ending in `.graphql.js`. + + + +See [a guide to Relay artifacts](https://www.internalfb.com/intern/wiki/Relay-team/generated-artifacts/). + + + +## AST + +Abstract Syntax Tree. In Relay, there are two types of ASTs, [normalization](#normalization-ast) and [reader](#reader-ast) ASTs. + +The default export of a `.graphql.js` file is an AST. + +The Relay compiler parses and transforms GraphQL literals, and generates Relay ASTs (see [artifact](#artifact)). Doing this work at compile time allows the Relay runtime to be faster. + +## Availability + +The concept of availability refers to whether there is enough non-stale, non-invalidated data in the store to fulfill a particular request immediately, or whether a request to server needs to be made in order to fulfill that request. + +## Babel Transform + +A build-time transformation of the Javascript codebase, which turns calls to + +```javascript +graphql`...` +``` + +into `require(NAME_OF_GENERATED_ARTIFACT)` calls. + +## Client Schema Extension + +[The GraphQL spec](https://spec.graphql.org/June2018/#sec-Schema-Extension) allow you to define new types, new fields on types, new directives, etc. locally. + +Relay supports adding types and fields in client schema extension files. Developers use this feature to add fields that contain purely local state that is associated with items on the graph. For example, an `is_selected` field on a `User`. + +## CacheConfig + +A value used to control how a query's response may be cached. Ultimately passed to `environment.execute`. + +## Check + +One of the core functions of the store. Given an operation, determines whether the store has all of the data necessary to render that operation. Calls `DataChecker.check`, which synchronously starts with the root node associated with the operation and walks the data in the store. + +In practice, exposed as a method on `environment`. + +In conjunction with the fetch policy, used by `loadQuery` (and other methods) to determine whether it is necessary to make a network request call to fulfill a query. + +## Commit + +After receiving a network response, the payload is committed, or written to the store. + +Commit is also the verb used to describe initiating a mutation and writing its data to the store. + +## Compiler + +The piece of code which scans your Javascript files for `graphql` tagged nodes and generates the appropriate files (`.graphql.js` files, `$Parameters.js` files, etc.) + +The generated output from the compiler is committed and checked into the repository. + +## Concrete Request + +An Abstract Syntax Tree representing a query, subscription or mutation. + +The default export of a `.graphql.js` file corresponding to a query, subscription or mutation. + +In addition, calls to `graphql`...`` are turned into concrete requests at build time via the Relay Babel transform. + +**See the important safety notes at Preloadable Concrete Request.** + +## Config + +A file or javascript object which controls, among other things, which files are scanned by the Relay [compiler](#compiler) for your project. + +## @connection + +A directive which declares that a field implements the [connection](#connection) spec. + +## Connection + + +A field implementing the connection spec. See here for more details on the spec, and the section of the guided tour on rendering list data and pagination. + + + +A field implementing the connection spec. See the section of the guided tour on rendering list data and pagination. + + +See also [`usePaginationFragment`](../api-reference/use-pagination-fragment). + +## Container + +A term for a higher order component that provided a child component with the data from queries and fragments. Associated with Relay Modern. + +You should use the Relay hooks API when possible. + +## Data Checker + +A class exposing a single method, `check`, which synchronously starts with the root node associated with the operation and walks the data in the store. It determines whether the data in the store suffices to fulfill a given operation. + +Called by `store.check`. + +## DataID + +The globally-unique identifier of a record. Can be generated on the client with [missing field handlers](#missing-field-handler). Usually corresponds to an Ent's ID (if available), but guaranteed to equal the value of the `__id` field. + +[`updater`](#updater) and [`optimisticUpdater`](#optimisticupdater) functions are passed instances of [`RelaySourceSelectorProxy`](#recordproxy). Calling `.get(id)` with the DataID on a `RelaySourceSelectorProxy` will look up that item in the store, and return a proxy of it. + +## Data Masking + +Refers to the idea that a component should not be able to access any data it does declare in its fragment or query, even inadvertently. This prevents accidental coupling of data between components, and means that every component can be refactored in isolation. It negates the risk that removing a field in a child component will accidentally break a different component, allowing components to *move fast, with stable infrastructure*. + +Also refers to the practice of hiding the data of child components from their parents, in keeping with the idea. + +In Relay, a query declared like `query FooQuery { viewer { ...subcomponent_``viewer_name } }` will not be able to access the data declared by `subcomponent_viewer_name` without access to the `ReaderFragment` representing the `subcomponent_viewer_name` fragment. + +See the [Thinking in Relay guide](../principles-and-architecture/thinking-in-relay#data-masking). + +## @defer + +A directive which can be added to a fragment spread or inline fragment to avoid blocking on that fragment's data. + + +See the [documentation](https://www.internalfb.com/intern/wiki/Relay/Web/incremental-data-delivery-defer-stream/#defer). + + +## Definition + +In the compiler, a definition refers to the text within a GraphQL literal where an operation or fragment was defined. + +## Descriptor + +Can refer to an `OperationDescriptor` or `RequestDescriptor`. Descriptors are types used internally to the Relay codebase, and generally, refer to an object containing the minimum amount of information needed to uniquely identify an operation or request, such as (for a `RequestIdentifier`), a node, identifier and variables. + +## DevTools + +An awesome Chrome extension for debugging Relay network requests, the Relay store and Relay events. Helpful for answering questions like "Why am I not seeing the data I expect to see?" "Why did this component suspend?" etc. + +See the [documentation](https://relay.dev/docs/debugging/relay-devtools/). + +## Document + +In the compiler, a Document refers to a GraphQL literal that contains one or more operation or fragment [definitions](#definition). Relay requires that GraphQL literals in JavaScript files contain a single definition. + +## Directive + +A special instruction, starting with `@` and contained in a `graphql` literal or graphql file, which provides special instructions to the relay compiler or to the server. Examples include `@defer`, `@stream` and `@match`. + +## Disposable + +Any object which contains a `.dispose` method which takes no parameters and provides no return value. Many objects in Relay (such query references and entrypoint references) and the return value of many methods (such as calls to `.subscribe` or `.retain`) are disposables. + +## Entrypoint + +A lightweight object containing information on the components which need to be loaded (as in the form of calls to `JSResource`) and which queries need to be loaded (in the form of preloadable concrete requests) before a particular route, popover or other piece of conditionally loaded UI can be rendered. + +All queries which are required for the initial rendering of a piece of UI should be included in that UI's entrypoint. + +Entrypoints can contain queries and other entrypoints. + +See also [preloadable concrete request](#preloadable-concrete-request) and [JSResource](#jsresource). + +## Environment + +An object bringing together many other Relay objects, most importantly a store and a network. Also, includes a publish queue, operation loader, scheduler and [missing fields handlers](#missing-field-handler). + +Set using a `RelayEnvironmentProvider` and passed down through React context. + +All non-internal Relay hooks require being called within a Relay environment context. + +## Execute + +Executing a query, mutation or subscription (collectively, an operation) roughly means "create a lazy observable that, when subscribed to, will make a network request fulfilling the operation and write the returned data to the store." + +A variety of `execute` methods are exposed on the Relay environment. + +## Fetch Policy + +A string that determines in what circumstances to make a network request in which circumstances to fulfill the query using data in the store, if available. Either `network-only`, `store-and-network`, `store-or-network` or `store-only`. (Some methods do not accept all fetch policies.) + +## Field + +Basically, anything you can select using a query, mutation, subscription or fragment. For example, `viewer`, `comment_create(input: $CommentCreateData)` and `name` are all fields. + +The GraphQL schema comprises many fields. + +## Fragment + +Fragment is an overloaded term, and has at least two distinct meanings in Relay. + +### Fragments as a GraphQL concept + +The fundamental reusable unit of GraphQL. Unlike queries, subscriptions and mutations, fragments cannot be queried on their own and must be embedded within a request. + +Fragments can be spread into queries, mutations, subscriptions and other fragments. + +Fragments can be standalone (as in `fragment Component_user on User { name }`) or inline, as in the `... on User { name }` in `query MyQuery { node(id: $id) { ... on User { name } } }`. + +Fragments are always defined on a particular type (`User` in the example), which defines what fields can be selected within it. + +### Fragments within Relay + +Within Relay, a fragment refers to the fields that are read out for a given fragment/operation. The term is also used colloquially to refer to reader ASTs. So, e.g. the following query and fragment might have identical reader ASTs: + +```graphql +query Foo { + actor { name } +} +``` + +``` +fragment Bar on Query { + actor { name } +} +``` + +## Fragment Identifier + +A string, providing enough information to provide the data for a particular fragment. For example: + +`1234{"scale":2}/Story_story/{"scale":2}/"4567"` + +This identifies by its persist ID (`1234`), followed by the variables it accepts, followed by the `Story_story` fragment (which does not have a persist id) and the variables it uses, followed by the Data ID (likely, the `id` field) of whatever Story happened to be referenced. + +## Fragment Reference + +A parameter passed to `useFragment`. Obtained by accessing the value onto which a fragment was spread in another [query](#query), fragment, subscription or mutation. For example, + +```javascript +const queryData = usePreloadedQuery( + graphql`query ComponentQuery { viewer { account_user { ...Component_name } } }`, + {}, +); + +// queryData.viewer is the FragmentReference +// Though this would usually happen in another file, you can +// extract the value of Component_name as follows: +const fragmentData = useFragment( + graphql`fragment Component_name on User { name }`, + queryData?.viewer?.account_user, +); +``` + +Just like a query reference and a graphql tagged literal describing a query (i.e. a concrete request) can be used to access the data from a query, a fragment reference and a graphql tagged literal describing a fragment (i.e. a reader fragment) can be used to access the data referenced from a fragment. + +## Fragment Resource + +An internal class supporting lazily loaded queries. Exposes two important methods: + +* `read`, which is meant to be called during a component's render phase. It will attempt to fulfill a query from the store (by calling `environment.lookup`) and suspend if the data is not available. It furthermore writes the results from the attempted read (whether a promise, error or result) to an internal cache, and updates that cached value when the promise resolves or rejects. +* `subscribe`, which is called during the commit phase, and establishes subscriptions to the relay store. + +If the component which calls `.read` successfully loads a query, but suspends on a subsequent hook before committing, the data from that query can be garbage collected before the component ultimately renders. Thus, components which rely on `FragmentResource` are at risk of rendering null data. + +Compare to [query resource](#query-resource). + +## Fragment Spec Resolver + +TODO + +## Fragment Spread + +A fragment spread is how one fragment is contained in a query, subscription, mutation or other fragment. In the following example, `...Component_name` is a fragment spread: + +```graphql +query ComponentQuery { + viewer { + account_user { + ...Component_name + } + } +} +``` + +In order for a fragment to be spread in a particular location, the types must match. For example, if `Component_name` was defined as follows: `fragment Component_name on User { name }`, this spread would be valid, as `viewer.account_user` has type `User`. + +## Garbage Collection + +Relay can periodically garbage collect data from queries which are no longer being retained. + +See more information in the [guided tour](https://relay.dev/docs/guided-tour/reusing-cached-data/presence-of-data/#garbage-collection-in-relay). + +## GraphQLTaggedNode + +This is the type of the call to + +```js +graphql`...` +``` + +It is the union of ReaderFragment, ReaderInlineDataFragment, ConcreteRequest, and ConcreteUpdatableQuery. + + + +Note that Flow can be configured to understand that the type of a GraphQL literal is the type of the default export of the generated `.graphql.js` file. + + + + + +Note that Flow is configured to understand that the type of a GraphQL literal is the type of the default export of the generated `.graphql.js` file. + + + +## Handler + +TODO + +## ID + +Relay treats ids specially. In particular, it does the following two things: + +* The compiler automatically adds a selection of the `id` field on every type where the `id` field has type `ID` or `ID!`. +* When [normalizing](#normalization) data, if an object has an `id` property, that field is used as its ID in the store. + +There are types in the schema where the `id` field does not have type `ID` or `ID!` (e.g. has the type `string` or `number`). If a user selects this field themselves, this field is used as an id. This is unexpected and incorrect behavior. + +## @include + +A directive that is added to fields, inline fragments and fragment spreads, and allows for conditional inclusion. It is the opposite of the [`@skip`](#skip) directive. + +In the compiler, the `@include`/`@skip` directives are treated specially, and produce `Condition` nodes. + +## @inline + +A directive that applies to fragments which enables developers to pass masked data to functions that are executed outside of the React render phase. + +Normally, data is read out using `useFragment`. However, this function can only be called during the render phase. If store data is needed in a outside of the render phase, a developer has several options: + +* read that data during the render phase, and pass it to the function/have the function close over that data. (See also [#relay]) +* pass a reference to an `@inline` fragment, which can then be accessed (outside of the render phase) using the `readInlineData` directive. + +This directive causes them to be read out when the parent fragment is read out, and unmasked by the call to `readInlineData`. + +## Interface (GraphQL) + +An *Interface* is an abstract type that includes a certain set of fields that a type must include to implement the interface. + +You can spread an fragment on an interface onto a concrete type (for example `query MyQuery { viewer { account_user { ...on Actor { can_viewer_message } } }`) or a fragment on a concrete type onto an interface (for example `query MyQuery { node(id: 4) { ... on User { name } } }`). You are no longer allowed to spread a fragment on an interface onto an interface. + +See also abstract type refinement. + +## Invalidation + +In certain cases, it is easy to determine the outcome of a mutation. For example, if you "like" a Feedback, the like count will increment and `viewer_did_like` will be set to true. However, in other cases, such as when you are blocking another user, the full impact on the data in your store is hard to determine. + +For situations like these, Relay allows you to invalidate a record (or the whole store), which will cause the data to be re-fetched the next time it is rendered. + +See the [section in the guide](https://relay.dev/docs/guided-tour/reusing-cached-data/staleness-of-data/). + +## JSResource + +A lightweight API for specifying a that a React component should be loaded on demand, instead of being bundled with the first require (as would be the case if you imported or required it directly.) + +This API is safe to use in entrypoint files. + + +See [the npm module](https://www.npmjs.com/package/jsresource). + + +## Lazy Loading + +A query or entry point is lazy loaded if the request for the data occurs at render time. + +Lazy loaded queries and entry points have performance downsides, are vulnerable to being over- and under-fetched, and can result in components being rendered with null data. They should be avoided. + +## Linked Record + +A linked record is a record that is directly accessible from another record. For example, in the query `query MyQuery { viewer { account_user { active_instant_game { id } } } }`, `active_instant_game` (which has the type `Application` is a linked record of `account_user`. + +A linked record cannot be queried by itself, but must be queried by selecting subfields on it. + +Compare to [value](#value). + +## Literal + +A GraphQL literal is a call to + +```javascript +graphql`...` +``` + +in your code. These are pre-processed, and replaced at build time with a [GraphlQLTaggedNode](#graphqltaggednode) containing an [AST](#ast) representation of the contents of the literal. + +## Lookup + +One of the main methods exposed by the Relay store. Using a [reader selector](#reader-selector), traverses the data in the store and returns a [snapshot](#snapshot), which contains the data being read, as well as information about whether data is missing and other pieces of information. Also exposed via the Relay environment. + +Calls [`Reader.read`](#reader). + +## @match + +A directive that, when used in combination with [@module](#module), allows users to download specific JS components alongside the rest of the GraphQL payload if the field decorated with @match has a certain type. See [3D](#3d). + +## MatchContainer + +A component that renders the component returned in conjunction with a field decorated with the [@match](#match) directive. See [3D](#3d). + +## Missing Field Handler + +A function that provides a [DataID](#dataid) for a field (for singular and plural linked fields) and default values (for scalar fields). + +For example, you may have already fetched an item with id: 4, and are executing a query which selects `node(id: 4)`. Without a missing field handler, Relay would not know that the item with id: 4 will be returned by `node(id: 4)`, and would thus attempt to fetch this data over the network. Providing a missing field handler can inform Relay that the results of this selection are present at id: 4, thus allowing Relay to avoid a network request. + +`getRelayFBMissingFieldHandlers.js` provides this and other missing field handlers. + +## @module + +A directive that, when used in combination with [@match](#match), allows users to specify which JS components to download if the field decorated with @match has a certain type. See [3D](#3d). + +## Module + +TODO + +## Mutation + +A mutation is a combination of two things: a mutation on the backend, followed by query against updated data. + + +See the [guide on mutations](../guided-tour/updating-data/graphql-mutations), and [this article](https://www.internalfb.com/intern/wiki/Graphql-for-hack-developers/mutation-root-fields/) on defining mutations in your hack code. + + + +See the [guide on mutations](../guided-tour/updating-data/graphql-mutations). + + +## Mutation Root Query + +The root object of a mutation query. In an `updater` or `optimisticUpdater`, calling `store.getRootField('field_name')` will return the object from the mutation root query named `field_name`. + +The fields exposed on this object are **not** the same as those available for queries, and differ across mutations. + +## Network + +Relay environments contain a `network` object, which exposes a single `execute` function. All network requests initiated by Relay will go through this piece of code. + +This provides a convenient place to handle cross-cutting concerns, like authentication and authorization. + +## Node + +TODO + +## Normalization + +Normalization is the process of turning nested data (such as the server response) and turning it into flat data (which is how Relay stores it in the store.) + +See the [response normalizer](#response-normalizer). + +## Normalization AST + +An [AST](#ast) that is associated with an [operation](#operation) that (in combination with [variables](#variables)) can be used to: +* write a network payload to the store, +* write an optimistic response to the store, +* determine whether a query can be fulfilled from data in the store, and +* determine which records in the store are reachable (used in [garbage collection](#garbage-collection)). + +Unlike the [reader AST](#reader-ast), the normalization AST includes information on the contents of nested fragments. + +The generated artifact associated with an operation (e.g. `FooQuery.graphql.js`) contains both a normalization AST and a reader AST. + +## Normalization Selector + +A selector defines the starting point for a traversal into the graph for the purposes of targeting a subgraph, combining a GraphQL fragment, variables, and the Data ID for the root object from which traversal should progress. + +## Notify + +A method exposed by the store which will notify each [subscriber](#subscribe) whose data has been modified. Causes components which are rendering data that has been modified to re-render with new data. + +## Observable + +The fundamental abstraction in Relay for representing data that may currently be present, but may also only be available in the future. + +Observables differ from promises in that if the data in an observable has already been loaded, you can access it synchronously as follows: + +```javascript +const completedObservable = Observable.from("Relay is awesome!"); +let valueFromObservable; +observable.subscribe({ + next: (value) => { + valueFromObservable = value; + /* this will execute in the same tick */ + }, +}); +console.log(valueFromObservable); // logs out "Relay is awesome!" +``` + +This is advantageous, as it allows Relay hooks to not suspend if data is already present in the store. + +In Relay, observables are a partial implementation of [RxJS Observables](https://rxjs-dev.firebaseapp.com/guide/observable). + +## Operation + +In [GraphQL](https://spec.graphql.org/June2018/#sec-Language.Operations), a query, subscription or mutation. + +In Relay, every operation also has an associated [fragment](#fragments-within-relay). So, an accurate mental model is that operations are fragments whose [type condition](#type-condition) is that they are on [Query/Mutation/Subscription](#root-type) and for which Relay knows how to make a network request. + +## Operation Descriptor + +Colloquially, an operation descriptor is an operation and variables. + +The operation descriptor flowtype contains the three pieces of information that Relay needs to work with the data: [a reader selector](#reader-selector), a [normalization selector](#normalization-selector) and a [request descriptor](#request-descriptor). + +The variables are filtered to exclude unneeded variables and are populated to include default values for missing variables, thus ensuring that requests that differ in irrelevant ways are cached using the same request ID. + +## Operation Mock Resolver + +A function taking an operation descriptor and returning a network response or error, used when testing. + +## Operation Tracker + +TODO + +## Optimistic Update + +TODO + +## Optimistic Updater + +TODO + +## Pagination + +Querying a list of data (a [connection](#connection)) in parts is known as pagination. + +See the [graphql docs](https://graphql.org/learn/pagination/) and our [guided tour](../guided-tour/list-data/pagination). + +## Payload + +The value returned from the GraphQL server as part of the response to a request. + +## Plural Field + +A field for which the value is an array of [values](#value) or [records](#record). + +## @preloadable + +A directive that modifies queries and which causes relay to generate `$Parameters.js` files and preloadable concrete requests. Required if the query is going to be used as part of an entry point. + +## Preloadable Concrete Request + +A small, lightweight object that provides enough information to initiate the query and fetch the full query AST (the `ConcreteRequest`.) This object will only be generated if the query is annotated with `@preloadable`, and is the default export of `$parameters.js` files. It is only generated for queries which are annotated with `@preloadable`. + +Unlike concrete requests (the default export of `.graphql.js` files), preloadable concrete requests are extremely light weight. + +Note that entrypoints accept either preloadable concrete requests or concrete requests in the `.queries[queryName].parameters` position. However, ***because a concrete request is not a lightweight object, you should only include preloadable concrete requests here.*** + +Note also that preloadable queries have `id` fields, whereas other queries do not. + +## Preloadable Query Registry + +A central registry which will execute callbacks when a particular Query AST (concrete request) is loaded. + +Required because of current limitations on dynamically loading components in React Native. + +## Project + +For Relay to process a file with a GraphQL literal, it must be included in a project. A project specifies the folders to which it applies and the schema against which to evaluate GraphQL literals, and includes other information needed by the Relay compiler. + + +Projects are defined in a single [config](#config) file, found [here](https://www.internalfb.com/intern/diffusion/WWW/browse/master/scripts/relay/compiler-rs/config.www.json) and [here](https://www.internalfb.com/intern/diffusion/FBS/browse/master/xplat/relay/compiler-rs/config.xplat.json). + + +## Profiler + +TODO + +## Publish + +One of the main methods exposed by the `store`. Accepts a [record source](#record-source), from which the records in the store are updated. Also updates the mapping of which records in the store have been updated as a result of publishing. + +One or more calls to `publish` should be followed by a call to [`notify`](#notify). + +## Publish Queue + +A class used internally by the environment to keep track of, apply and revert pending (optimistic) updates; commit client updates; and commit server responses. + +Exposes mutator methods like `commitUpdate` that only add or remove updates from the queue, as well as a `run` method that actually performs these updates and calls `store.publish` and `store.notify`. + +## Query + +A [GraphQL query](https://graphql.org/learn/queries/) is a request that can be sent to a GraphQL server in combination with a set of [variables](../guided-tour/rendering/variables), in order to fetch some data. It consists of a [selection](#selection) of fields, and potentially includes other [fragments](#fragment). + +## Query Executor + +A class that normalizes and publishes optimistic responses and network responses from a network observable to the store. + +After each response is published to the store, `store.notify` is called, updating all components that need to re-render. + +Used by `environment` in methods such as `execute`, `executeWithSource` and `executeMutation`, among others. + +## Query Reference + +TODO + +## Query Resource + +A class for helping with lazily loaded queries and exposing two important methods: `prepare` and `retain`. + +* `prepare` is called during a component's render method, and will either read an existing cached value for the query, or fetch the query and suspend. It also stores the results of the attempted read (whether the data, a promise for the data or an error) in a local cache. +* `retain` is called after the component has successfully rendered. + +If the component which calls `.prepare` successfully loads a query, but suspends on a subsequent hook before committing, the data from that query can be garbage collected before the component ultimately renders. Thus, components which rely on `QueryResource` are at risk of rendering null data. + +Compare to [fragment resource](#fragment-resource). + +## `@raw_response_type` + +A directive added to queries which tells Relay to generate types that cover the `optimisticResponse` parameter to `commitMutation`. + + +See the [documentation](../guided-tour/updating-data/local-data-updates) for more. + +## Reader + +TODO this section + +## Reader AST + +An [AST](#AST) that is used to read the data selected in a given fragment. + +Both [operations](#operation) and [fragments](#fragment) have reader ASTs. + +A reader AST contains information about which fragments are spread at a given location, but unlike a [normalization AST](#normalization-ast), does not include information about the fields selected within these fragments. + +## Reader Fragment + +TODO + +See [GraphlQLTaggedNode](#graphqltaggednode). + +## Reader Selector + +An object containing enough information for the store to traverse its data and construct an object represented by a query or fragment. Intuitively, this "selects" a portion of the object graph. + +See also [lookup](#lookup). + +## Record + +A record refers to any item in the Relay [store](#store) that is stored by [ID](#id). [Values](#value) are not records; most everything else is. + +## Record Source + +An abstract interface for storing [records](#record), keyed by [DataID](#dataid), used both for representing the store's cache for updates to it. + +## Record Source Selector Proxy + +See [record proxy](#record-proxy). + +## Record Proxy + +See the [store documentation](../api-reference/store). + +## Ref Counting + +The pattern of keeping track of how many other objects can access a particular object, and cleaning it up or disposing of it when that number reaches zero. This pattern is implemented throughout the Relay codebase. + +## Reference Marker + +TODO + +## @refetchable + +A directive that modifies a fragment, and causes Relay to generate a query for that fragment. + +This yields efficiency gains. The fragment can be loaded as part of a single, larger query initially (thus requiring only a single request to fetch all of the data), and yet refetched independently. + +## @relay + +A directive that allows you to turn off data masking and is used on plural types. + +See [the documentation](../api-reference/graphql-and-directives/#relaymask-boolean). + +## Relay Classic + +An even older version of Relay. + +## Relay Hooks + +The easiest-to-use, safest Relay API. It relies on suspense, and is safe to use in React concurrent mode. + +You should not write new code using Relay Classic or Relay Modern. + +## Relay Modern + +An older version of Relay. This version of Relay had an API that was heavily focused on Containers. + +## Relay Resolvers + +Relay Resolvers is an experimental Relay feature which enables modeling derived state as client-only fields in Relay’s GraphQL graph. + +See also [the Relay Resolvers guide](../guides/relay-resolvers). + +## Release Buffer + +As queries are released (no longer [retained](#retain)), their root nodes are stored in a release buffer of fixed size, and only evicted by newly released queries when there isn't enough space in the release buffer. When Relay runs garbage collection, queries that are present in the release buffer and not disposed. + +The size of the release buffer is configured with the `gcReleaseBufferSize` parameter. + +## `@required` + +A Relay directive that makes handling potentially `null` values more egonomic. + +See also [the `@required` guide](../guides/required-directive/). + +## Request + +A request refers to an API call made over the network to access or mutate some data, or both. + +A query, when initiated, may or may not involve making a request, depending on whether the query can be fulfilled from the store or not. + +## Request Descriptor + +An object associating a [concrete request](#concrete-request) and [variables](#variables), as well as a pre-computed request ID. The variables should be filtered to exclude unneeded variables and are populated to include default values for missing variables, thus ensuring that requests that differ in irrelevant ways are cached using the same request ID. + +## Resolver + +An overloaded term, mostly referring to virtual fields, but also occassionally referring to other things. + +### When describing a field + +A resolver field is a "virtual" field that is backed by a function from a fragment reference on the same type to some arbitrary value. + +A live resolver is a "virtual" field that is backed by an external data source. e.g. one might use an external resolver to expose some state that is stored in local storage, or in an external Flux store. + +### Other meanings + +It can also be a [fragment spec resolver](#fragment-spec-resolver) or a [operation mock resolver](#operation-mock-resolver). + +## Response + +TODO + +## Response Normalizer + +A class, exposing a single method `normalize`. This will traverse the denormalized response from the API request, normalize it and write the normalized results into a given `MutableRecordSource`. It is called from the query executor. + +## Restore + +TODO + +## Retain + +TODO + +## Render Policy + +TODO + +## Revert + +TODO + +## Root Field + +TODO + +## Root Type + +The [GraphQL spec](https://spec.graphql.org/June2018/#sec-Root-Operation-Types) defines three special root types: Query, Mutation and Subscription. Queries must select fields off of the Query root type, etc. + +## Root + +Outermost React Component for a given page or screen. Can be associated with an entrypoint. + +Roots for entrypoints are referred to by the [`JSResource`](#JSResource) to the root React component module. + +## Scalar + +TODO + +## Scheduler + +TODO + +## Schema + +A collection of all of the GraphQL types that are known to Relay, for a given [project](#project). + + +## Schema Sync + +The GraphQL [schema](#schema) is derived from annotations on Hack classes in the www repository. + +Periodically, those changes are synced to fbsource in a schema sync diff. If the updated schema would break relay on fbsource, these schema sync diffs will not land. + +If a field is removed from www, but is only used in fbsource, the application developer may not notice that the field cannot be removed. This is a common source of schema breakages. + +For more info, look [here](https://www.internalfb.com/intern/wiki/GraphQL/Build_Infra/Schema_Sync/) and [here](https://www.internalfb.com/intern/wiki/Relay-team/GraphQL_Schema_Sync/). + + +## Schema Extension + +TODO + +## Selection + +A "selection of fields" refers to the fields you are requesting on an object that you are accessing, as part of a query, mutation, subscription or fragment. + +## Selector + +See [normalization selector](#normalization-selector). + +## @skip + +A directive that is added to fields, inline fragments and fragment spreads, and allows for conditional inclusion. It is the opposite of the [`@include`](#include) directive. + +## Snapshot + +The results of running a reader selector against the data currently in the store. See [lookup](#lookup). + +## Stale + +TODO + +## Store + +TODO + +## @stream + +TODO + +## @stream_connection + +TODO + +## Subscribe + +A method exposed by the Relay store. Accepts a callback and a snapshot (see [lookup](#lookup)). The relay store will call this callback when [`notify`](#notify) is called, if the data referenced by that snapshot has been updated or invalidated. + +## Subscription + +[GraphQL Subscriptions](../guided-tour/updating-data/graphql-subscriptions) are a mechanism which allow clients to subscribe to changes in a piece of data from the server, and get notified whenever that data changes. + +A GraphQL Subscription looks very similar to a query, with the exception that it uses the subscription keyword: + +```graphql +subscription FeedbackLikeSubscription($input: FeedbackLikeSubscribeData!) { + feedback_like_subscribe(data: $input) { + feedback { + id + like_count + } + } +} +``` + + + +See also [the guide](../guides/writing-subscriptions). + + + +## Transaction ID + +A unique id for a given instance of a call to `network.execute`. This ID will be consistent for the entire duration of a network request. It can be consumed by custom log functions passed to `RelayModernEnvironment`. + +## Traversal + +There are four tree traversals that are core to understanding the internal behavior of Relay. + +* Using the normalization AST: + * When Relay normalizes the payload it receives from the GraphQL server in the Response Normalizer; + * When Relay reads determines whether there is enough data for to fulfill an operation, in the Data Checker; and + * When Relay determines what data is no longer accessible during garbage collection, in the Reference Marker. +* Using the reader AST: + * When Relay reads data for rendering, in the Reader. + +## Type + +The GraphQL type of a field is a description of a field on a schema, in terms of what subfields it has, or what it's representation is (String, number, etc.). + +See also [interface](#interface-graphql), [abstract type](#abstract-type) and [the GraphQL docs](https://graphql.org/learn/schema/#type-language) for more info. + +## Type Refinement + +The inclusion of a fragment of particular type in a location only known to potentially implement that type. This allows us to select fields if and only if they are defined on that particular object, and return null otherwise. + +For example, `node(id: 4) { ... on User { name } }`. In this case, we do now know ahead of time whether `node(id: 4)` is a User. If it is, this fragment will include the user name. + +See also [abstract type refinement](#abstract-type-refinement). + +## Updater + +A callback passed to `commitMutation`, which provides the application developer with imperative control over the data in the store. + + +See [the documentation](../guided-tour/updating-data/) and also optimistic updater. + +## Value + +A single value on a record, such as `has_viewer_liked`, or `name`. + +Compare with [linked record](#linked-record). + +## Variables + +GraphQL variables are a construct that allows referencing dynamic values inside a GraphQL query. They must be provided when the query is initiated, and can be used throughout nested fragments. + +See the [variables section of the guided tour](../guided-tour/rendering/variables) and compare with [@argumentDefinitions](#argumentdefinitions). + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/introduction.md b/website/versioned_docs/version-v15.0.0/guided-tour/introduction.md new file mode 100644 index 0000000000000..a99a99075d522 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/introduction.md @@ -0,0 +1,57 @@ +--- +id: introduction +title: Introduction +slug: /guided-tour/ +description: Relay guided tour +keywords: +- guided tour +- relay +- graphql +- documentation +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +import FbCrashCourse from './fb/FbCrashCourse.md'; + +In this guided tour, we're going to go over how to use Relay to build out some of the more common use cases in apps. If you're interested in a detailed reference of our APIs, check out our **[API Reference](../api-reference/relay-environment-provider/)**. + + +## Before you read + +Before getting started, bear in mind that we assume some level of familiarity with: + + + +* [Javascript](https://our.internmc.facebook.com/intern/wiki/JavaScript/) +* [React](https://our.internmc.facebook.com/intern/wiki/ReactGuide/) +* [GraphQL](https://our.internmc.facebook.com/intern/wiki/GraphQL/) and our internal [GraphQL Server](https://our.internmc.facebook.com/intern/wiki/Graphql-for-hack-developers/) + + + + + +* [Javascript](https://felix-kling.de/jsbasics/) +* [React](https://reactjs.org/docs/getting-started.html) +* [GraphQL](https://graphql.org/learn/) + + + +## On to the Tutorial + + + +* [Tutorial](https://www.internalfb.com/intern/staticdocs/relay/docs/tutorial/intro/) + + + + + +* [Tutorial](https://relay.dev/docs/tutorial/intro/) + + + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/list-data/advanced-pagination.md b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/advanced-pagination.md new file mode 100644 index 0000000000000..84e7594eb00cb --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/advanced-pagination.md @@ -0,0 +1,200 @@ +--- +id: advanced-pagination +title: Advanced Pagination +slug: /guided-tour/list-data/advanced-pagination/ +description: Relay guide for advanced pagination +keywords: +- pagination +- usePaginationFragment +- prefetching +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +In this section we're going to cover how to implement more advanced pagination use cases than the default cases covered by `usePaginationFragment`. + + +## Pagination Over Multiple Connections + +If you need to paginate over multiple connections within the same component, you can use `usePaginationFragment` multiple times: + +```js +import type {CombinedFriendsListComponent_user$key} from 'CombinedFriendsListComponent_user.graphql'; +import type {CombinedFriendsListComponent_viewer$key} from 'CombinedFriendsListComponent_viewer.graphql'; + +const React = require('React'); + +const {graphql, usePaginationFragment} = require('react-relay'); + +type Props = { + user: CombinedFriendsListComponent_user$key, + viewer: CombinedFriendsListComponent_viewer$key, +}; + +function CombinedFriendsListComponent(props: Props) { + + const {data: userData, ...userPagination} = usePaginationFragment( + graphql` + fragment CombinedFriendsListComponent_user on User { + name + friends + @connection( + key: "CombinedFriendsListComponent_user_friends_connection" + ) { + edges { + node { + name + age + } + } + } + } + `, + props.user, + ); + + const {data: viewerData, ...viewerPagination} = usePaginationFragment( + graphql` + fragment CombinedFriendsListComponent_user on Viewer { + actor { + ... on User { + name + friends + @connection( + key: "CombinedFriendsListComponent_viewer_friends_connection" + ) { + edges { + node { + name + age + } + } + } + } + } + } + `, + props.viewer, + ); + + return (...); +} +``` + +However, we recommend trying to keep a single connection per component, to keep the components easier to follow. + + + +## Bi-directional Pagination + +In the [Pagination](../pagination/) section we covered how to use `usePaginationFragment` to paginate in a single *"forward"* direction. However, connections also allow paginating in the opposite *"backward"* direction. The meaning of *"forward"* and *"backward"* directions will depend on how the items in the connection are sorted, for example *"forward"* could mean more recent*, and "backward"* could mean less recent. + +Regardless of the semantic meaning of the direction, Relay also provides the same APIs to paginate in the opposite direction, using `usePaginationFragment`, as long as the `before` and `last` connection arguments are also used along with `after` and `first`: + +```js +import type {FriendsListComponent_user$key} from 'FriendsListComponent_user.graphql'; + +const React = require('React'); +const {Suspense} = require('React'); + +const {graphql, usePaginationFragment} = require('react-relay'); + +type Props = { + userRef: FriendsListComponent_user$key, +}; + +function FriendsListComponent(props: Props) { + const { + data, + loadPrevious, + hasPrevious, + // ... forward pagination values + } = usePaginationFragment( + graphql` + fragment FriendsListComponent_user on User { + name + friends(after: $after, before: $before, first: $first, last: $last) + @connection(key: "FriendsListComponent_user_friends_connection") { + edges { + node { + name + age + } + } + } + } + `, + userRef, + ); + + return ( + <> +

Friends of {data.name}:

+ edge.node)}> + {node => { + return ( +
+ {node.name} - {node.age} +
+ ); + }} +
+ + {hasPrevious ? ( + + ) : null} + + {/* Forward pagination controls can go simultaneously here */} + + ); +} +``` + +* The APIs for both *"forward"* and *"backward"* are exactly the same, they're only named differently. When paginating forward, then the `after` and `first` connection arguments will be used, when paginating backward, the `before` and `last` connection arguments will be used. +* Note that the primitives for both *"forward"* and *"backward"* pagination are exposed from a single call to `usePaginationFragment`, so both *"forward"* and *"backward"* pagination can be performed simultaneously in the same component. + + + +## Custom Connection State + +By default, when using `usePaginationFragment` and `@connection`, Relay will *append* new pages of items to the connection when paginating *"forward",* and *prepend* new pages of items when paginating *"backward"*. This means that your component will always render the *full* connection, with *all* of the items that have been accumulated so far via pagination, and/or items that have been added or removed via mutations or subscriptions. + +However, it is possible that you'd need different behavior for how to merge and accumulate pagination results (or other updates to the connection), and/or derive local component state from changes to the connection. Some examples of this might be: + +* Keeping track of different *visible* slices or windows of the connection. +* Visually separating each *page* of items. This requires knowledge of the exact set of items inside each page that has been fetched. +* Displaying different ends of the same connection simultaneously, while keeping track of the "gaps" between them, and being able to merge results when preforming pagination between the gaps. For example, imagine rendering a list of comments where the oldest comments are displayed at the top, then a "gap" that can be interacted with to paginate, and then a section at the bottom which shows the most recent comments that have been added by the user or by real-time subscriptions. + + +To address these more complex use cases, Relay is still working on a solution: + + +> TBD + + + + +## Refreshing connections + +> TBD + + + + +## Prefetching Pages of a Connection + +> TBD + + + + +## Rendering One Page of Items at a Time + +> TBD + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/list-data/connections.md b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/connections.md new file mode 100644 index 0000000000000..de0a92fde3023 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/connections.md @@ -0,0 +1,23 @@ +--- +id: connections +title: Connections +slug: /guided-tour/list-data/connections/ +description: Relay guide for connections +keywords: +- pagination +- connections +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +There are several scenarios in which we'll want to query a list of data from the GraphQL server. Often times we don't want to query the *entire* set of data up front, but rather discrete sub-parts of the list, incrementally, usually in response to user input or other events. Querying a list of data in discrete parts is usually known as [Pagination](https://graphql.org/learn/pagination/). + + +Specifically in Relay, we do this via GraphQL fields known as [Connections](https://graphql.org/learn/pagination/#complete-connection-model). Connections are GraphQL fields that take a set of arguments to specify which "slice" of the list to query, and include in their response both the "slice" of the list that was requested, as well as information to indicate if there is more data available in the list and how to query it; this additional information can be used in order to perform pagination by querying for more "slices" or pages on the list. + +More specifically, we perform *cursor-based pagination,* in which the input used to query for "slices" of the list is a `cursor` and a `count`. Cursors are essentially opaque tokens that serve as markers or pointers to a position in the list. If you're curious to learn more about the details of cursor-based pagination and connections, check out the spec. + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/list-data/pagination.md b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/pagination.md new file mode 100644 index 0000000000000..ca2d662221436 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/pagination.md @@ -0,0 +1,141 @@ +--- +id: pagination +title: Pagination +slug: /guided-tour/list-data/pagination/ +description: Relay guide to pagination +keywords: +- pagination +- usePaginationFragment +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbPaginationUsingUseTransition from './fb/FbPaginationUsingUseTransition.md'; + +To actually perform pagination over the connection, we need use the `loadNext` function to fetch the next page of items, which is available from `usePaginationFragment`: + + + + + + + +```js +import type {FriendsListComponent_user$key} from 'FriendsList_user.graphql'; + +const React = require('React'); + +const {graphql, usePaginationFragment} = require('react-relay'); + +const {Suspense} = require('React'); + +type Props = { + user: FriendsListComponent_user$key, +}; + +function FriendsListComponent(props: Props) { + const {data, loadNext} = usePaginationFragment( + graphql` + fragment FriendsListComponent_user on User + @refetchable(queryName: "FriendsListPaginationQuery") { + name + friends(first: $count, after: $cursor) + @connection(key: "FriendsList_user_friends") { + edges { + node { + name + age + } + } + } + } + `, + props.user, + ); + + return ( + <> +

Friends of {data.name}:

+
+ {(data.friends?.edges ?? []).map(edge => { + const node = edge.node; + return ( + }> + + + ); + })} +
+ + + + ); +} + +module.exports = FriendsListComponent; +``` + +Let's distill what's happening here: + +* `loadNext` takes a count to specify how many more items in the connection to fetch from the server. In this case, when `loadNext` is called we'll fetch the next 10 friends in the friends list of our currently rendered `User`. +* When the request to fetch the next items completes, the connection will be automatically updated and the component will re-render with the latest items in the connection. In our case, this means that the `friends` field will always contain *all* of the friends that we've fetched so far. By default, *Relay will automatically append new items to the connection upon completing a pagination request,* and will make them available to your fragment component*.* If you need a different behavior, check out our [Advanced Pagination Use Cases](../advanced-pagination/) section. +* `loadNext` may cause the component or new children components to suspend (as explained in [Loading States with Suspense](../../rendering/loading-states/)). This means that you'll need to make sure that there's a `Suspense` boundary wrapping this component from above. + +
+ + +Often, you will also want to access information about whether there are more items available to load. To do this, you can use the `hasNext` value, also available from `usePaginationFragment`: + +```js +import type {FriendsListPaginationQuery} from 'FriendsListPaginationQuery.graphql'; +import type {FriendsListComponent_user$key} from 'FriendsList_user.graphql'; + +const React = require('React'); +const {Suspense} = require('React'); + +const {graphql, usePaginationFragment} = require('react-relay'); + +type Props = { + user: FriendsListComponent_user$key, +}; + +function FriendsListComponent(props: Props) { + // ... + const { + data, + loadNext, + hasNext, + } = usePaginationFragment( + graphql`...`, + props.user, + ); + + return ( + <> +

Friends of {data.name}:

+ {/* ... */} + + {/* Only render button if there are more friends to load in the list */} + {hasNext ? ( + + ) : null} + + ); +} + +module.exports = FriendsListComponent; +``` + +* `hasNext` is a boolean which indicates if the connection has more items available. This information can be useful for determining if different UI controls should be rendered. In our specific case, we only render the `Button` if there are more friends available in the connection. + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/list-data/refetching-connections.md b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/refetching-connections.md new file mode 100644 index 0000000000000..b30bbf7dec66f --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/refetching-connections.md @@ -0,0 +1,210 @@ +--- +id: refetching-connections +title: Refetching Connections (Using and Changing Filters) +slug: /guided-tour/list-data/refetching-connections/ +description: Relay guide to refetching connections +keywords: +- pagination +- refetching +- connection +- useRefetchableFragment +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbRefetchingConnectionsUsingUseTransition from './fb/FbRefetchingConnectionsUsingUseTransition.md'; + +Often times when querying for a list of data, you can provide different values in the query which serve as filters that change the result set, or sort it differently. + +Some examples of this are: + +* Building a search typeahead, where the list of results is a list filtered by the search term entered by the user. +* Changing the ordering mode of the list comments currently displayed for a post, which could produce a completely different set of comments from the server. +* Changing the way News Feed is ranked and sorted. + + +Specifically, in GraphQL, connection fields can accept arguments to sort or filter the set of queried results: + +```graphql +fragment UserFragment on User { + name + friends(order_by: DATE_ADDED, search_term: "Alice", first: 10) { + edges { + node { + name + age + } + } + } +} +``` + + +In Relay, we can pass those arguments as usual using GraphQL [variables](../../rendering/variables/) + +```js +type Props = { + userRef: FriendsListComponent_user$key, +}; + +function FriendsListComponent(props: Props) { + const userRef = props.userRef; + + const {data, ...} = usePaginationFragment( + graphql` + fragment FriendsListComponent_user on User { + name + friends( + order_by: $orderBy, + search_term: $searchTerm, + after: $cursor, + first: $count, + ) @connection(key: "FriendsListComponent_user_friends_connection") { + edges { + node { + name + age + } + } + } + } + `, + userRef, + ); + + return (...); +} +``` + + +When paginating, the original values for those filters will be preserved: + +```js +type Props = { + userRef: FriendsListComponent_user$key, +}; + +function FriendsListComponent(props: Props) { + const userRef = props.userRef; + + const {data, loadNext} = usePaginationFragment( + graphql` + fragment FriendsListComponent_user on User { + name + friends(order_by: $orderBy, search_term: $searchTerm) + @connection(key: "FriendsListComponent_user_friends_connection") { + edges { + node { + name + age + } + } + } + } + `, + userRef, + ); + + return ( + <> +

Friends of {data.name}:

+ {...} + + {/* + Loading the next items will use the original order_by and search_term + values used for the initial query + */ } + + + ); +} +``` +* Note that calling `loadNext` will use the original `order_by` and `search_term` values used for the initial query. During pagination, these value won't (*and shouldn't*) change. + +If we want to refetch the connection with *different* variables, we can use the `refetch` function provided by `usePaginationFragment`, similarly to how we do so when [Refetching Fragments with Different Data](../../refetching/refetching-fragments-with-different-data/): + + + + + + + +```js +/** + * FriendsListComponent.react.js + */ +import type {FriendsListComponent_user$key} from 'FriendsListComponent_user.graphql'; + +const React = require('React'); +const {useState, useEffect} = require('React'); + +const {graphql, usePaginationFragment} = require('react-relay'); + + +type Props = { + searchTerm?: string, + user: FriendsListComponent_user$key, +}; + +function FriendsListComponent(props: Props) { + const searchTerm = props.searchTerm; + const {data, loadNext, refetch} = usePaginationFragment( + graphql` + fragment FriendsListComponent_user on User { + name + friends( + order_by: $orderBy, + search_term: $searchTerm, + after: $cursor, + first: $count, + ) @connection(key: "FriendsListComponent_user_friends_connection") { + edges { + node { + name + age + } + } + } + } + `, + props.user, + ); + + useEffect(() => { + // When the searchTerm provided via props changes, refetch the connection + // with the new searchTerm + refetch({first: 10, search_term: searchTerm}, {fetchPolicy: 'store-or-network'}); + }, [searchTerm]) + + return ( + <> +

Friends of {data.name}:

+ + {/* When the button is clicked, refetch the connection but sorted differently */} + + + ... + + + ); +} +``` + +Let's distill what's going on here: + +* Calling `refetch` and passing a new set of variables will fetch the fragment again *with the newly provided variables*. The variables you need to provide are a subset of the variables that the generated query expects; the generated query will require an `id`, if the type of the fragment has an `id` field, and any other variables that are transitively referenced in your fragment. + * In our case, we need to pass the count we want to fetch as the `first` variable, and we can pass different values for our filters, like `orderBy` or `searchTerm`. +* This will re-render your component and may cause it to suspend (as explained in [Loading States with Suspense](../../rendering/loading-states/)) if it needs to send and wait for a network request. If `refetch` causes the component to suspend, you'll need to make sure that there's a `Suspense` boundary wrapping this component from above. +* Conceptually, when we call refetch, we're fetching the connection *from scratch*. It other words, we're fetching it again from the *beginning* and *"resetting"* our pagination state. For example, if we fetch the connection with a different `search_term`, our pagination information for the previous `search_term` no longer makes sense, since we're essentially paginating over a new list of items. + +
+ + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/list-data/rendering-connections.md b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/rendering-connections.md new file mode 100644 index 0000000000000..377838ea5d99e --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/rendering-connections.md @@ -0,0 +1,112 @@ +--- +id: rendering-connections +title: Rendering Connections +slug: /guided-tour/list-data/rendering-connections/ +description: Relay guide to rendering connections +keywords: +- pagination +- usePaginationFragment +- connection +--- + +import DocsRating from '@site/src/core/DocsRating'; +import FbSuspenseListAlternative from './fb/FbSuspenseListAlternative.md'; +import FbRenderingConnectionsUsingSuspenseList from './fb/FbRenderingConnectionsUsingSuspenseList.md'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +In Relay, in order to display a list of data that is backed by a GraphQL connection, first you need to declare a fragment that queries for a connection: + +```js +const {graphql} = require('RelayModern'); + +const userFragment = graphql` + fragment UserFragment on User { + name + friends(after: $cursor, first: $count) + @connection(key: "UserFragment_friends") { + edges { + node { + ...FriendComponent + } + } + } + } +`; +``` + +* In the example above, we're querying for the `friends` field, which is a connection; in other words, it adheres to the connection spec. Specifically, we can query the `edges` and `node`s in the connection; the `edges` usually contain information about the relationship between the entities, while the `node`s are the actual entities at the other end of the relationship; in this case, the `node`s are objects of type `User` representing the user's friends. +* In order to indicate to Relay that we want to perform pagination over this connection, we need to mark the field with the `@connection` directive. We must also provide a *static* unique identifier for this connection, known as the `key`. We recommend the following naming convention for the connection key: `_`. +* We will go into more detail later as to why it is necessary to mark the field as a `@connection` and give it a unique `key` in our [Updating Connections](../updating-connections/) section. + + +In order to render this fragment which queries for a connection, we can use the `usePaginationFragment` Hook: + + + + + + + +```js +import type {FriendsListComponent_user$key} from 'FriendsList_user.graphql'; + +const React = require('React'); +const {Suspense} = require('React'); + +const {graphql, usePaginationFragment} = require('react-relay'); + +type Props = { + user: FriendsListComponent_user$key, +}; + +function FriendsListComponent(props: Props) { + const {data} = usePaginationFragment( + graphql` + fragment FriendsListComponent_user on User + @refetchable(queryName: "FriendsListPaginationQuery") { + name + friends(first: $count, after: $cursor) + @connection(key: "FriendsList_user_friends") { + edges { + node { + ...FriendComponent + } + } + } + } + `, + props.user, + ); + + + return ( + <> + {data.name != null ?

Friends of {data.name}:

: null} + +
+ {/* Extract each friend from the resulting data */} + {(data.friends?.edges ?? []).map(edge => { + const node = edge.node; + return ( + }> + + + ); + })} +
+ + ); +} + +module.exports = FriendsListComponent; +``` + + +* `usePaginationFragment` behaves the same way as a `useFragment` (see the [Fragments](../../rendering/fragments/) section), so our list of friends is available under `data.friends.edges.node`, as declared by the fragment. However, it also has a few additions: + * It expects a fragment that is a connection field annotated with the `@connection` directive + * It expects a fragment that is annotated with the `@refetchable` directive. Note that `@refetchable` directive can only be added to fragments that are "refetchable", that is, on fragments that are on `Viewer`, on `Query`, on any type that implements `Node` (i.e. a type that has an `id` field), or on a `@fetchable` type. For more info on `@fetchable` types, see [this post](https://fb.workplace.com/groups/graphql.fyi/permalink/1539541276187011/). +* It takes two Flow type parameters: the type of the generated query (in our case `FriendsListPaginationQuery`), and a second type which can always be inferred, so you only need to pass underscore (`_`). + +
+ + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/list-data/streaming-pagination.md b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/streaming-pagination.md new file mode 100644 index 0000000000000..2cb8795be20b4 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/streaming-pagination.md @@ -0,0 +1,87 @@ +--- +id: streaming-pagination +title: Streaming Pagination +slug: /guided-tour/list-data/streaming-pagination/ +description: Relay guide to streaming pagination +keywords: +- pagination +- usePaginationFragment +- connection +- streaming +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + + + +Additionally, we can combine `usePaginationFragment` with Relay's [Incremental Data Delivery](../../../guides/incremental-data-delivery/) capabilities in order to fetch a connection and incrementally receive each item in the connection as it becomes ready, instead of waiting for the whole list of items to be returned in a single payload. This can be useful when for example computing each item in the connection is an expensive operation in the server, and we want to be able to show the first item(s) in the list as soon as possible without blocking on *all* the items that we need to become available; for example, on News Feed a user could ideally see and start interacting with the first story while additional stories loaded in below. + + + + + +Additionally, we can combine `usePaginationFragment` with Relay's Incremental Data Delivery capabilities in order to fetch a connection and incrementally receive each item in the connection as it becomes ready, instead of waiting for the whole list of items to be returned in a single payload. This can be useful when for example computing each item in the connection is an expensive operation in the server, and we want to be able to show the first item(s) in the list as soon as possible without blocking on *all* the items that we need to become available; for example, on News Feed a user could ideally see and start interacting with the first story while additional stories loaded in below. + + + +In order to do so, we can use the `@stream_connection` directive instead of the `@connection` directive: + +```js +import type {FriendsListComponent_user$key} from 'FriendsList_user.graphql'; + +const React = require('React'); + +const {graphql, usePaginationFragment} = require('react-relay'); + +type Props = { + user: FriendsListComponent_user$key, +}; + +function FriendsListComponent(props: Props) { + // ... + + const { + data, + loadNext, + hasNext, + } = usePaginationFragment( + graphql` + fragment FriendsListComponent_user on User + @refetchable(queryName: "FriendsListPaginationQuery") { + name + friends(first: $count, after: $cursor) + @stream_connection(key: "FriendsList_user_friends", initial_count: 2,) { + edges { + node { + name + age + } + } + } + } + `, + props.user, + ); + + return (...); +} + +module.exports = FriendsListComponent; +``` + +Let's distill what's happening here: + +* The `@stream_connection` directive can be used directly in place of the `@connection` directive; it accepts the same arguments as @connection plus additional, *optional* parameters to control streaming: + * `initial_count: Int`: A number (defaulting to zero) that controls how many items will be included in the initial payload. Any subsequent items are streamed, so when set to zero the list will initially be empty and all items will be streamed. Note that this number does not affect how many items are returned *total*, only how many items are included in the initial payload. For example, consider a product that today makes an initial fetch for 2 items and then *immediately* issues a pagination query to fetch 3 more. With streaming, this product could instead choose to fetch 5 items in the initial query with initial_count=2, in order to fetch the 2 items quickly while avoiding a round trip for the subsequent 3 items. +* As with regular usage of `usePaginationFragment`, the connection will be automatically updated as new items are streamed in from the server, and the component will re-render each time with the latest items in the connection. + + + + +For more information, see our docs on [Incremental Data Delivery](../../../guides/incremental-data-delivery/#stream_connection). + + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/list-data/updating-connections.md b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/updating-connections.md new file mode 100644 index 0000000000000..af82fb204c3f5 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/list-data/updating-connections.md @@ -0,0 +1,603 @@ +--- +id: updating-connections +title: Updating Connections +slug: /guided-tour/list-data/updating-connections/ +description: Relay guide to updating connections +keywords: +- pagination +- usePaginationFragment +- updating +- connection +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +Usually when you're rendering a connection, you'll also want to be able to add or remove items to/from the connection in response to user actions. + +As explained in our [Updating Data](../../updating-data/) section, Relay holds a local in-memory store of normalized GraphQL data, where records are stored by their IDs. When creating mutations, subscriptions, or local data updates with Relay, you must provide an [`updater`](../../updating-data/graphql-mutations/#updater-functions) function, inside which you can access and read records, as well as write and make updates to them. When records are updated, any components affected by the updated data will be notified and re-rendered. + + +## Connection Records + +In Relay, connection fields that are marked with the `@connection` directive are stored as special records in the store, and they hold and accumulate *all* of the items that have been fetched for the connection so far. In order to add or remove items from a connection, we need to access the connection record using the connection `key`, which was provided when declaring a `@connection`; specifically, this allows us to access a connection inside an [`updater`](../../updating-data/graphql-mutations/#updater-functions) function using the `ConnectionHandler` APIs. + +For example, given the following fragment that declares a `@connection`, we can access the connection record inside an `updater` function in a few different ways: + +```js +const {graphql} = require('react-relay'); + +const storyFragment = graphql` + fragment StoryComponent_story on Story { + comments @connection(key: "StoryComponent_story_comments_connection") { + nodes { + body { + text + } + } + } + } +`; +``` + +### Accessing connections using `__id` + +We can query for a connection's `__id` field, and then use that `__id` to access the record in the store: + +```js +const fragmentData = useFragment( + graphql` + fragment StoryComponent_story on Story { + comments @connection(key: "StoryComponent_story_comments_connection") { + # Query for the __id field + __id + + # ... + } + } + `, + props.story, +); + +// Get the connection record id +const connectionID = fragmentData?.comments?.__id; +``` + +Then use it to access the record in the store: + +```js +function updater(store: RecordSourceSelectorProxy) { + // connectionID is passed as input to the mutation/subscription + const connection = store.get(connectionID); + + // ... +} +``` + +:::note +The `__id` field is **NOT** something that your GraphQL API needs to expose. Instead, it's an identifier that Relay automatically adds to identify the connection record. +::: + +### Accessing connections using `ConnectionHandler.getConnectionID` + +If we have access to the ID of the parent record that holds the connection, we can access the connection record by using the `ConnectionHandler.getConnectionID` API: + +```js +const {ConnectionHandler} = require('relay-runtime'); + +function updater(store: RecordSourceSelectorProxy) { + // Get the connection ID + const connectionID = ConnectionHandler.getConnectionID( + storyID, // passed as input to the mutation/subscription + 'StoryComponent_story_comments_connection', + ); + + // Get the connection record + const connectionRecord = store.get(connectionID); + + // ... +} +``` + +### Accessing connections using `ConnectionHandler.getConnection` + +If we have access to the parent record that holds the connection, we can access the connection record via the parent, by using the `ConnectionHandler.getConnection` API: + +```js +const {ConnectionHandler} = require('relay-runtime'); + +function updater(store: RecordSourceSelectorProxy) { + // Get parent story record + // storyID is passed as input to the mutation/subscription + const storyRecord = store.get(storyID); + + // Get the connection record from the parent + const connectionRecord = ConnectionHandler.getConnection( + storyRecord, + 'StoryComponent_story_comments_connection', + ); + + // ... +} +``` + +## Adding edges + +There are a couple of alternatives for adding edges to a connection: + +### Using declarative directives + +Usually, mutation or subscription payloads will expose the new edges that were added on the server as a field with a single edge or list of edges. If your mutation or subscription exposes an edge or edges field that you can query for in the response, then you can use the `@appendEdge` and `@prependEdge` declarative mutation directives on that field in order to add the newly created edges to the specified connections (note that these directives also work on queries). + +Alternatively, mutation or subscription payloads might expose the new nodes that were added on the server as a field with a single node or list of nodes. If your mutation or subscription exposes a node or nodes field that you can query for in the response, then you can use the `@appendNode` and `@prependNode` declarative mutation directives on that field in order to add the newly created nodes, wrapped inside edges, to the specified connections (note that these directives also work on queries). + +These directives accept a `connections` parameter, which needs to be a GraphQL variable containing an array of connection IDs. Connection IDs can be obtained either by using the [`__id` field on connections](#accessing-connections-using-__id) or using the [`ConnectionHandler.getConnectionID`](#accessing-connections-using-connectionhandlergetconnectionid) API. + + +#### `@appendEdge` / `@prependEdge` + +These directives work on a field with a single edge or list of edges. `@prependEdge` will add the selected edges to the beginning of each connection defined in the `connections` array, whereas `@appendEdge` will add the selected edges to the end of each connection in the array. + +**Arguments:** +- `connections`: An array of connection IDs. Connection IDs can be obtained either by using the [`__id` field on connections](#accessing-connections-using-__id) or using the [`ConnectionHandler.getConnectionID`](#accessing-connections-using-connectionhandlergetconnectionid) API. + + +**Example:** + +```js +// Get the connection ID using the `__id` field +const connectionID = fragmentData?.comments?.__id; + +// Or get it using `ConnectionHandler.getConnectionID()` +const connectionID = ConnectionHandler.getConnectionID( + '', + 'StoryComponent_story_comments_connection', +); + +// ... + +// Mutation +commitMutation(environment, { + mutation: graphql` + mutation AppendCommentMutation( + # Define a GraphQL variable for the connections array + $connections: [ID!]! + $input: CommentCreateInput + ) { + commentCreate(input: $input) { + # Use @appendEdge or @prependEdge on the edge field + feedbackCommentEdge @appendEdge(connections: $connections) { + cursor + node { + id + } + } + } + } + `, + variables: { + input, + // Pass the `connections` array + connections: [connectionID], + }, +}); +``` + + +#### `@appendNode` / `@prependNode` + +These directives work on a field with a single node or list of nodes, and will create edges with the specified `edgeTypeName`. `@prependNode` will add edges containing the selected nodes to the beginning of each connection defined in the `connections` array, whereas `@appendNode` will add edges containing the selected nodes to the end of each connection in the array. + +**Arguments:** +- `connections`: An array of connection IDs. Connection IDs can be obtained either by using the [`__id` field on connections](#accessing-connections-using-__id) or using the [`ConnectionHandler.getConnectionID`](#accessing-connections-using-connectionhandlergetconnectionid) API. +- `edgeTypeName`: The type name of the edge that contains the node, corresponding to the edge type argument in `ConnectionHandler.createEdge`. + +**Example:** +```js +// Get the connection ID using the `__id` field +const connectionID = fragmentData?.comments?.__id; + +// Or get it using `ConnectionHandler.getConnectionID()` +const connectionID = ConnectionHandler.getConnectionID( + '', + 'StoryComponent_story_comments_connection', +); + +// ... + +// Mutation +commitMutation(environment, { + mutation: graphql` + mutation AppendCommentMutation( + # Define a GraphQL variable for the connections array + $connections: [ID!]! + $input: CommentCreateInput + ) { + commentCreate(input: $input) { + # Use @appendNode or @prependNode on the node field + feedbackCommentNode @appendNode(connections: $connections, edgeTypeName: "CommentsEdge") { + id + } + } + } + `, + variables: { + input, + // Pass the `connections` array + connections: [connectionID], + }, +}); +``` + + +#### Order of execution + +For all of these directives, they will be executed in the following order within the mutation or subscription, as per the [order of execution of updates](../../updating-data/graphql-mutations/#order-of-execution-of-updater-functions): + +* When the mutation is initiated, after the optimistic response is handled, and after the optimistic updater function is executed, the `@prependEdge`, `@appendEdge`, `@prependNode`, and `@appendNode` directives will be applied to the optimistic response. +* If the mutation succeeds, after the data from the network response is merged with the existing values in the store, and after the updater function is executed, the `@prependEdge`, `@appendEdge`, `@prependNode`, and `@appendNode` directives will be applied to the data in the network response. +* If the mutation failed, the updates from processing the `@prependEdge`, `@appendEdge`, `@prependNode`, and `@appendNode` directives will be rolled back. + + +### Manually adding edges + +The directives described [above](#using-declarative-directives) largely remove the need to manually add and remove items from a connection, however, they do not provide as much control as you can get with manually writing an updater, and may not fulfill every use case. + +In order to write an updater to modify the connection, we need to make sure we have access to the [connection record](#connection-record). Once we have the connection record, we also need a record for the new edge that we want to add to the connection. Usually, mutation or subscription payloads will contain the new edge that was added; if not, you can also construct a new edge from scratch. + +For example, in the following mutation we can query for the newly created edge in the mutation response: + +```js +const {graphql} = require('react-relay'); + +const createCommentMutation = graphql` + mutation CreateCommentMutation($input: CommentCreateData!) { + comment_create(input: $input) { + comment_edge { + cursor + node { + body { + text + } + } + } + } + } +`; +``` + +* Note that we also query for the `cursor` for the new edge; this isn't strictly necessary, but it is information that will be required if we need to perform pagination based on that `cursor`. + + +Inside an [`updater`](../../updating-data/graphql-mutations/#updater-functions), we can access the edge inside the mutation response using Relay store APIs: + +```js +const {ConnectionHandler} = require('relay-runtime'); + +function updater(store: RecordSourceSelectorProxy) { + const storyRecord = store.get(storyID); + const connectionRecord = ConnectionHandler.getConnection( + storyRecord, + 'StoryComponent_story_comments_connection', + ); + + // Get the payload returned from the server + const payload = store.getRootField('comment_create'); + + // Get the edge inside the payload + const serverEdge = payload.getLinkedRecord('comment_edge'); + + // Build edge for adding to the connection + const newEdge = ConnectionHandler.buildConnectionEdge( + store, + connectionRecord, + serverEdge, + ); + + // ... +} +``` + +* The mutation payload is available as a root field on that store, which can be read using the `store.getRootField` API. In our case, we're reading `comment_create`, which is the root field in the response. +* Note that we need to construct the new edge from the edge received from the server using `ConnectionHandler.buildConnectionEdge` before we can add it to the connection. + + +If you need to create a new edge from scratch, you can use `ConnectionHandler.createEdge`: + +```js +const {ConnectionHandler} = require('relay-runtime'); + +function updater(store: RecordSourceSelectorProxy) { + const storyRecord = store.get(storyID); + const connectionRecord = ConnectionHandler.getConnection( + storyRecord, + 'StoryComponent_story_comments_connection', + ); + + // Create a new local Comment record + const id = `client:new_comment:${randomID()}`; + const newCommentRecord = store.create(id, 'Comment'); + + // Create new edge + const newEdge = ConnectionHandler.createEdge( + store, + connectionRecord, + newCommentRecord, + 'CommentEdge', /* GraphQl Type for edge */ + ); + + // ... +} +``` + + +Once we have a new edge record, we can add it to the the connection using `ConnectionHandler.insertEdgeAfter` or `ConnectionHandler.insertEdgeBefore`: + +```js +const {ConnectionHandler} = require('relay-runtime'); + +function updater(store: RecordSourceSelectorProxy) { + const storyRecord = store.get(storyID); + const connectionRecord = ConnectionHandler.getConnection( + storyRecord, + 'StoryComponent_story_comments_connection', + ); + + const newEdge = (...); + + // Add edge to the end of the connection + ConnectionHandler.insertEdgeAfter( + connectionRecord, + newEdge, + ); + + // Add edge to the beginning of the connection + ConnectionHandler.insertEdgeBefore( + connectionRecord, + newEdge, + ); +} +``` + +* Note that these APIs will *mutate* the connection in place + +:::note +Check out our complete [Relay Store APIs](../../../api-reference/store/). +::: + +## Removing edges + +### Using the declarative deletion directive + +Similarly to the [directives to add edges](#using-declarative-directives), we can use the `@deleteEdge` directive to delete edges from connections. If your mutation or subscription exposes a field with the ID or IDs of the nodes that were deleted that you can query for in the response, then you can use the `@deleteEdge` directive on that field to delete the respective edges from the connection (note that this directive also works on queries). + +#### `@deleteEdge` + +Works on GraphQL fields that return an `ID` or `[ID]`. Will delete the edges with nodes that match the `id` from each connection defined in the `connections` array. + +**Arguments:** +- `connections`: An array of connection IDs. Connection IDs can be obtained either by using the [`__id` field on connections](#accessing-connections-using-__id) or using the [`ConnectionHandler.getConnectionID`](#accessing-connections-using-connectionhandlergetconnectionid) API. + + +**Example:** + +```js +// Get the connection ID using the `__id` field +const connectionID = fragmentData?.comments?.__id; + +// Or get it using `ConnectionHandler.getConnectionID()` +const connectionID = ConnectionHandler.getConnectionID( + '', + 'StoryComponent_story_comments_connection', +); + +// ... + +// Mutation +commitMutation(environment, { + mutation: graphql` + mutation DeleteCommentsMutation( + # Define a GraphQL variable for the connections array + $connections: [ID!]! + $input: CommentsDeleteInput + ) { + commentsDelete(input: $input) { + deletedCommentIds @deleteEdge(connections: $connections) + } + } + `, + variables: { + input, + // Pass the `connections` array + connections: [connectionID], + }, +}); +``` + +### Manually removing edges + +`ConnectionHandler` provides a similar API to remove an edge from a connection, via `ConnectionHandler.deleteNode`: + +```js +const {ConnectionHandler} = require('RelayModern'); + +function updater(store: RecordSourceSelectorProxy) { + const storyRecord = store.get(storyID); + const connectionRecord = ConnectionHandler.getConnection( + storyRecord, + 'StoryComponent_story_comments_connection', + ); + + // Remove edge from the connection, given the ID of the node + ConnectionHandler.deleteNode( + connectionRecord, + commentIDToDelete, + ); +} +``` + +* In this case `ConnectionHandler.deleteNode` will remove an edge given a *`node` ID*. This means it will look up which edge in the connection contains a node with the provided ID, and remove that edge. +* Note that this API will *mutate* the connection in place. + + +:::note +Remember: when performing any of the operations described here to mutate a connection, any fragment or query components that are rendering the affected connection will be notified and re-render with the latest version of the connection. +::: + + +## Connection identity with filters + +In our previous examples, our connections didn't take any arguments as filters. If you declared a connection that takes arguments as filters, the values used for the filters will be part of the connection identifier. In other words, *each of the values passed in as connection filters will be used to identify the connection in the Relay store.* + +:::note +Note that this excludes pagination arguments, i.e. it excludes `first`, `last`, `before`, and `after`. +::: + + +For example, let's say the `comments` field took the following arguments, which we pass in as GraphQL [variables](../../rendering/variables/): + +```js +const {graphql} = require('RelayModern'); + +const storyFragment = graphql` + fragment StoryComponent_story on Story { + comments( + order_by: $orderBy, + filter_mode: $filterMode, + language: $language, + ) @connection(key: "StoryComponent_story_comments_connection") { + edges { + nodes { + body { + text + } + } + } + } + } +`; +``` + +In the example above, this means that whatever values we used for `$orderBy`, `$filterMode` and `$language` when we queried for the `comments` field will be part of the connection identifier, and we'll need to use those values when accessing the connection record from the Relay store. + +In order to do so, we need to pass a third argument to `ConnectionHandler.getConnection`, with concrete filter values to identify the connection: + +```js +const {ConnectionHandler} = require('RelayModern'); + +function updater(store: RecordSourceSelectorProxy) { + const storyRecord = store.get(storyID); + + // Get the connection instance for the connection with comments sorted + // by the date they were added + const connectionRecordSortedByDate = ConnectionHandler.getConnection( + storyRecord, + 'StoryComponent_story_comments_connection', + {order_by: '*DATE_ADDED*', filter_mode: null, language: null} + ); + + // Get the connection instance for the connection that only contains + // comments made by friends + const connectionRecordFriendsOnly = ConnectionHandler.getConnection( + storyRecord, + 'StoryComponent_story_comments_connection', + {order_by: null, filter_mode: '*FRIENDS_ONLY*', langugage: null} + ); +} +``` + +This implies that by default, *each combination of values used for filters will produce a different record for the connection.* + +When making updates to a connection, you will need to make sure to update all of the relevant records affected by a change. For example, if we were to add a new comment to our example connection, we'd need to make sure *not* to add the comment to the `FRIENDS_ONLY` connection, if the new comment wasn't made by a friend of the user: + +```js +const {ConnectionHandler} = require('relay-runtime'); + +function updater(store: RecordSourceSelectorProxy) { + const storyRecord = store.get(storyID); + + // Get the connection instance for the connection with comments sorted + // by the date they were added + const connectionRecordSortedByDate = ConnectionHandler.getConnection( + storyRecord, + 'StoryComponent_story_comments_connection', + {order_by: '*DATE_ADDED*', filter_mode: null, language: null} + ); + + // Get the connection instance for the connection that only contains + // comments made by friends + const connectionRecordFriendsOnly = ConnectionHandler.getConnection( + storyRecord, + 'StoryComponent_story_comments_connection', + {order_by: null, filter_mode: '*FRIENDS_ONLY*', language: null} + ); + + const newComment = (...); + const newEdge = (...); + + ConnectionHandler.insertEdgeAfter( + connectionRecordSortedByDate, + newEdge, + ); + + if (isMadeByFriend(storyRecord, newComment) { + // Only add new comment to friends-only connection if the comment + // was made by a friend + ConnectionHandler.insertEdgeAfter( + connectionRecordFriendsOnly, + newEdge, + ); + } +} +``` + + + +_Managing connections with many filters:_ + +As you can see, just adding a few filters to a connection can make the complexity and number of connection records that need to be managed explode. In order to more easily manage this, Relay provides 2 strategies: + +1) Specify exactly *which* filters should be used as connection identifiers. + +By default, *all* non-pagination filters will be used as part of the connection identifier. However, when declaring a `@connection`, you can specify the exact set of filters to use for connection identity: + +```js +const {graphql} = require('relay-runtime'); + +const storyFragment = graphql` + fragment StoryComponent_story on Story { + comments( + order_by: $orderBy + filter_mode: $filterMode + language: $language + ) + @connection( + key: "StoryComponent_story_comments_connection" + filters: ["order_by", "filter_mode"] + ) { + edges { + nodes { + body { + text + } + } + } + } + } +`; +``` + +* By specifying `filters` when declaring the `@connection`, we're indicating to Relay the exact set of filter values that should be used as part of connection identity. In this case, we're excluding `language`, which means that only values for `order_by` and `filter_mode` will affect connection identity and thus produce new connection records. +* Conceptually, this means that we're specifying which arguments affect the output of the connection from the server, or in other words, which arguments are *actually* *filters*. If one of the connection arguments doesn't actually change the set of items that are returned from the server, or their ordering, then it isn't really a filter on the connection, and we don't need to identify the connection differently when that value changes. In our example, changing the `language` of the comments we request doesn't change the set of comments that are returned by the connection, so it is safe to exclude it from `filters`. +* This can also be useful if we know that any of the connection arguments will never change in our app, in which case it would also be safe to exclude from `filters`. + + + +2) An easier API alternative to manage multiple connections with multiple filter values is still pending + + +> TBD + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/prefetching-queries.md b/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/prefetching-queries.md new file mode 100644 index 0000000000000..46cb0f1a9a054 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/prefetching-queries.md @@ -0,0 +1,10 @@ +--- +id: prefetching-queries +title: Prefetching Queries +slug: /guided-tour/accessing-data-without-react/prefetching-queries/ +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/reading-fragments.md b/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/reading-fragments.md new file mode 100644 index 0000000000000..50932e7d0da96 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/reading-fragments.md @@ -0,0 +1,12 @@ +--- +id: reading-fragments +title: Reading Fragments +slug: /guided-tour/accessing-data-without-react/reading-fragments/ +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/reading-queries.md b/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/reading-queries.md new file mode 100644 index 0000000000000..ee479755318f3 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/reading-queries.md @@ -0,0 +1,12 @@ +--- +id: reading-queries +title: Reading Queries +slug: /guided-tour/accessing-data-without-react/reading-queries/ +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/retaining-queries.md b/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/retaining-queries.md new file mode 100644 index 0000000000000..e0e865a20020e --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/retaining-queries.md @@ -0,0 +1,51 @@ +--- +id: retaining-queries +title: Retaining Queries +slug: /guided-tour/accessing-data-without-react/retaining-queries/ +description: Relay guide to retaining queries +keywords: +- retaining +- query +- environment +- garbage collection +- gc +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +In order to manually retain a query so that the data it references isn’t garbage collected by Relay, we can use the `environment.retain` method: + +```js +const { + createOperationDescriptor, + getRequest, + graphql, +} = require('relay-runtime') + +// Query graphql object +const query = graphql`...`; + +// Construct Relay's internal representation of the query +const queryRequest = getRequest(query); +const queryDescriptor = createOperationDescriptor( + queryRequest, + variables +); + +// Retain query; this will prevent the data for this query and +// variables from being gabrage collected by Relay +const disposable = environment.retain(queryDescriptor); + +// Disposing of the disposable will release the data for this query +// and variables, meaning that it can be deleted at any moment +// by Relay's garbage collection if it hasn't been retained elsewhere +disposable.dispose(); +``` + +:::note +Relay automatically manages the query data retention based on any mounted query components that are rendering the data, so you usually should not need to call retain directly within product code. For any advanced or special use cases, query data retention should usually be handled within infra-level code, such as a Router. +::: + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/subscribing-to-queries.md b/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/subscribing-to-queries.md new file mode 100644 index 0000000000000..24321e5e076ba --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/managing-data-outside-react/subscribing-to-queries.md @@ -0,0 +1,12 @@ +--- +id: subscribing-to-queries +title: Subscribing to Queries +slug: /guided-tour/accessing-data-without-react/subscribing-to-queries/ +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/refetching/OssAvoidSuspenseNote.md b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/OssAvoidSuspenseNote.md new file mode 100644 index 0000000000000..27fd6885b1f5d --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/OssAvoidSuspenseNote.md @@ -0,0 +1,3 @@ +:::note +In future versions of React when concurrent rendering is supported, React will provide an option to support this case and avoid hiding already rendered content with a Suspense fallback when suspending. +::: diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/refetching/introduction.md b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/introduction.md new file mode 100644 index 0000000000000..08c2e1948b5ea --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/introduction.md @@ -0,0 +1,17 @@ +--- +id: introduction +title: Introduction +slug: /guided-tour/refetching/ +description: Relay guide to refetching +keywords: +- refetching +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +After an app has been initially rendered, there are various scenarios in which you might want to refetch and show *new* or *different* data (e.g. change the currently displayed item), or maybe refresh the currently rendered data with the latest version from the server (e.g. refreshing a count), usually as a result of an event or user interaction. + +In this section we'll cover some of the most common scenarios and how to build them with Relay. + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refetching-fragments-with-different-data.md b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refetching-fragments-with-different-data.md new file mode 100644 index 0000000000000..81e954561a270 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refetching-fragments-with-different-data.md @@ -0,0 +1,171 @@ +--- +id: refetching-fragments-with-different-data +title: Refetching Fragments with Different Data +slug: /guided-tour/refetching/refetching-fragments-with-different-data/ +description: Relay guide to refetching fragments with different data +keywords: +- refetching +- fragment +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbRefetchingFragments from './fb/FbRefetchingFragments.md'; +import FbAvoidSuspenseCaution from './fb/FbAvoidSuspenseCaution.md'; +import OssAvoidSuspenseNote from './OssAvoidSuspenseNote.md'; + +When referring to **"refetching a fragment"**, we mean fetching a *different* version of the data than the one was originally rendered by the fragment. For example, this might be to change a currently selected item, to render a different list of items than the one being shown, or more generally to transition the currently rendered content to show new or different content. + +Conceptually, this means fetching and rendering the currently rendered fragment again, but under a new query with *different variables*; or in other words, rendering the fragment under a new query root. Remember that *fragments can't be fetched by themselves: they need to be part of a query,* so we can't just "fetch" the fragment again by itself. + +## Using `useRefetchableFragment` + +To do so, we can also use the [`useRefetchableFragment`](../../../api-reference/use-refetchable-fragment/) Hook in combination with the `@refetchable` directive, which will automatically generate a query to refetch the fragment under, and which we can fetch using the `refetch` function: + + + + + + + +```js +import type {CommentBody_comment$key} from 'CommentBody_comment.graphql'; + +type Props = { + comment: CommentBody_comment$key, +}; + +function CommentBody(props: Props) { + const [data, refetch] = useRefetchableFragment( + graphql` + fragment CommentBody_comment on Comment + # @refetchable makes it so Relay autogenerates a query for + # fetching this fragment + @refetchable(queryName: "CommentBodyRefetchQuery") { + body(lang: $lang) { + text + } + } + `, + props.comment, + ); + + const refetchTranslation = () => { + // We call refetch with new variables, + // which will refetch the @refetchable query with the + // new variables and update this component with the + // latest fetched data. + refetch({lang: 'SPANISH'}); + }; + + return ( + <> +

{data.body?.text}

+ + + ); +} +``` + +Let's distill what's happening in this example: + +* `useRefetchableFragment` behaves similarly to [`useFragment`](../../../api-reference/use-fragment/) (see the [Fragments](../../rendering/fragments/) section), but with a few additions: + * It expects a fragment that is annotated with the `@refetchable` directive. Note that `@refetchable` directive can only be added to fragments that are "refetchable", that is, on fragments that are on `Viewer`, on `Query`, on any type that implements `Node` (i.e. a type that has an `id` field), or on a [`@fetchable`](https://fb.workplace.com/groups/graphql.fyi/permalink/1539541276187011/) type. +* It returns a `refetch` function, which is already Flow-typed to expect the query variables that the generated query expects. +* It takes two Flow type parameters: the type of the generated query (in our case `CommentBodyRefetchQuery`), and a second type which can always be inferred, so you only need to pass underscore (`_`). +* We're calling the `refetch` function with 2 main inputs: + * The first argument is the set of variables to fetch the fragment with. In this case, calling `refetch` and passing a new set of variables will fetch the fragment again *with the newly provided variables*. The variables you need to provide are a subset of the variables that the `@refetchable` query expects; the query will require an `id`, if the type of the fragment has an `id` field, and any other variables that are transitively referenced in your fragment. + * In this case we're passing the current comment `id` and a new value for the `translationType` variable to fetch the translated comment body. + * We are not passing a second options argument in this case, which means that we will use the default `fetchPolicy` of `'store-or-network'`, which will skip the network request if the new data for that fragment is already cached (as we covered in [Reusing Cached Data For Render](../../reusing-cached-data/)). +* Calling `refetch` will re-render the component and may cause `useRefetchableFragment` to suspend (as explained in [Loading States with Suspense](../../rendering/loading-states/)). This means that you'll need to make sure that there's a `Suspense` boundary wrapping this component from above in order to show a fallback loading state. + +
+ +:::info +Note that this same behavior also applies to using the `refetch` function from [`usePaginationFragment`](../../../api-reference/use-pagination-fragment). +::: + +### If you need to avoid Suspense + +In some cases, you might want to avoid showing a Suspense fallback, which would hide the already rendered content. For these cases, you can use [`fetchQuery`](../../../api-reference/fetch-query/) instead, and manually keep track of a loading state: + + + + + + + + + +```js +import type {CommentBody_comment$key} from 'CommentBody_comment.graphql'; + +type Props = { + comment: CommentBody_comment$key, +}; + +function CommentBody(props: Props) { + const [data, refetch] = useRefetchableFragment( + graphql` + fragment CommentBody_comment on Comment + # @refetchable makes it so Relay autogenerates a query for + # fetching this fragment + @refetchable(queryName: "CommentBodyRefetchQuery") { + body(lang: $lang) { + text + } + } + `, + props.comment, + ); + + const [isRefetching, setIsRefreshing] = useState(false) + const refetchTranslation = () => { + if (isRefetching) { return; } + setIsRefreshing(true); + + // fetchQuery will fetch the query and write + // the data to the Relay store. This will ensure + // that when we re-render, the data is already + // cached and we don't suspend + fetchQuery(environment, AppQuery, variables) + .subscribe({ + complete: () => { + setIsRefreshing(false); + + // *After* the query has been fetched, we call + // refetch again to re-render with the updated data. + // At this point the data for the query should + // be cached, so we use the 'store-only' + // fetchPolicy to avoid suspending. + refetch({lang: 'SPANISH'}, {fetchPolicy: 'store-only'}); + } + error: () => { + setIsRefreshing(false); + } + }); + }; + + return ( + <> +

{data.body?.text}

+ + + ); +} +``` + +Let's distill what's going on here: + +* When refetching, we now keep track of our own `isRefetching` loading state, since we are avoiding suspending. We can use this state to render a busy spinner or similar loading UI in our component, *without* hiding the content. +* In the event handler, we first call `fetchQuery`, which will fetch the query and write the data to the local Relay store. When the `fetchQuery` network request completes, we call `refetch` so that we render the updated data, similar to the previous example. +* At this point, when `refetch` is called, the data for the fragment should already be cached in the local Relay store, so we use `fetchPolicy` of `'store-only'` to avoid suspending and only read the already cached data. + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refetching-queries-with-different-data.md b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refetching-queries-with-different-data.md new file mode 100644 index 0000000000000..6640c5a9c101c --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refetching-queries-with-different-data.md @@ -0,0 +1,344 @@ +--- +id: refetching-queries-with-different-data +title: Refetching Queries with Different Data +slug: /guided-tour/refetching/refetching-queries-with-different-data/ +description: Relay guide to refetching queries with different data +keywords: +- refetching +- query +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbRefetchingQueriesUsingUseQueryLoader from './fb/FbRefetchingQueriesUsingUseQueryLoader.md'; +import FbRefetchingQueriesUsingUseLazyLoadQuery from './fb/FbRefetchingQueriesUsingUseLazyLoadQuery.md'; +import FbAvoidSuspenseCaution from './fb/FbAvoidSuspenseCaution.md'; +import OssAvoidSuspenseNote from './OssAvoidSuspenseNote.md'; + +When referring to **"refetching a query"**, we mean fetching the query again for *different* data than was originally rendered by the query. For example, this might be to change a currently selected item, to render a different list of items than the one being shown, or more generally to transition the currently rendered content to show new or different content. + +## When using `useQueryLoader` / `loadQuery` + +Similarly to [Refreshing Queries with `useQueryLoader`](../refreshing-queries/#when-using-usequeryloader--loadquery), we can also use the `useQueryLoader` Hook described in our [Fetching Queries for Render](../../rendering/queries/#fetching-queries-for-render) section, but this time passing *different query variables*: + + + + + + + +```js +/** + * App.react.js + */ +const AppQuery = require('__generated__/AppQuery.graphql'); + +function App(props: Props) { + const variables = {id: '4'}; + const [queryRef, loadQuery] = useQueryLoader( + AppQuery, + props.appQueryRef /* initial query ref */ + ); + + const refetch = useCallback(() => { + // Load the query again using the same original variables. + // Calling loadQuery will update the value of queryRef. + loadQuery({id: 'different-id'}); + }, [/* ... */]); + + return ( + + + + ); +} +``` + +```js +/** + * MainContent.react.js + */ + +// Renders the preloaded query, given the query reference +function MainContent(props) { + const {refetch, queryRef} = props; + const data = usePreloadedQuery( + graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + friends { + count + } + } + } + `, + queryRef, + ); + + return ( + <> +

{data.user?.name}

+
Friends count: {data.user?.friends?.count}
+ + + ); +} +``` + +Let's distill what's going on here: + +* We call `loadQuery` in the event handler for refetching, so the network request starts immediately, and then pass the `queryRef` to `usePreloadedQuery`, so it renders the updated data. +* We are not passing a `fetchPolicy` to `loadQuery`, meaning that it will use the default value of `'store-or-network'`. We could provide a different policy in order to specify whether to use locally cached data (as we covered in [Reusing Cached Data For Render](../../reusing-cached-data/)). +* Calling `loadQuery` will re-render the component and may cause `usePreloadedQuery` to suspend (as explained in [Loading States with Suspense](../../rendering/loading-states/)). This means that we'll need to make sure that there's a `Suspense` boundary wrapping the `MainContent` component, in order to show a fallback loading state. + +
+ + +### If you need to avoid Suspense + +In some cases, you might want to avoid showing a Suspense fallback, which would hide the already rendered content. For these cases, you can use [`fetchQuery`](../../../api-reference/fetch-query/) instead, and manually keep track of a loading state: + + + + + + + + + +```js +/** + * App.react.js + */ +const AppQuery = require('__generated__/AppQuery.graphql'); + +function App(props: Props) { + const environment = useRelayEnvironment(); + const [queryRef, loadQuery] = useQueryLoader( + AppQuery, + props.appQueryRef /* initial query ref */ + ); + const [isRefetching, setIsRefetching] = useState(false) + + const refetch = useCallback(() => { + if (isRefetching) { return; } + setIsRefetching(true); + + // fetchQuery will fetch the query and write + // the data to the Relay store. This will ensure + // that when we re-render, the data is already + // cached and we don't suspend + fetchQuery(environment, AppQuery, variables) + .subscribe({ + complete: () => { + setIsRefetching(false); + + // *After* the query has been fetched, we call + // loadQuery again to re-render with a new + // queryRef. + // At this point the data for the query should + // be cached, so we use the 'store-only' + // fetchPolicy to avoid suspending. + loadQuery({id: 'different-id'}, {fetchPolicy: 'store-only'}); + }, + error: () => { + setIsRefetching(false); + } + }); + }, [/* ... */]); + + return ( + + + + ); +} +``` + +Let's distill what's going on here: + +* When refetching, we now keep track of our own `isRefetching` loading state, since we are avoiding suspending. We can use this state to render a busy spinner or similar loading UI inside the `MainContent` component, *without* hiding the `MainContent`. +* In the event handler, we first call `fetchQuery`, which will fetch the query and write the data to the local Relay store. When the `fetchQuery` network request completes, we call `loadQuery` so that we obtain an updated `queryRef` that we then pass to `usePreloadedQuery` in order render the updated data, similar to the previous example. +* At this point, when `loadQuery` is called, the data for the query should already be cached in the local Relay store, so we use `fetchPolicy` of `'store-only'` to avoid suspending and only read the already cached data. + +## When using `useLazyLoadQuery` + +Similarly to [Refreshing Queries with `useLazyLoadQuery`](../refreshing-queries/#when-using-uselazyloadquery), we can also use the [`useLazyLoadQuery`](../../../api-reference/use-lazy-load-query/) Hook described in our [Lazily Fetching Queries during Render](../../rendering/queries/#lazily-fetching-queries-during-render) section, but this time passing *different query variables*: + + + + + + + +```js +/** + * App.react.js + */ +const AppQuery = require('__generated__/AppQuery.graphql'); + +function App(props: Props) { + const [queryArgs, setQueryArgs] = useState({ + options: {fetchKey: 0}, + variables: {id: '4'}, + }); + + const refetch = useCallback(() => { + // Trigger a re-render of useLazyLoadQuery with new variables, + // *and* an updated fetchKey. + // The new fetchKey will ensure that the query is fully + // re-evaluated and refetched. + setQueryArgs(prev => ({ + options: { + fetchKey: (prev?.options.fetchKey ?? 0) + 1, + }, + variables: {id: 'different-id'} + })); + }, [/* ... */]); + + return ( + + + + ); +} +``` + +```js +/** + * MainContent.react.js + */ +// Fetches and renders the query, given the fetch options +function MainContent(props) { + const {refetch, queryArgs} = props; + const data = useLazyLoadQuery( + graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + friends { + count + } + } + } + `, + queryArgs.variables, + queryArgs.options, + ); + + return ( + <> +

{data.user?.name}

+
Friends count: {data.user.friends?.count}
+ + + ); +} +``` + +Let's distill what's going on here: + +* We update the component in the event handler for refreshing by setting new query args in state. This will cause the `MainContent` component that uses `useLazyLoadQuery` to re-render with the new `variables` and `fetchKey`, and refetch the query upon rendering. +* We are passing a new value of `fetchKey` which we increment on every update. Passing a new `fetchKey` to `useLazyLoadQuery` on every update will ensure that the query is fully re-evaluated and refetched. +* We are not passing a new `fetchPolicy` to `useLazyLoadQuery`, meaning that it will use the default value of `'store-or-network'`. We could provide a different policy in order to specify whether to use locally cached data (as we covered in [Reusing Cached Data For Render](../../reusing-cached-data/)). +* The state update in `refetch` will re-render the component and may cause the component to suspend (as explained in [Loading States with Suspense](../../rendering/loading-states/)). This means that we'll need to make sure that there's a `Suspense` boundary wrapping the `MainContent` component, in order to show a fallback loading state. + + +
+ +### If you need to avoid Suspense + +In some cases, you might want to avoid showing a Suspense fallback, which would hide the already rendered content. For these cases, you can use [`fetchQuery`](../../../api-reference/fetch-query/) instead, and manually keep track of a loading state: + + + + + + + + + +```js +/** + * App.react.js + */ +const AppQuery = require('__generated__/AppQuery.graphql'); + +function App(props: Props) { + const environment = useRelayEnvironment(); + const [isRefreshing, setIsRefreshing] = useState(false) + const [queryArgs, setQueryArgs] = useState({ + options: {fetchKey: 0, fetchPolicy: 'store-or-network'}, + variables: {id: '4'}, + }); + + const refetch = useCallback(() => { + if (isRefreshing) { return; } + setIsRefreshing(true); + + // fetchQuery will fetch the query and write + // the data to the Relay store. This will ensure + // that when we re-render, the data is already + // cached and we don't suspend + fetchQuery(environment, AppQuery, variables) + .subscribe({ + complete: () => { + setIsRefreshing(false); + + // *After* the query has been fetched, we update + // our state to re-render with the new fetchKey + // and fetchPolicy. + // At this point the data for the query should + // be cached, so we use the 'store-only' + // fetchPolicy to avoid suspending. + setQueryArgs(prev => ({ + options: { + fetchKey: (prev?.options.fetchKey ?? 0) + 1, + fetchPolicy: 'store-only', + }, + variables: {id: 'different-id'} + })); + }, + error: () => { + setIsRefreshing(false); + } + }); + }, [/* ... */]); + + return ( + + + + ); +} +``` + +Let's distill what's going on here: + +* When refetching, we now keep track of our own `isRefetching` loading state, since we are avoiding suspending. We can use this state to render a busy spinner or similar loading UI inside the `MainContent` component, *without* hiding the `MainContent`. +* In the event handler, we first call `fetchQuery`, which will fetch the query and write the data to the local Relay store. When the `fetchQuery` network request completes, we update our state so that we re-render an updated `fetchKey` and `fetchPolicy` that we then pass to `useLazyLoadQuery` in order render the updated data, similar to the previous example. +* At this point, when we update the state, the data for the query should already be cached in the local Relay store, so we use `fetchPolicy` of `'store-only'` to avoid suspending and only read the already cached data. + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refreshing-fragments.md b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refreshing-fragments.md new file mode 100644 index 0000000000000..937af7feb3495 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refreshing-fragments.md @@ -0,0 +1,191 @@ +--- +id: refreshing-fragments +title: Refreshing Fragments +slug: /guided-tour/refetching/refreshing-fragments/ +description: Relay guide to refreshing fragments +keywords: +- refreshing +- fragment +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbRefreshingUsingRealTimeFeatures from './fb/FbRefreshingUsingRealTimeFeatures.md'; +import FbRefreshingFragments from './fb/FbRefreshingFragments.md'; +import FbAvoidSuspenseCaution from './fb/FbAvoidSuspenseCaution.md'; +import OssAvoidSuspenseNote from './OssAvoidSuspenseNote.md'; + +When referring to **"refreshing a fragment"**, we mean fetching the *exact* same data that was originally rendered by the fragment, in order to get the most up-to-date version of that data from the server. + +## Using real-time features + + + + + + +If we want to keep our data up to date with the latest version from the server, the first thing to consider is if it appropriate to use any real-time features, which can make it easier to automatically keep the data up to date without manually refreshing the data periodically. + +One example of this is using [GraphQL Subscriptions](https://relay.dev/docs/guided-tour/updating-data/graphql-subscriptions/), which will require additional configuration on your server and [network layer](https://relay.dev/docs/guided-tour/updating-data/graphql-subscriptions/#configuring-the-network-layer). + + +## Using `useRefetchableFragment` + +In order to manually refresh the data for a fragment, we need a query to refetch the fragment under; remember, *fragments can't be fetched by themselves: they need to be part of a query,* so we can't just "fetch" the fragment again by itself. + +To do so, we can also use the [`useRefetchableFragment`](../../../api-reference/use-refetchable-fragment/) Hook in combination with the `@refetchable` directive, which will automatically generate a query to refetch the fragment under, and which we can fetch using the `refetch` function: + + + + + + + +```js +import type {UserComponent_user$key} from 'UserComponent_user.graphql'; + +type Props = { + user: UserComponent_user$key, +}; + +function UserComponent(props: Props) { + const [data, refetch] = useRefetchableFragment( + graphql` + fragment UserComponent_user on User + # @refetchable makes it so Relay autogenerates a query for + # fetching this fragment + @refetchable(queryName: "UserComponentRefreshQuery") { + id + name + friends { + count + } + } + `, + props.user, + ); + + const refresh = useCallback(() => { + // We call refetch with empty variables: `{}`, + // which will refetch the @refetchable query with the same + // original variables the fragment was fetched with, and update + // this component with the latest fetched data. + // The fetchPolicy ensures we always fetch from the server and skip + // the local data cache. + refetch({}, {fetchPolicy: 'network-only'}) + }), [/* ... */]; + + return ( + <> +

{data.name}

+
Friends count: {data.friends?.count}
+ + + ); +} +``` + +Let's distill what's happening in this example: + +* `useRefetchableFragment` behaves similarly to [`useFragment`](../../../api-reference/use-fragment/) (see the [Fragments](../../rendering/fragments/) section), but with a few additions: + * It expects a fragment that is annotated with the `@refetchable` directive. Note that `@refetchable` directive can only be added to fragments that are "refetchable", that is, on fragments that are on `Viewer`, on `Query`, on any type that implements `Node` (i.e. a type that has an `id` field). +* It returns a `refetch` function, which is already Flow-typed to expect the query variables that the generated query expects +* It takes two Flow type parameters: the type of the generated query (in our case `UserComponentRefreshQuery`), and a second type which can always be inferred, so you only need to pass underscore (`_`). +* We're calling the `refetch` function with 2 main inputs: + * The first argument is the set of variables to fetch the fragment with. In this case, calling `refetch` and passing an empty set of variables will fetch the fragment again *with the exact same variables the fragment was originally fetched with,* which is what we want for a refresh. + * In the second argument we are passing a `fetchPolicy` of `'network-only'` to ensure that we always fetch from the network and skip the local data cache. +* Calling `refetch` will re-render the component and cause `useRefetchableFragment` to suspend (as explained in [Loading States with Suspense](../../rendering/loading-states/)), since a network request will be required due to the `fetchPolicy` we are using. This means that you'll need to make sure that there's a `Suspense` boundary wrapping this component from above in order to show a fallback loading state. + +
+ +:::info +Note that this same behavior also applies to using the `refetch` function from [`usePaginationFragment`](../../../api-reference/use-pagination-fragment). +::: + +### If you need to avoid Suspense + +In some cases, you might want to avoid showing a Suspense fallback, which would hide the already rendered content. For these cases, you can use [`fetchQuery`](../../../api-reference/fetch-query/) instead, and manually keep track of a loading state: + + + + + + + + + +```js +import type {UserComponent_user$key} from 'UserComponent_user.graphql'; + +type Props = { + user: UserComponent_user$key, +}; + +function UserComponent(props: Props) { + const [data, refetch] = useRefetchableFragment( + graphql` + fragment UserComponent_user on User + # @refetchable makes it so Relay autogenerates a query for + # fetching this fragment + @refetchable(queryName: "UserComponentRefreshQuery") { + id + name + friends { + count + } + } + `, + props.user, + ); + + const [isRefreshing, setIsRefreshing] = useState(false); + const refresh = useCallback(() => { + if (isRefreshing) { return; } + setIsRefreshing(true); + + // fetchQuery will fetch the query and write + // the data to the Relay store. This will ensure + // that when we re-render, the data is already + // cached and we don't suspend + fetchQuery(environment, AppQuery, variables) + .subscribe({ + complete: () => { + setIsRefreshing(false); + + // *After* the query has been fetched, we call + // refetch again to re-render with the updated data. + // At this point the data for the query should + // be cached, so we use the 'store-only' + // fetchPolicy to avoid suspending. + refetch({}, {fetchPolicy: 'store-only'}); + }, + error: () => { + setIsRefreshing(false); + } + }); + }, [/* ... */]); + + return ( + <> +

{data.name}

+
Friends count: {data.friends?.count}
+ + + ); +} +``` + +Let's distill what's going on here: + +* When refreshing, we now keep track of our own `isRefreshing` loading state, since we are avoiding suspending. We can use this state to render a busy spinner or similar loading UI in our component, *without* hiding the content. +* In the event handler, we first call `fetchQuery`, which will fetch the query and write the data to the local Relay store. When the `fetchQuery` network request completes, we call `refetch` so that we render the updated data, similar to the previous example. +* At this point, when `refetch` is called, the data for the fragment should already be cached in the local Relay store, so we use `fetchPolicy` of `'store-only'` to avoid suspending and only read the already cached data. + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refreshing-queries.md b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refreshing-queries.md new file mode 100644 index 0000000000000..41af90583486d --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/refetching/refreshing-queries.md @@ -0,0 +1,357 @@ +--- +id: refreshing-queries +title: Refreshing Queries +slug: /guided-tour/refetching/refreshing-queries/ +description: Relay guide to refreshing queries +keywords: +- refreshing +- queries +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbRefreshingUsingRealTimeFeatures from './fb/FbRefreshingUsingRealTimeFeatures.md'; +import FbRefreshingQueriesUsingUseQueryLoader from './fb/FbRefreshingQueriesUsingUseQueryLoader.md'; +import FbAvoidSuspenseCaution from './fb/FbAvoidSuspenseCaution.md'; +import FbRefreshingQueriesUsingUseLazyLoadQuery from './fb/FbRefreshingQueriesUsingUseLazyLoadQuery.md'; +import OssAvoidSuspenseNote from './OssAvoidSuspenseNote.md'; + +When referring to **"refreshing a query"**, we mean fetching the *exact* same data that was originally rendered by the query, in order to get the most up-to-date version of that data from the server. + +## Using real-time features + + + + + + +If we want to keep our data up to date with the latest version from the server, the first thing to consider is if it appropriate to use any real-time features, which can make it easier to automatically keep the data up to date without manually refreshing the data periodically. + +One example of this is using [GraphQL Subscriptions](https://relay.dev/docs/guided-tour/updating-data/graphql-subscriptions), which will require additional configuration on your server and [network layer](https://relay.dev/docs/guided-tour/updating-data/graphql-subscriptions/#configuring-the-network-layer). + + +## When using `useQueryLoader` / `loadQuery` + +To refresh a query using the [`useQueryLoader`](../../../api-reference/use-query-loader/) Hook described in our [Fetching Queries for Render](../../rendering/queries/#fetching-queries-for-render) section, we only need to call `loadQuery` again: + + + + + + + +```js +/** + * App.react.js + */ + +const AppQuery = require('__generated__/AppQuery.graphql'); + +function App(props: Props) { + const [queryRef, loadQuery] = useQueryLoader( + AppQuery, + props.appQueryRef /* initial query ref */ + ); + + const refresh = useCallback(() => { + // Load the query again using the same original variables. + // Calling loadQuery will update the value of queryRef. + // The fetchPolicy ensures we always fetch from the server and skip + // the local data cache. + const {variables} = props.appQueryRef; + loadQuery(variables, {fetchPolicy: 'network-only'}); + }, [/* ... */]); + + return ( + + + + ); +} +``` + +```js +/** + * MainContent.react.js + */ + +// Renders the preloaded query, given the query reference +function MainContent(props) { + const {refresh, queryRef} = props; + const data = usePreloadedQuery( + graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + friends { + count + } + } + } + `, + queryRef, + ); + + return ( + <> +

{data.user?.name}

+
Friends count: {data.user.friends?.count}
+ + + ); +} +``` + +Let's distill what's going on here: + +* We call `loadQuery` in the event handler for refreshing, so the network request starts immediately, and then pass the updated `queryRef` to the `MainContent` component that uses `usePreloadedQuery`, so it renders the updated data. +* We are passing a `fetchPolicy` of `'network-only'` to ensure that we always fetch from the network and skip the local data cache. +* Calling `loadQuery` will re-render the component and cause `usePreloadedQuery` to suspend (as explained in [Loading States with Suspense](../../rendering/loading-states/)), since a network request will always be made due to the `fetchPolicy` we are using. This means that we'll need to make sure that there's a `Suspense` boundary wrapping the `MainContent` component in order to show a fallback loading state. + +
+ +### If you need to avoid Suspense + +In some cases, you might want to avoid showing a Suspense fallback, which would hide the already rendered content. For these cases, you can use [`fetchQuery`](../../../api-reference/fetch-query/) instead, and manually keep track of a loading state: + + + + + + + + + +```js +/** + * App.react.js + */ + +const AppQuery = require('__generated__/AppQuery.graphql'); + +function App(props: Props) { + const environment = useRelayEnvironment(); + const [queryRef, loadQuery] = useQueryLoader( + AppQuery, + props.appQueryRef /* initial query ref */ + ); + const [isRefreshing, setIsRefreshing] = useState(false) + + const refresh = useCallback(() => { + if (isRefreshing) { return; } + const {variables} = props.appQueryRef; + setIsRefreshing(true); + + // fetchQuery will fetch the query and write + // the data to the Relay store. This will ensure + // that when we re-render, the data is already + // cached and we don't suspend + fetchQuery(environment, AppQuery, variables) + .subscribe({ + complete: () => { + setIsRefreshing(false); + + // *After* the query has been fetched, we call + // loadQuery again to re-render with a new + // queryRef. + // At this point the data for the query should + // be cached, so we use the 'store-only' + // fetchPolicy to avoid suspending. + loadQuery(variables, {fetchPolicy: 'store-only'}); + } + error: () => { + setIsRefreshing(false); + } + }); + }, [/* ... */]); + + return ( + + + + ); +} +``` + +Let's distill what's going on here: + +* When refreshing, we now keep track of our own `isRefreshing` loading state, since we are avoiding suspending. We can use this state to render a busy spinner or similar loading UI inside the `MainContent` component, *without* hiding the `MainContent`. +* In the event handler, we first call `fetchQuery`, which will fetch the query and write the data to the local Relay store. When the `fetchQuery` network request completes, we call `loadQuery` so that we obtain an updated `queryRef` that we then pass to `usePreloadedQuery` in order render the updated data, similar to the previous example. +* At this point, when `loadQuery` is called, the data for the query should already be cached in the local Relay store, so we use `fetchPolicy` of `'store-only'` to avoid suspending and only read the already cached data. + + +## When using `useLazyLoadQuery` + +To refresh a query using the [`useLazyLoadQuery`](../../../api-reference/use-lazy-load-query/) Hook described in our [Lazily Fetching Queries during Render](../../rendering/queries/#lazily-fetching-queries-during-render) section, we can do the following: + + + + + + + +```js +/** + * App.react.js + */ +const AppQuery = require('__generated__/AppQuery.graphql'); + +function App(props: Props) { + const variables = {id: '4'}; + const [refreshedQueryOptions, setRefreshedQueryOptions] = useState(null); + + const refresh = useCallback(() => { + // Trigger a re-render of useLazyLoadQuery with the same variables, + // but an updated fetchKey and fetchPolicy. + // The new fetchKey will ensure that the query is fully + // re-evaluated and refetched. + // The fetchPolicy ensures that we always fetch from the network + // and skip the local data cache. + setRefreshedQueryOptions(prev => ({ + fetchKey: (prev?.fetchKey ?? 0) + 1, + fetchPolicy: 'network-only', + })); + }, [/* ... */]); + + return ( + + + + ); +``` + +```js +/** + * MainContent.react.js + */ + +// Fetches and renders the query, given the fetch options +function MainContent(props) { + const {refresh, queryOptions, variables} = props; + const data = useLazyLoadQuery( + graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + friends { + count + } + } + } + `, + variables, + queryOptions, + ); + + return ( + <> +

{data.user?.name}

+
Friends count: {data.user.friends?.count}
+ + + ); +} +``` + +Let's distill what's going on here: + +* We update the component in the event handler for refreshing by setting new options in state. This will cause the `MainContent` component that uses `useLazyLoadQuery` to re-render with the new `fetchKey` and `fetchPolicy`, and refetch the query upon rendering. +* We are passing a new value of `fetchKey` which we increment on every update. Passing a new `fetchKey` to `useLazyLoadQuery` on every update will ensure that the query is fully re-evaluated and refetched. +* We are passing a `fetchPolicy` of `'network-only'` to ensure that we always fetch from the network and skip the local data cache. +* The state update in `refresh` will cause the component to suspend (as explained in [Loading States with Suspense](../../rendering/loading-states/)), since a network request will always be made due to the `fetchPolicy` we are using. This means that we'll need to make sure that there's a `Suspense` boundary wrapping the `MainContent` component in order to show a fallback loading state. + +
+ +### If you need to avoid Suspense + +In some cases, you might want to avoid showing a Suspense fallback, which would hide the already rendered content. For these cases, you can use [`fetchQuery`](../../../api-reference/fetch-query/) instead, and manually keep track of a loading state: + + + + + + + + + +```js +/** + * App.react.js + */ +import type {AppQuery as AppQueryType} from 'AppQuery.graphql'; + +const AppQuery = require('__generated__/AppQuery.graphql'); + +function App(props: Props) { + const variables = {id: '4'} + const environment = useRelayEnvironment(); + const [refreshedQueryOptions, setRefreshedQueryOptions] = useState(null); + const [isRefreshing, setIsRefreshing] = useState(false) + + const refresh = useCallback(() => { + if (isRefreshing) { return; } + setIsRefreshing(true); + + // fetchQuery will fetch the query and write + // the data to the Relay store. This will ensure + // that when we re-render, the data is already + // cached and we don't suspend + fetchQuery(environment, AppQuery, variables) + .subscribe({ + complete: () => { + setIsRefreshing(false); + + // *After* the query has been fetched, we update + // our state to re-render with the new fetchKey + // and fetchPolicy. + // At this point the data for the query should + // be cached, so we use the 'store-only' + // fetchPolicy to avoid suspending. + setRefreshedQueryOptions(prev => ({ + fetchKey: (prev?.fetchKey ?? 0) + 1, + fetchPolicy: 'store-only', + })); + } + error: () => { + setIsRefreshing(false); + } + }); + }, [/* ... */]); + + return ( + + + + ); +} +``` + +Let's distill what's going on here: + +* When refreshing, we now keep track of our own `isRefreshing` loading state, since we are avoiding suspending. We can use this state to render a busy spinner or similar loading UI inside the `MainContent` component, *without* hiding the `MainContent`. +* In the event handler, we first call `fetchQuery`, which will fetch the query and write the data to the local Relay store. When the `fetchQuery` network request completes, we update our state so that we re-render an updated `fetchKey` and `fetchPolicy` that we then pass to `useLazyLoadQuery` in order render the updated data, similar to the previous example. +* At this point, when we update the state, the data for the query should already be cached in the local Relay store, so we use `fetchPolicy` of `'store-only'` to avoid suspending and only read the already cached data. + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/rendering/environment.md b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/environment.md new file mode 100644 index 0000000000000..e4fd6516fc560 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/environment.md @@ -0,0 +1,59 @@ +--- +id: environment +title: Environment +slug: /guided-tour/rendering/environment/ +description: Relay guide to the environment +keywords: +- environment +- RelayEnvironmentProvider +- useRelayEnvironment +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbActorsAndEnvironments from './fb/FbActorsAndEnvironments.md'; +import FbEnvironmentSetup from './fb/FbEnvironmentSetup.md'; + +## Relay Environment Provider + +In order to render Relay components, you need to render a `RelayEnvironmentProvider` component at the root of the app: + +```js +// App root + +const {RelayEnvironmentProvider} = require('react-relay'); +const Environment = require('MyEnvironment'); + +function Root() { + return ( + + {/*... */} + + ); +} +``` + +* The `RelayEnvironmentProvider` takes an environment, which it will make available to all descendant Relay components, and which is necessary for Relay to function. + + + +## Accessing the Relay Environment + +If you want to access the *current* Relay Environment within a descendant of a `RelayEnvironmentProvider` component, you can use the `useRelayEnvironment` Hook: + +```js +const {useRelayEnvironment} = require('react-relay'); + +function UserComponent(props: Props) { + const environment = useRelayEnvironment(); + + return (...); +} +``` + + + + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/rendering/error-states.md b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/error-states.md new file mode 100644 index 0000000000000..c07745359f189 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/error-states.md @@ -0,0 +1,295 @@ +--- +id: error-states +title: Error States with ErrorBoundaries +slug: /guided-tour/rendering/error-states/ +description: Relay guide to rendering error states +keywords: +- rendering +- error +- boundary +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbErrorBoundary from './fb/FbErrorBoundary.md'; + + + +As you may have noticed, we mentioned that using `usePreloadedQuery` will render data from a query that was (or is) being fetched from the server, but we didn't elaborate on how to render UI to show an error if an error occurred during fetch. We will cover that in this section. + +We can use [Error Boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) components to catch errors that occur during render (due to a network error, or any kind of error), and render an alternative error UI when that occurs. The way it works is similar to how `Suspense` works, by wrapping a component tree in an error boundary, we can specify how we want to react when an error occurs, for example by rendering a fallback UI. + +[Error boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) are simply components that implement the static `getDerivedStateFromError` method: + +```js +const React = require('React'); + +type State = {error: ?Error}; + +class ErrorBoundary extends React.Component { + static getDerivedStateFromError(error): State { + // Set some state derived from the caught error + return {error: error}; + } +} +``` + +```js +/** + * App.react.js + */ + +const ErrorBoundary = require('ErrorBoundary'); +const React = require('React'); + +const MainContent = require('./MainContent.react'); +const SecondaryContent = require('./SecondaryContent.react'); + +function App() { + return ( + // Render an ErrorSection if an error occurs within + // MainContent or Secondary Content + }> + + + + ); +} +``` + +* We can use the Error Boundary to wrap subtrees and show a different UI when an error occurs within that subtree. When an error occurs, the specified `fallback` will be rendered instead of the content inside the boundary. +* Note that we can also control the granularity at which we render error UIs, by wrapping components at different levels with error boundaries. In this example, if any error occurs within `MainContent` or `SecondaryContent`, we will render an `ErrorSection` in place of the entire app content. + + + +## Retrying after an Error + +### When using `useQueryLoader` / `loadQuery` + +When using `useQueryLoader`/`loadQuery` to fetch a query, in order to retry after an error has occurred, you can call `loadQuery` again and pass the *new* query reference to `usePreloadedQuery`: + +```js +/** + * ErrorBoundaryWithRetry.react.js + */ + +const React = require('React'); + +// NOTE: This is NOT actual production code; +// it is only used to illustrate example +class ErrorBoundaryWithRetry extends React.Component { + state = {error: null}; + + static getDerivedStateFromError(error): State { + return {error: error}; + } + + _retry = () => { + // This ends up calling loadQuery again to get and render + // a new query reference + this.props.onRetry(); + this.setState({ + // Clear the error + error: null, + }); + } + + render() { + const {children, fallback} = this.props; + const {error} = this.state; + if (error) { + if (typeof fallback === 'function') { + return fallback({error, retry: this._retry}); + } + return fallback; + } + return children; + } +} +``` +* When an error occurs, we render the provided `fallback`. +* When `retry` is called, we will clear the error, and call `loadQuery` again. This will fetch the query again and provide us a new query reference, which we can then pass down to `usePreloadedQuery`. + +```js +/** + * App.react.js + */ + +const ErrorBoundaryWithRetry = require('ErrorBoundaryWithRetry'); +const React = require('React'); + +const MainContent = require('./MainContent.react'); + +const query = require('__generated__/MainContentQuery.graphql'); + +// NOTE: This is NOT actual production code; +// it is only used to illustrate example +function App(props) { + // E.g., initialQueryRef provided by router + const [queryRef, loadQuery] = useQueryLoader(query, props.initialQueryRef); + + return ( + loadQuery(/* ... */)} + fallback={({error, retry}) => + <> + + {/* Render a button to retry; this will attempt to re-render the + content inside the boundary, i.e. the query component */} + + + }> + {/* The value of queryRef will be updated after calling + loadQuery again */} + + + ); +} + +/** + * MainContent.react.js + */ +function MainContent(props) { + const data = usePreloadedQuery( + graphql`...`, + props.queryRef + ); + + return (/* ... */); +} +``` +* The sample Error Boundary in this example code will provide a `retry` function to the `fallback` which we can use to clear the error, re-load the query, and re-render with a new query ref that we can pass to the component that uses `usePreloadedQuery`. That component will consume the new query ref and suspend if necessary on the new network request. + + +### When using `useLazyLoadQuery` + +When using `useLazyLoadQuery` to fetch a query, in order to retry after an error has occurred, you can attempt to re-mount *and* re-evaluate the query component by passing it a new `fetchKey`: + +```js +/** + * ErrorBoundaryWithRetry.react.js + */ + +const React = require('React'); + +// NOTE: This is NOT actual production code; +// it is only used to illustrate example +class ErrorBoundaryWithRetry extends React.Component { + state = {error: null, fetchKey: 0}; + + static getDerivedStateFromError(error): State { + return {error: error, fetchKey: 0}; + } + + _retry = () => { + this.setState(prev => ({ + // Clear the error + error: null, + // Increment and set a new fetchKey in order + // to trigger a re-evaluation and refetching + // of the query using useLazyLoadQuery + fetchKey: prev.fetchKey + 1, + })); + } + + render() { + const {children, fallback} = this.props; + const {error, fetchKey} = this.state; + if (error) { + if (typeof fallback === 'function') { + return fallback({error, retry: this._retry}); + } + return fallback; + } + return children({fetchKey}); + } +} +``` +* When an error occurs, we render the provided `fallback`. +* When `retry` is called, we will clear the error, and increment our `fetchKey` which we can then pass down to `useLazyLoadQuery`. This will make it so we re-render the component that uses `useLazyLoadQuery` with a new `fetchKey`, ensuring that the query is refetched upon the new call to `useLazyLoadQuery`. + +```js +/** + * App.react.js + */ + +const ErrorBoundaryWithRetry = require('ErrorBoundaryWithRetry'); +const React = require('React'); + +const MainContent = require('./MainContent.react'); + +// NOTE: This is NOT actual production code; +// it is only used to illustrate example +function App() { + return ( + + <> + + {/* Render a button to retry; this will attempt to re-render the + content inside the boundary, i.e. the query component */} + + + }> + {({fetchKey}) => { + // If we have retried, use the new `retryQueryRef` provided + // by the Error Boundary + return ; + }} + + ); +} + +/** + * MainContent.react.js + */ +function MainContent(props) { + const data = useLazyLoadQuery( + graphql`...`, + variables, + {fetchKey: props.fetchKey} + ); + + return (/* ... */); +} +``` +* The sample Error Boundary in this example code will provide a `retry` function to the `fallback` which we can use to clear the error and re-render `useLazyLoadQuery` with a new `fetchKey`. This will cause the query to be re-evaluated and refetched, and `useLazyLoadQuery` start a new network request and suspend. + + + +## Accessing errors in GraphQL Responses + + + + +By default, internally at fb, Relay will *only* surface errors to React that are returned in the top-level [`errors` field](https://graphql.org/learn/validation/) if they are ether: + +* of `CRITICAL` severity, +* *or* if the top-level `data` field wasn't returned in the response. + + + + +If you wish to access error information in your application to display user friendly messages, the recommended approach is to model and expose the error information as part of your GraphQL schema. + +For example, you could expose a field in your schema that returns either the expected result, or an Error object if an error occurred while resolving that field (instead of returning null): + + +```js +type Error { + # User friendly message + message: String! +} + +type Foo { + bar: Result | Error +} +``` + + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/rendering/fragments.md b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/fragments.md new file mode 100644 index 0000000000000..300913a407fce --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/fragments.md @@ -0,0 +1,354 @@ +--- +id: fragments +title: Fragments +slug: /guided-tour/rendering/fragments/ +description: Relay guide to rendering fragments +keywords: +- useFragment +- rendering +- fragment +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +The main building block for declaring data dependencies for React Components in Relay are [GraphQL Fragments](https://graphql.org/learn/queries/#fragments). Fragments are reusable units in GraphQL that represent a set of data to query from a GraphQL type exposed in the [schema](https://graphql.org/learn/schema/). + +In practice, they are a selection of fields on a GraphQL Type: + +```graphql +fragment UserFragment on User { + name + age + profile_picture(scale: 2) { + uri + } +} +``` + + +In order to declare a fragment inside your JavaScript code, you must use the `graphql` tag: + +```js +const {graphql} = require('react-relay'); + +const userFragment = graphql` + fragment UserFragment_user on User { + name + age + profile_picture(scale: 2) { + uri + } + } +`; +``` + +## Rendering Fragments + +In order to *render* the data for a fragment, you can use the `useFragment` Hook: + +```js +import type {UserComponent_user$key} from 'UserComponent_user.graphql'; + +const React = require('React'); + +const {graphql, useFragment} = require('react-relay'); + +type Props = { + user: UserComponent_user$key, +}; + +function UserComponent(props: Props) { + const data = useFragment( + graphql` + fragment UserComponent_user on User { + name + profile_picture(scale: 2) { + uri + } + } + `, + props.user, + ); + + return ( + <> +

{data.name}

+
+ +
+ + ); +} + +module.exports = UserComponent; +``` + +Let's distill what's going on here: + +* `useFragment` takes a fragment definition and a *fragment reference*, and returns the corresponding `data` for that fragment and reference. + * This is similar to `usePreloadedQuery`, which takes a query definition and a query reference. +* A *fragment reference* is an object that Relay uses to *read* the data declared in the fragment definition; as you can see, the `UserComponent_user` fragment itself just declares fields on the `User` type, but we need to know *which* specific user to read those fields from; this is what the fragment reference corresponds to. In other words, a fragment reference is like *a pointer to a specific instance of a type* that we want to read data from. +* Note that *the component is automatically subscribed to updates to the fragment data*: if the data for this particular `User` is updated anywhere in the app (e.g. via fetching new data, or mutating existing data), the component will automatically re-render with the latest updated data. +* Relay will automatically generate Flow types for any declared fragments when the compiler is run, so you can use these types to declare the type for your Component's `props`. + * The generated Flow types include a type for the fragment reference, which is the type with the `$key` suffix: `$key`, and a type for the shape of the data, which is the type with the `$data` suffix: `$data`; these types are available to import from files that are generated with the following name: `.graphql.js`. + * We use our [lint rule](https://github.com/relayjs/eslint-plugin-relay) to enforce that the type of the fragment reference prop is correctly declared when using `useFragment`. By using a properly typed fragment reference as input, the type of the returned `data` will automatically be Flow-typed without requiring an explicit annotation. + * In our example, we're typing the `user` prop as the fragment reference we need for `useFragment`, which corresponds to the `UserComponent_user$key` imported from `UserComponent_user.graphql`, which means that the type of `data` above would be: `{ name: ?string, profile_picture: ?{ uri: ?string } }`. +* Fragment names need to be globally unique. In order to easily achieve this, we name fragments using the following convention based on the module name followed by an identifier: `_`. This makes it easy to identify which fragments are defined in which modules and avoids name collisions when multiple fragments are defined in the same module. + + +If you need to render data from multiple fragments inside the same component, you can use `useFragment` multiple times: + +```js +import type {UserComponent_user$key} from 'UserComponent_user.graphql'; +import type {UserComponent_viewer$key} from 'UserComponent_viewer.graphql'; + +const React = require('React'); +const {graphql, useFragment} = require('react-relay'); + +type Props = { + user: UserComponent_user$key, + viewer: UserComponent_viewer$key, +}; + +function UserComponent(props: Props) { + const userData = useFragment( + graphql` + fragment UserComponent_user on User { + name + profile_picture(scale: 2) { + uri + } + } + `, + props.user, + ); + + const viewerData = useFragment( + graphql` + fragment UserComponent_viewer on Viewer { + actor { + name + } + } + `, + props.viewer, + ); + + return ( + <> +

{userData.name}

+
+ + Acting as: {viewerData.actor?.name ?? 'Unknown'} +
+ + ); +} + +module.exports = UserComponent; +``` + +## Composing Fragments + +In GraphQL, fragments are reusable units, which means they can include *other* fragments, and consequently a fragment can be included within other fragments or [queries](../queries/): + +```graphql +fragment UserFragment on User { + name + age + profile_picture(scale: 2) { + uri + } + ...AnotherUserFragment +} + +fragment AnotherUserFragment on User { + username + ...FooUserFragment +} +``` + + +With Relay, you can compose fragment components in a similar way, using both component composition and fragment composition. Each React component is responsible for fetching the data dependencies of its direct children - just as it has to know about its children's props in order to render them correctly. This pattern means that developers are able to reason locally about components - what data they need, what components they render - but Relay is able to derive a global view of the data dependencies of an entire UI tree. + +```js +/** + * UsernameSection.react.js + * + * Child Fragment Component + */ + +import type {UsernameSection_user$key} from 'UsernameSection_user.graphql'; + +const React = require('React'); +const {graphql, useFragment} = require('react-relay'); + +type Props = { + user: UsernameSection_user$key, +}; + +function UsernameSection(props: Props) { + const data = useFragment( + graphql` + fragment UsernameSection_user on User { + username + } + `, + props.user, + ); + + return
{data.username ?? 'Unknown'}
; +} + +module.exports = UsernameSection; +``` + +```js +/** + * UserComponent.react.js + * + * Parent Fragment Component + */ + +import type {UserComponent_user$key} from 'UserComponent_user.graphql'; + +const React = require('React'); +const {graphql, useFragment} = require('react-relay'); + +const UsernameSection = require('./UsernameSection.react'); + +type Props = { + user: UserComponent_user$key, +}; + +function UserComponent(props: Props) { + const user = useFragment( + graphql` + fragment UserComponent_user on User { + name + age + profile_picture(scale: 2) { + uri + } + + # Include child fragment: + ...UsernameSection_user + } + `, + props.user, + ); + + return ( + <> +

{user.name}

+
+ + {user.age} + + {/* Render child component, passing the _fragment reference_: */} + +
+ + ); +} + +module.exports = UserComponent; +``` + +There are a few things to note here: + +* `UserComponent` both renders `UsernameSection`, *and* includes the fragment declared by `UsernameSection` inside its own `graphql` fragment declaration. +* `UsernameSection` expects a *fragment reference* as the `user` prop. As we've mentioned before, a fragment reference is an object that Relay uses to *read* the data declared in the fragment definition; as you can see, the child `UsernameSection_user` fragment itself just declares fields on the `User` type, but we need to know *which* specific user to read those fields from; this is what the fragment reference corresponds to. In other words, a fragment reference is like *a pointer to a specific instance of a type* that we want to read data from. +* Note that in this case the `user` passed to `UsernameSection`, i.e. the fragment reference, *doesn't actually contain any of the data declared by the child `UsernameSection` component*; instead, `UsernameSection` will use the fragment reference to read the data *it* declared internally, using `useFragment`. + * This means that the parent component will not receive the data selected by a child component (unless that parent explicitly selected the same fields). Likewise, child components will not receive the data selected by their parents (again, unless the child selected those same fields). + * This prevents separate components from *even accidentally* having implicit dependencies on each other. If this wasn't the case, modifying a component could break other components! + * This allows us to reason locally about our components and modify them without worrying about affecting other components. + * This is known as [*data masking*](../../../principles-and-architecture/thinking-in-relay/). +* The *fragment reference* that the child (i.e. `UsernameSection`) expects is the result of reading a parent fragment that *includes* the child fragment. In our particular example, that means the result of reading a fragment that includes `...UsernameSection_user` will be the fragment reference that `UsernameSection` expects. In other words, the data obtained as a result of reading a fragment via `useFragment` also serves as the fragment reference for any child fragments included in that fragment. + + +## Composing Fragments into Queries + +Fragments in Relay allow declaring data dependencies for a component, but they ***can't be fetched by themselves***. Instead, they need to be included in a query, either directly or transitively. This means that *all fragments must belong to a query when they are rendered*, or in other words, they must be "rooted" under some query. Note that a single fragment can still be included by multiple queries, but when rendering a specific *instance* of a fragment component, it must have been included as part of a specific query request. + +To fetch and render a query that includes a fragment, you can compose them in the same way fragments are composed, as shown in the [Composing Fragments](#composing-fragments) section: + +```js +/** + * UserComponent.react.js + * + * Fragment Component + */ + +import type {UserComponent_user$key} from 'UserComponent_user.graphql'; + +const React = require('React'); +const {graphql, useFragment} = require('react-relay'); + +type Props = { + user: UserComponent_user$key, +}; + +function UserComponent(props: Props) { + const data = useFragment( + graphql`...`, + props.user, + ); + + return (...); +} + +module.exports = UserComponent; +``` + +```js +/** + * App.react.js + * + * Query Component + */ + +import type {AppQuery} from 'AppQuery.graphql'; +import type {PreloadedQuery} from 'react-relay'; + +const React = require('React'); +const {graphql, usePreloadedQuery} = require('react-relay'); + +const UserComponent = require('./UserComponent.react'); + +type Props = { + appQueryRef: PreloadedQuery, +} + +function App({appQueryRef}) { + const data = usePreloadedQuery( + graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + + # Include child fragment: + ...UserComponent_user + } + } + `, + appQueryRef, + ); + + return ( + <> +

{data.user?.name}

+ {/* Render child component, passing the fragment reference: */} + + + ); +} +``` + +Note that: + +* The *fragment reference* that `UserComponent` expects is the result of reading a parent query that includes its fragment, which in our case means a query that includes `...UsernameSection_user`. In other words, the `data` obtained as a result of `usePreloadedQuery` also serves as the fragment reference for any child fragments included in that query. +* As mentioned previously, *all fragments must belong to a query when they are rendered,* which means that all fragment components *must* be descendants of a query. This guarantees that you will always be able to provide a fragment reference for `useFragment`, by starting from the result of reading a root query with `usePreloadedQuery`. + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/rendering/loading-states.md b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/loading-states.md new file mode 100644 index 0000000000000..0c52b41bd5781 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/loading-states.md @@ -0,0 +1,257 @@ +--- +id: loading-states +title: Loading States with Suspense +slug: /guided-tour/rendering/loading-states/ +description: Relay guide to loading states +keywords: +- suspense +- loading +- glimmer +- fallback +- spinner +--- + +import DocsRating from '@site/src/core/DocsRating'; +import FbSuspensePlaceholder from '../../fb/FbSuspensePlaceholder.md'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbSuspenseDefinition from './fb/FbSuspenseDefinition.md'; +import FbSuspenseMoreInfo from './fb/FbSuspenseMoreInfo.md'; +import FbSuspenseTransitionsAndUpdatesThatSuspend from './fb/FbSuspenseTransitionsAndUpdatesThatSuspend.md'; +import FbSuspenseInRelayTransitions from './fb/FbSuspenseInRelayTransitions.md'; +import FbSuspenseInRelayFragments from './fb/FbSuspenseInRelayFragments.md'; + + +As you may have noticed, we mentioned that using `usePreloadedQuery` and `useLazyLoadQuery` will render data from a query that was being fetched from the server, but we didn't elaborate on how to render a loading UI (such as a glimmer) while that data is still being fetched. We will cover that in this section. + + + + + + + +To render loading states while a query is being fetched, we rely on [React Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html). Suspense is a new feature in React that allows components to interrupt or *"suspend"* rendering in order to wait for some asynchronous resource (such as code, images or data) to be loaded; when a component "suspends", it indicates to React that the component isn't *"ready"* to be rendered yet, and won't be until the asynchronous resource it's waiting for is loaded. When the resource finally loads, React will try to render the component again. + + + +This capability is useful for components to express asynchronous dependencies like data, code, or images that they require in order to render, and lets React coordinate rendering the loading states across a component tree as these asynchronous resources become available. More generally, the use of Suspense give us better control to implement more deliberately designed loading states when our app is loading for the first time or when it's transitioning to different states, and helps prevent accidental flickering of loading elements (such as spinners), which can commonly occur when loading sequences aren't explicitly designed and coordinated. + + + + + + + + +:::caution +Note that this **DOES NOT** mean that "Suspense for Data Fetching" is ready for general implementation and adoption yet. **Support, general guidance, and requirements for usage of Suspense for Data Fetching are still not ready**, and the React team is still defining what this guidance will be for upcoming React releases. + +Even though there will be some limitations when Suspense is used in React 17, Relay Hooks are stable and on the trajectory for supporting upcoming releases of React. + +For more information, see our **[Suspense Compatibility](../../../migration-and-compatibility/suspense-compatibility/)** guide. +::: + + + +## Loading fallbacks with Suspense Boundaries + +When a component is suspended, we need to render a *fallback* in place of the component while we wait for it to become *"ready"*. In order to do so, we use the `Suspense` component provided by React: + +```js +const React = require('React'); +const {Suspense} = require('React'); + +function App() { + return ( + // Render a fallback using Suspense as a wrapper + }> + + + ); +} +``` + + +`Suspense` components can be used to wrap any component; if the target component suspends, `Suspense` will render the provided fallback until all its descendants become *"ready"* (i.e. until *all* of the suspended components within the subtree resolve). Usually, the fallback is used to render fallback loading states such as a glimmers and placeholders. + +Usually, different pieces of content in our app might suspend, so we can show loading state until they are resolved by using `Suspense` : + +```js +/** + * App.react.js + */ + +const React = require('React'); +const {Suspense} = require('React'); + +function App() { + return ( + // LoadingGlimmer is rendered via the Suspense fallback + }> + {/* MainContent may suspend */} + + ); +} +``` + + + + + +Let's distill what's going on here: + +* If `MainContent` suspends because it's waiting on some asynchronous resource (like data), the `Suspense` component that wraps `MainContent` will detect that it suspended, and will render the `fallback` element (i.e. the `LoadingGlimmer` in this case) up until `MainContent` is ready to be rendered. Note that this also transitively includes descendants of `MainContent`, which might also suspend. + + +What's nice about Suspense is that you have granular control about how to accumulate loading states for different parts of your component tree: + +```js +/** + * App.react.js + */ + +const React = require('React'); +const {Suspense} = require('React'); + +function App() { + return ( + // A LoadingGlimmer for all content is rendered via the Suspense fallback + }> + + {/* SecondaryContent can also suspend */} + + ); +} +``` + + + +* In this case, both `MainContent` and `SecondaryContent` may suspend while they load their asynchronous resources; by wrapping both in a `Suspense`, we can show a single loading state up until they are *all* ready, and then render the entire content in a single paint, after everything has successfully loaded. +* In fact, `MainContent` and `SecondaryContent` may suspend for different reasons other than fetching data, but the same `Suspense` component can be used to render a fallback up until *all* components in the subtree are ready to be rendered. Note that this also transitively includes descendants of `MainContent` or `SecondaryContent`, which might also suspend. + + +Conversely, you can also decide to be more granular about your loading UI and wrap Suspense components around smaller or individual parts of your component tree: + +```js +/** + * App.react.js + */ + +const React = require('React'); +const {Suspense} = require('React'); + +function App() { + return ( + <> + {/* Show a separate loading UI for the LeftHandColumn */} + }> + + + + {/* Show a separate loading UI for both the Main and Secondary content */} + }> + + + + + ); +} +``` + + + +* In this case, we're showing 2 separate loading UIs: + * One to be shown until the `LeftColumn` becomes ready + * And one to be shown until both the `MainContent` and `SecondaryContent` become ready. +* What is powerful about this is that by more granularly wrapping our components in Suspense, *we allow other components to be rendered earlier as they become ready*. In our example, by separately wrapping `MainContent` and `SecondaryContent` under `Suspense`, we're allowing `LeftColumn` to render as soon as it becomes ready, which might be earlier than when the content sections become ready. + + +## Transitions and Updates that Suspend + + + + + + + +`Suspense` boundary fallbacks allow us to describe our loading placeholders when initially rendering some content, but our applications will also have transitions between different content. Specifically, when switching between two components within an already mounted boundary, the new component you're switching to might not have loaded all of its async dependencies, which means that it might also suspend. + +In these cases, we would still show the `Suspense` boundary fallbacks. However, this means that we would hide existing content in favor of showing the `Suspense` fallback. In future versions of React when concurrent rendering is supported, React will provide an option to support this case and avoid hiding already rendered content with a Suspense fallback when suspending. + + + +## How We Use Suspense in Relay + +### Queries + +In our case, our query components are components that can suspend, so we use Suspense to render loading states while a query is being fetched. Let's see what that looks like in practice: + +Say we have the following query renderer component: + +```js +/** + * MainContent.react.js + * + * Query Component + */ + +const React = require('React'); +const {graphql, usePreloadedQuery} = require('react-relay'); + +function MainContent(props) { + // Fetch and render a query + const data = usePreloadedQuery( + graphql`...`, + props.queryRef, + ); + + return (...); +} +``` + +```js +/** + * App.react.js + */ + +const React = require('React'); +const {Suspense} = require('React'); + +function App() { + return ( + // LoadingGlimmer is rendered via the Suspense fallback + }> + {/* MainContent may suspend */} + + ); +} +``` + + + +Let's distill what's going on here: + +* We have a `MainContent` component, which is a query renderer that fetches and renders a query. `MainContent` will *suspend* rendering when it attempts to fetch the query, indicating that it isn't ready to be rendered yet, and it will resolve when the query is fetched. +* The `Suspense `component that wraps `MainContent` will detect that `MainContent` suspended, and will render the `fallback` element (i.e. the `LoadingGlimmer` in this case) up until `MainContent` is ready to be rendered; that is, up until the query is fetched. + + +### Fragments + + + + + + + +Fragments are also integrated with Suspense in order to support rendering of data that's being `@defer'`d or data that's partially available in the Relay Store (i.e. [partial rendering](../../reusing-cached-data/rendering-partially-cached-data/)). + + + +### Transitions + + + + + +Additionally, our APIs for refetching ([Refreshing and Refetching](../../refetching/)) and for [rendering connections](../../list-data/connections/) are also integrated with Suspense; for these use cases, these APIs will also suspend. + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/rendering/queries.md b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/queries.md new file mode 100644 index 0000000000000..9cd08b286363b --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/queries.md @@ -0,0 +1,261 @@ +--- +id: queries +title: Queries +slug: /guided-tour/rendering/queries/ +description: Relay guide to queries +keywords: +- query +- usePreloadedQuery +- useLazyLoadQuery +- useQueryLoader +- loadQuery +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +import FbEntrypointsExtraInfo from './fb/FbEntrypointsExtraInfo.md'; + +A [GraphQL Query](https://graphql.org/learn/queries/) is a description of data you want to query from a GraphQL server. It consists of a set of fields (and potentially [fragments](../fragments/)) that we want to request from the GraphQL server. What we can query for will depend on the [GraphQL Schema](https://graphql.org/learn/schema/) exposed on the server, which describes the data that is available for querying. + +A query can be sent as a request over the network, along with an optional collection of [variables](../variables/) that the query uses, in order to fetch the data. The server response will be a JSON object that matches the shape of the query we sent: + +```graphql +query UserQuery($id: ID!) { + user(id: $id) { + id + name + ...UserFragment + } + viewer { + actor { + name + } + } +} + +fragment UserFragment on User { + username +} +``` + + + +[Sample response](https://fburl.com/graphiql/v5hs717f): + + + + + +Sample response: + + + +```json +{ + "data": { + "user": { + "id": "4", + "name": "Mark Zuckerberg", + "username": "zuck" + }, + "viewer": { + "actor": { + "name": "Your Name" + } + } + } +} +``` + + + +## Rendering Queries + +To *render* a query in Relay, you can use the `usePreloadedQuery` hook. `usePreloadedQuery` takes a query definition and a query reference, and returns the corresponding data for that query and reference. + +```js +import type {HomeTabQuery} from 'HomeTabQuery.graphql'; +import type {PreloadedQuery} from 'react-relay'; + +const React = require('React'); +const {graphql, usePreloadedQuery} = require('react-relay'); + +type Props = { + queryRef: PreloadedQuery, +}; + +function HomeTab(props: Props) { + const data = usePreloadedQuery( + graphql` + query HomeTabQuery($id: ID!) { + user(id: $id) { + name + } + } + `, + props.queryRef, + ); + + return ( +

{data.user?.name}

+ ); +} +``` + +Lets see what's going on here: + +* `usePreloadedQuery` takes a `graphql` query and a `PreloadedQuery` reference, and returns the data that was fetched for that query. + * The `PreloadedQuery` (in this case `queryRef`) is an object that describes and references an *instance* of our query that is being (or was) fetched. + * We'll cover how to actually fetch the query in the next section below, and cover how to show loading states if the query is in-flight when we try to render it in the [Loading States with Suspense](../loading-states/) section. +* Similarly to [fragments](../fragments/), *the component is automatically subscribed to updates to the query data*: if the data for this query is updated anywhere in the app, the component will automatically re-render with the latest updated data. +* `usePreloadedQuery` also takes a Flow type parameter, which corresponds to the Flow type for the query, in this case `HomeTabQuery`. + * The Relay compiler automatically generates Flow types for any declared queries, which are available to import from the generated files with the following name format: *``*`.graphql.js`. + * Note that the `data` is already properly Flow-typed without requiring an explicit annotation, and is based on the types from the GraphQL schema. For example, the type of `data` above would be: `{ user: ?{ name: ?string } }`. +* Make sure you're providing a Relay environment using a [Relay Environment Provider](../environment/) at the root of your app before trying to render a query. + + +## Fetching Queries for Render + +Apart from *rendering* a query, we also need to fetch it from the server. Usually we want to fetch queries somewhere at the root of our app, and only have **one or a few queries that [*accumulate*](../fragments/#composing-fragments-into-queries) all the data required to render the screen**. Ideally, we'd fetch them as early as possible, before we even start rendering our app. + +In order to *fetch* a query for later rendering it, you can use the `useQueryLoader` Hook: + +```js +import type {HomeTabQuery as HomeTabQueryType} from 'HomeTabQuery.graphql'; +import type {PreloadedQuery} from 'react-relay'; + +const HomeTabQuery = require('HomeTabQuery.graphql') +const {useQueryLoader} = require('react-relay'); + + +type Props = { + initialQueryRef: PreloadedQuery, +}; + +function AppTabs(props) { + const [ + homeTabQueryRef, + loadHomeTabQuery, + ] = useQueryLoader( + HomeTabQuery, + props.initialQueryRef, /* e.g. provided by router */ + ); + + const onSelectHomeTab = () => { + // Start loading query for HomeTab immediately in the event handler + // that triggers navigation to that tab, *before* we even start + // rendering the target tab. + // Calling this function will update the value of homeTabQueryRef. + loadHomeTabQuery({id: '4'}); + + // ... + } + + // ... + + return ( + screen === 'HomeTab' && homeTabQueryRef != null ? + // Pass to component that uses usePreloadedQuery + : + // ... + ); +} +``` + +The example above is somewhat contrived, but let's distill what is happening: + +* We are calling `useQueryLoader` inside our `AppTabs` component. + * It takes a query, which in this case is our `HomeTabQuery` (the query that we declared in our previous example), and which we can obtain by requiring the auto-generated file: `'HomeTabQuery.graphql'`. + * It takes an optional initial `PreloadedQuery` to be used as the initial value of the `homeTabQueryRef` that is stored in state and returned by `useQueryLoader`. + * It also additionally takes a Flow type parameter, which corresponds to the Flow type for the query, in this case `HomeTabQueryType`, which you can also obtain from the auto-generated file: `'HomeTabQuery.graphql'`. +* Calling `useQueryLoader` allows us to obtain 2 things: + * `homeTabQueryRef`: A `?PreloadedQuery`, which is an object that describes and references an *instance* of our query that is being (or was) fetched. This value will be null if we haven't fetched the query, i.e. if we haven't called `loadHomeTabQuery`. + * `loadHomeTabQuery`: A function that will *fetch* the data for this query from the server (if it isn't already cached), and given an object with the [variables](../variables/) the query expects, in this case `{id: '4'}` (we'll go into more detail about how Relay uses cached data in the [Reusing Cached Data For Render](../../reusing-cached-data/) section). Calling this function will also update the value of `homeTabQueryRef` to an instance of a `PreloadedQuery`. + * Note that the `variables` we pass to this function will be checked by Flow to ensure that you are passing values that match what the GraphQL query expects. + * Also note that we are calling this function in the event handler that causes the `HomeTab` to be rendered. This allows us to start fetching the data for the screen as early as possible, even before the new tab starts rendering. + * In fact, `loadQuery` will throw an error if it is called during React's render phase! +* Note that `useQueryLoader` will automatically dispose of all queries that have been loaded when the component unmounts. Disposing of a query means that Relay will no longer hold on to the data for that particular instance of the query in its cache (we'll cover the lifetime of query data in [Reusing Cached Data For Render](../../reusing-cached-data/) section). Additionally, if the request for the query is still in flight when disposal occurs, it will be canceled. +* Our `AppTabs` component renders the `HomeTab` component from the previous example, and passes it the corresponding query reference. Note that this parent component owns the lifetime of the data for that query, meaning that when it unmounts, it will of dispose of that query, as mentioned above. +* Finally, make sure you're providing a Relay environment using a [Relay Environment Provider](../environment/) at the root of your app before trying to use `useQueryLoader`. + + +Sometimes, you want to start a fetch outside of the context of a parent component, for example to fetch the data required for the initial load of the application. For these cases, you can use the `loadQuery` API directly, without using `useQueryLoader`: + +```js +import type {HomeTabQuery as HomeTabQueryType} from 'HomeTabQuery.graphql'; + +const HomeTabQuery = require('HomeTabQuery.graphql') +const {loadQuery} = require('react-relay'); + + +const environment = createEnvironment(...); + +// At some point during app initialization +const initialQueryRef = loadQuery( + environment, + HomeTabQuery, + {id: '4'}, +); + +// ... + +// E.g. passing the initialQueryRef to the root component +render() +``` + +* In this example, we are calling the `loadQuery` function directly to obtain a `PreloadedQuery` instance that we can later pass to a component that uses `usePreloadedQuery`. +* In this case, we would expect the root `AppTabs` component to manage the lifetime of the query reference, and dispose of it at the appropriate time, if at all. +* We've left the details of "app initialization" vague in this example, since that will vary from application to application. The important thing to note here is that we should obtain a query reference before we start rendering the root component. In fact, `loadQuery` will throw an error if it is called during React's render phase! + + +### Render as you Fetch + +The examples above illustrate how to separate fetching the data from rendering it, in order to start the fetch as early as possible (as opposed to waiting until the component is rendered to start the fetch), and allow us to show content to our users a lot sooner. It also helps prevent waterfalling round trips, and gives us more control and predictability over when the fetch occurs, whereas if we fetch during render, it becomes harder to determine when the fetch will (or should) occur. This fits nicely with the [*"render-as-you-fetch"*](https://reactjs.org/docs/concurrent-mode-suspense.html#approach-3-render-as-you-fetch-using-suspense) pattern with [React Suspense](../loading-states/). + +This is the preferred pattern for fetching data with Relay, and it applies in several circumstances, such as the initial load of an application, during subsequent navigations, or generally when using UI elements which are initially hidden and later revealed upon an interaction (such as menus, popovers, dialogs, etc), and which also require fetching additional data. + + + + +## Lazily Fetching Queries during Render + +Another alternative for fetching a query is to lazily fetch the query when the component is rendered. However, as we've mentioned previously, the preferred pattern is to start fetching queries ahead of rendering. If lazy fetching is used without caution, it can trigger nested or waterfalling round trips, and can degrade performance. + +To fetch a query lazily, you can use the `useLazyLoadQuery` Hook: + +```js +const React = require('React'); +const {graphql, useLazyLoadQuery} = require('react-relay'); + +function App() { + const data = useLazyLoadQuery( + graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + } + } + `, + {id: '4'}, + ); + + return ( +

{data.user?.name}

+ ); +} +``` +Lets see what's going on here: + +* `useLazyLoadQuery` takes a graphql query and some variables for that query, and returns the data that was fetched for that query. The variables are an object containing the values for the [variables](../variables/) referenced inside the GraphQL query. +* Similarly to [fragments](../fragments/), the component is automatically subscribed to updates to the query data: if the data for this query is updated anywhere in the app, the component will automatically re-render with the latest updated data. +* `useLazyLoadQuery` additionally takes a Flow type parameter, which corresponds to the Flow type for the query, in this case AppQuery. + * Remember that Relay automatically generates Flow types for any declared queries, which you can import and use with `useLazyLoadQuery`. These types are available in the generated files with the following name format: `.graphql.js`. + * Note that the `variables` will be checked by Flow to ensure that you are passing values that match what the GraphQL query expects. + * Note that the data is already properly Flow-typed without requiring an explicit annotation, and is based on the types from the GraphQL schema. For example, the type of `data` above would be: `{ user: ?{ name: ?string } }`. +* By default, when the component renders, Relay will *fetch* the data for this query (if it isn't already cached), and return it as a the result of the `useLazyLoadQuery` call. We'll go into more detail about how to show loading states in the [Loading States with Suspense](../loading-states/) section, and how Relay uses cached data in the [Reusing Cached Data For Rendering](../../reusing-cached-data/) section. +* Note that if you re-render your component and pass *different query variables* than the ones originally used, it will cause the query to be fetched again with the new variables, and potentially re-render with different data. +* Finally, make sure you're providing a Relay environment using a [Relay Environment Provider](../../../api-reference/relay-environment-provider/) at the root of your app before trying to render a query. + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/rendering/variables.md b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/variables.md new file mode 100644 index 0000000000000..39154f6c9607a --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/rendering/variables.md @@ -0,0 +1,233 @@ +--- +id: variables +title: Variables +slug: /guided-tour/rendering/variables/ +description: Relay guide to query variables +keywords: +- query +- variables +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +You may have noticed that the query declarations in our examples above contain references to an `$id` symbol inside the GraphQL code: these are [GraphQL Variables](https://graphql.org/learn/queries/#variables). + +GraphQL variables are a construct that allows referencing dynamic values inside a GraphQL query. When fetching a query from the server, we also need to provide as input the actual set of values to use for the variables declared inside the query: + +```graphql +query UserQuery($id: ID!) { + # The value of $id is used as input to the user() call: + user(id: $id) { + id + name + } +} +``` + +In the above, `ID!` is the type of the `$id` variable. That is, it is a required ID. + +When sending a network request to fetch the query above, we need to provide both the query, and the variables to be used for this particular execution of the query. For example: + +```graphql +# Query: +query UserQuery($id: ID!) { + # ... +} + +# Variables: +{"id": 4} +``` + + + + +Fetching the above query and variables from the server would produce the following response, which can also be visualized in [GraphiQL](https://fburl.com/graphiql/kiuar058): + + + + + +Fetching the above query and variables from the server would produce the following response: + + + +```json +{ + "data": { + "user": { + "id": "4", + "name": "Mark Zuckerberg" + } + } +} +``` + + +* * * + +Fragments can also reference variables that have been declared by a query: + +```graphql +fragment UserFragment on User { + name + profile_picture(scale: $scale) { + uri + } +} + + +query ViewerQuery($scale: Float!) { + viewer { + actor { + ...UserFragment + } + } +} +``` + +* Even though the fragment above doesn't *declare* the `$scale` variable directly, it can still reference it. Doing so makes it so any query that includes this fragment, either directly or transitively, *must* declare the variable and its type, otherwise an error will be produced. +* In other words, *query variables are available globally by any fragment that is a descendant of the query*. +* A fragment which references a global variable can only be included (directly or transitively) in a query which defines that global variable. + + +In Relay, fragment declarations inside components can also reference query variables: + +```js +function UserComponent(props: Props) { + const data = useFragment( + graphql` + fragment UserComponent_user on User { + name + profile_picture(scale: $scale) { + uri + } + } + `, + props.user, + ); + + return (...); +} +``` + +* The above fragment could be included by multiple queries, and rendered by different components, which means that any query that ends up rendering/including the above fragment *must* declare the `$scale` variable. +* If any query that happens to include this fragment *doesn't* declare the `$scale` variable, an error will be produced by the Relay Compiler at build time, ensuring that an incorrect query never gets sent to the server (sending a query with missing variable declarations will also produce an error in the server). + + + +## @arguments and @argumentDefinitions + +Relay also provides a way to declare variables that are scoped locally to a fragment using the `@arguments` and `@argumentDefinitions` directives. Fragments that use local variables are easy to customize and reuse, since they do not depend on the value of global (query-level) variables. + +```js +/** + * Declare a fragment that accepts arguments with @argumentDefinitions + */ + +function TaskView(props) { + const data = useFragment( + graphql` + fragment TaskView_task on Task + @argumentDefinitions(showDetailedResults: {type: "Boolean!"}) { + name + is_completed + ... @include(if: $showDetailedResults) { + description + } + } + `, + props.task, + ); +} +``` + +```js +/** + * Include fragment using @arguments + */ + +function TaskList(props) { + const data = usePreloadedQuery( + graphql` + query TaskListQuery { + todays_tasks { + ...TaskView_task @arguments(showDetailedResults: true) + } + tomorrows_tasks { + ...TaskView_task @arguments(showDetailedResults: false) + } + } + `, + props.queryRef, + ); +} +``` + +* Locally-scoped variables also make it easier to reuse a fragment from another query. + * A query definition must list all variables that are used by any nested fragments, including in recursively-nested fragments. + * Since a fragment can potentially be accessible from many queries, modifying a fragment that uses global variables can require changes to many query definitions. + * This can also lead to awkward situations, like having multiple versions of the "same" variable, such as `$showDetailedResults` and `$showDetails`. + + * Since fragments with only locally-scoped variables do not use global variables, they do not suffer from this issue. + +* Note that when passing `@arguments` to a fragment, we can pass a literal value or pass another variable. The variable can be a global query variable, a local variable declared via `@argumentDefinitions` or a literal (e.g. `42.0`). +* When we actually fetch `TaskView_task` as part of a query, the `showDetailedResults` value will depend on the argument that was provided by the parent of `TaskView_task`: + +Fragments that expect arguments can also declare default values, making the arguments optional: + +```js +/** + * Declare a fragment that accepts arguments with default values + */ + +function TaskView(props) { + const data = useFragment( + graphql` + fragment TaskView_task on Task + @argumentDefinitions(showDetailedResults: {type: "Boolean!", defaultValue: true}) { + name + is_completed + ... @include(if: $showDetailedResults) { + description + } + } + `, + props.task, + ); +} +``` + +```js +function TaskList(props) { + const data = usePreloadedQuery( + graphql` + query TaskListQuery { + todays_tasks { + ...TaskView_task + } + tomorrows_tasks { + ...TaskView_task @arguments(showDetailedResults: false) + } + } + `, + props.queryRef, + ); +} +``` + +* Not passing the argument to `TaskView_task` makes it use the default value for its locally declared `$showDetailedResult` variable. + + + +## Accessing GraphQL Variables At Runtime + + +If you want to access the variables that were set at the query root, the recommended approach is to pass the variables down the component tree in your application, using props, or your own application-specific context. + +Relay currently does not expose the resolved variables (i.e. after applying argument definitions) for a specific fragment, and you should very rarely need to do so. + + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/availability-of-data.md b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/availability-of-data.md new file mode 100644 index 0000000000000..81ef071f315ea --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/availability-of-data.md @@ -0,0 +1,18 @@ +--- +id: availability-of-data +title: Availability of Data +slug: /guided-tour/reusing-cached-data/availability-of-data/ +description: Relay guide to the availability of data +keywords: +- availability +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +The behavior of the fetch policies described in the [previous section](../fetch-policies/) will depend on the availability of the data in the Relay store at the moment we attempt to evaluate a query. + +There are two factors that determine the availability of data: the [presence of data](../presence-of-data/) and [staleness of data](../staleness-of-data/). + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/fetch-policies.md b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/fetch-policies.md new file mode 100644 index 0000000000000..cf5a31a4c22f6 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/fetch-policies.md @@ -0,0 +1,56 @@ +--- +id: fetch-policies +title: Fetch Policies +slug: /guided-tour/reusing-cached-data/fetch-policies/ +description: Relay guide to fetch policies +keywords: +- fetch policy +- network-only +- store-only +- store-and-network +- store-or-network +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +The first step to reusing locally cached data is to pass a `fetchPolicy` to the `loadQuery` function, which can be provided by `useQueryLoader` (see the [Fetching Queries section](../../rendering/queries/)): + +```js +const React = require('React'); +const {graphql} = require('react-relay'); + +function AppTabs() { + const [ + queryRef, + loadQuery, + ] = useQueryLoader(HomeTabQuery); + + const onSelectHomeTab = () => { + loadQuery({id: '4'}, {fetchPolicy: 'store-or-network'}); + } + + // ... +} +``` + +The provided `fetchPolicy` will determine: + +* *whether* the query should be fulfilled from the local cache, and +* *whether* a network request should be made to fetch the query from the server, depending on the [availability of the data for that query in the store](../availability-of-data/). + + +By default, Relay will try to read the query from the local cache; if any piece of data for that query is [missing](../presence-of-data/) or [stale](../staleness-of-data/), it will fetch the entire query from the network. This default `fetchPolicy` is called "*store-or-network".* + +Specifically, `fetchPolicy` can be any of the following options: ** + +* "store-or-network": *(default)* *will* reuse locally cached data, and will *only* send a network request if any data for the query is [missing](../presence-of-data/) or [stale](../staleness-of-data/). If the query is fully cached, a network request will *not* be made. +* "store-and-network": *will* reuse locally cached data and will *always* send a network request, regardless of whether any data was [missing](../presence-of-data/) or [stale](../staleness-of-data/) in the store. +* "network-only": *will* *not* reuse locally cached data, and will *always* send a network request to fetch the query, ignoring any data that might be locally cached and whether it's [missing](../presence-of-data/) or [stale](../staleness-of-data/). +* "store-only": *will* *only* reuse locally cached data, and will *never* send a network request to fetch the query. In this case, the responsibility of fetching the query falls to the caller, but this policy could also be used to read and operate on data that is entirely [local](../../updating-data/local-data-updates/). + + +Note that the `refetch` function discussed in the [Fetching and Rendering Different Data](../../refetching/) section also takes a `fetchPolicy`. + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/filling-in-missing-data.md b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/filling-in-missing-data.md new file mode 100644 index 0000000000000..ee183a527fe5e --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/filling-in-missing-data.md @@ -0,0 +1,102 @@ +--- +id: filling-in-missing-data +title: Filling in Missing Data (Missing Field Handlers) +slug: /guided-tour/reusing-cached-data/filling-in-missing-data/ +description: Relay guide to filling in missing data +keywords: +- missing field handler +- missing data +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +import FbMissingFieldHandlers from './fb/FbMissingFieldHandlers.md'; + +In the previous section we covered how to reuse data that is fully or partially cached, however there are cases in which Relay can't automatically tell that it can reuse some of the data it already has from other queries in order to fulfill a specific query. Specifically, Relay knows how to reuse data that is cached for a query that has been fetched before; that is, if you fetch the exact same query twice, Relay will know that it has the data cached for that query the second time it tries to evaluate it. + +However, when using different queries, there might still be cases where different queries point to the same data, which we'd want to be able to reuse. For example, imagine the following two queries: + +```js +// Query 1 + +query UserQuery { + user(id: 4) { + name + } +} +``` + +```js +// Query 2 + +query NodeQuery { + node(id: 4) { + ... on User { + name + } + } +} +``` + + +These two queries are different, but reference the exact same data. Ideally, if one of the queries was already cached in the store, we should be able to reuse that data when rendering the other query. However, Relay doesn't have this knowledge by default, so we need to configure it to encode the knowledge that a `node(id: 4)` *"is the same as"* `user(id: 4)`. + +To do so, we can provide `missingFieldHandlers` to the `RelayEnvironment` which specifies this knowledge. + + + +```js +const {ROOT_TYPE, Environment} = require('relay-runtime'); + +const missingFieldHandlers = [ + { + handle(field, record, argValues): ?string { + // Make sure to add a handler for the node field + if ( + record != null && + record.getType() === ROOT_TYPE && + field.name === 'node' && + argValues.hasOwnProperty('id') + ) { + return argValues.id + } + if ( + record != null && + record.getType() === ROOT_TYPE && + field.name === 'user' && + argValues.hasOwnProperty('id') + ) { + // If field is user(id: $id), look up the record by the value of $id + return argValues.id; + } + if ( + record != null && + record.getType() === ROOT_TYPE && + field.name === 'story' && + argValues.hasOwnProperty('story_id') + ) { + // If field is story(story_id: $story_id), look up the record by the + // value of $story_id. + return argValues.story_id; + } + return undefined; + }, + kind: 'linked', + }, +]; + +const environment = new Environment({/*...*/, missingFieldHandlers}); +``` + +* `missingFieldHandlers` is an array of *handlers*. Each handler must specify a `handle` function, and the kind of missing fields it knows how to handle. The 2 main types of fields that you'd want to handle are: + * *'scalar'*: This represents a field that contains a scalar value, for example a number or a string. + * *'linked'*: This represents a field that references another object, i.e. not a scalar. +* The `handle` function takes the field that is missing, the record that field belongs to, and any arguments that were passed to the field in the current execution of the query. + * When handling a *'scalar'* field, the handle function should return a scalar value, in order to use as the value for a missing field + * When handling a *'linked'* field*,* the handle function should return an *ID*, referencing another object in the store that should be use in place of the missing field. ** +* As Relay attempts to fulfill a query from the local cache, whenever it detects any missing data, it will run any of the provided missing field handlers that match the field type before definitively declaring that the data is missing. + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/introduction.md b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/introduction.md new file mode 100644 index 0000000000000..c4ea2384f6dbf --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/introduction.md @@ -0,0 +1,22 @@ +--- +id: introduction +title: Reusing Cached Data +slug: /guided-tour/reusing-cached-data/ +description: Relay guide to reusing cached data +keywords: +- reusing +- cached +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +While an app is in use, Relay will accumulate and cache *(for some time)* the data for the multiple queries that have been fetched throughout usage of our app. Often times, we'll want to be able to reuse and immediately render this data that is locally cached instead of waiting for a network request when fulfilling a query; this is what we'll cover in this section. + +Some examples of when this might be useful are: + +* Navigating between tabs in an app, where each app renders a query. If a tab has already been visited, re-visiting the tab should render it instantly, without having to wait for a network request to fetch the data that we've already fetched before. +* Navigating to a post that was previously rendered on a feed. If the post has already been rendered on a feed, navigating to the post's permalink page should render the post immediately, since all of the data for the post should already be cached. + * Even if rendering the post in the permalink page requires more data than rendering the post on a feed, we'd still like to reuse and immediately render as much of the post's data that we already have available locally, without blocking render for the entire post if only a small bit of data is missing. + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/presence-of-data.md b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/presence-of-data.md new file mode 100644 index 0000000000000..b057115fbfede --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/presence-of-data.md @@ -0,0 +1,93 @@ +--- +id: presence-of-data +title: Presence of Data +slug: /guided-tour/reusing-cached-data/presence-of-data/ +description: Relay guide to the presence of data +keywords: +- presence +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbGarbageCollection from './fb/FbGarbageCollection.md'; + + +An important thing to keep in mind when attempting to reuse data that is cached in the Relay store is to understand the lifetime of that data; that is, if it is present in the store, and for how long it will be. + +Data in the Relay store for a given query will generally be present after the query has been fetched for the first time, as long as that query is being rendered on the screen. If we've never fetched data for a specific query, then it will be missing from the store. + +However, even after we've fetched data for different queries, we can't keep all of the data that we've fetched indefinitely in memory, since over time it would grow to be too large and too stale. In order to mitigate this, Relay runs a process called *Garbage Collection*, in order to delete data that we're no longer using. + +## Garbage Collection in Relay + +Specifically, Relay runs garbage collection on the local in-memory store by deleting any data that is no longer being referenced by any component in the app. + +However, this can be at odds with reusing cached data; if the data is deleted too soon, before we try to reuse it again later, that will prevent us from reusing that data to render a screen without having to wait on a network request. To address this, this section will cover what you need to do in order to ensure that the data you want to reuse is kept cached for as long as you need it. + + +:::note +NOTE: Usually, you shouldn't need to worry about configuring garbage collection and data retention, as this should be configured by the app infrastructure at the RelayEnvironment level; however, we will cover it here for reference. +::: + + + + + +## Query Retention + +Retaining a query indicates to Relay that the data for that query and variables shouldn't be deleted (i.e. garbage collected). Multiple callers might retain a single query, and as long as there is at least one caller retaining a query, it won't be deleted from the store. + +By default, any query components using `useQueryLoader` / `usePreloadedQuery` or our other APIs will retain the query for as long as they are mounted. After they unmount, they will release the query, which means that the query might be deleted at any point in the future after that occurs. + +If you need to retain a specific query outside of the components lifecycle, you can use the [`retain`](../../accessing-data-without-react/retaining-queries/) operation: + +```js +// Retain query; this will prevent the data for this query and +// variables from being garbage collected by Relay +const disposable = environment.retain(queryDescriptor); + +// Disposing of the disposable will release the data for this query +// and variables, meaning that it can be deleted at any moment +// by Relay's garbage collection if it hasn't been retained elsewhere +disposable.dispose(); +``` + +* As mentioned, this will allow you to retain the query even after a query component has unmounted, allowing other components, or future instances of the same component, to reuse the retained data. + + +## Controlling Relay's Garbage Collection Policy + +There are currently 2 options you can provide to your Relay Store in to control the behavior of garbage collection: + +### GC Scheduler + +The `gcScheduler` is a function you can provide to the Relay Store which will determine when a GC execution should be scheduled to run: + +```js +// Sample scheduler function +// Accepts a callback and schedules it to run at some future time. +function gcScheduler(run: () => void) { + resolveImmediate(run); +} + +const store = new Store(source, {gcScheduler}); +``` + +* By default, if a `gcScheduler` option is not provided, Relay will schedule garbage collection using the `resolveImmediate` function. +* You can provide a scheduler function to make GC scheduling less aggressive than the default, for example based on time or [scheduler](https://github.com/facebook/react/tree/main/packages/scheduler) priorities, or any other heuristic. By convention, implementations should not execute the callback immediately. + + +### GC Release Buffer Size + +The Relay Store internally holds a release buffer to keep a specific (configurable) number of queries temporarily retained even *after* they have been released by their original owner (which will happen by default when a component rendering that query unmounts). This makes it possible (and more likely) to be able to reuse data when navigating back to a page, tab or piece of content that has been visited before. + +In order to configure the size of the release buffer, we can specify the `gcReleaseBufferSize` option to the Relay Store: + +```js +const store = new Store(source, {gcReleaseBufferSize: 10}); +``` + +* Note that having a buffer size of 0 is equivalent to not having the release buffer, which means that queries will be immediately released and collected. +* By default, environments have a release buffer size of 10. + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/rendering-partially-cached-data.md b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/rendering-partially-cached-data.md new file mode 100644 index 0000000000000..6028410b79f6b --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/rendering-partially-cached-data.md @@ -0,0 +1,175 @@ +--- +id: rendering-partially-cached-data +title: Rendering Partially Cached Data +slug: /guided-tour/reusing-cached-data/rendering-partially-cached-data/ +description: Relay guide to rendering partially cached data +keywords: +- partially cached data +- renderPolicy +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbProfilePhotoHeaderExample from './fb/FbProfileHeaderExample.md'; +import FbSuspensePlaceholder from '../../fb/FbSuspensePlaceholder.md'; + +When rendering cached data in Relay, it is possible to perform partial rendering. We define *"partial rendering"* as the ability to immediately render a query that is partially cached. That is, parts of the query might be missing, but parts of the query might already be cached. In these cases, we want to be able to immediately render the parts of the query that are cached, without waiting on the full query to be fetched. + +This can be useful in scenarios where we want to render a screen or a page as fast as possible, and we know that some of the data for that page is already cached so we can skip a loading state. For example, take the profile page: it is very likely that the user's name has already been cached at some point during usage of the app, so when visiting a profile page, if the name of the user is cached, we'd like to render immediately, even if the rest of the data for the profile page isn't available yet. + + +### Fragments as boundaries for partial rendering + +To do this, we rely on the ability of fragment components to *suspend* (see the [Loading States with Suspense](../../rendering/loading-states/) section). A fragment component will suspend if any of the data it declared locally is missing during render, and is currently being fetched. Specifically, it will suspend until the data it requires is fetched, that is, until the query to which it belongs (its *parent query*) is fetched. + +Let's explain what this means with an example. Say we have the following fragment component: + +```js +/** + * UsernameComponent.react.js + * + * Fragment Component + */ + +import type {UsernameComponent_user$key} from 'UsernameComponent_user.graphql'; + +const React = require('React'); +const {graphql, useFragment} = require('react-relay'); + +type Props = { + user: UsernameComponent_user$key, +}; + +function UsernameComponent(props: Props) { + const user = useFragment( + graphql` + fragment UsernameComponent_user on User { + username + } + `, + props.user, + ); + return (...); +} + +module.exports = UsernameComponent; +``` + + +And we have the following query component, which queries for some data, and also includes the fragment above: + +```javascript +/** + * AppTabs.react.js + * + * Query Loader Component + */ + + // .... + + const onSelectHomeTab = () => { + loadHomeTabQuery({id: '4'}, {fetchPolicy: 'store-or-network'}); + } + + // ... + +/** + * HomeTab.react.js + * + * Query Component + */ + +const React = require('React'); +const {graphql, usePreloadedQuery} = require('react-relay'); + +const UsernameComponent = require('./UsernameComponent.react'); + +function HomeTab(props: Props) { + const data = usePreloadedQuery( + graphql` + query HomeTabQuery($id: ID!) { + user(id: $id) { + name + ...UsernameComponent_user + } + } + `, + props.queryRef, + ); + + return ( + <> +

{data.user?.name}

+ + + ); +} +``` + + +Say that when this `HomeTab` component is rendered, we've already previously fetched *(_only_)* the `name` for the `User` with `{id: 4}`, and it is locally cached in the Relay store associated with our current Relay environment. + +If we attempt to render the query with a `fetchPolicy` that allows reusing locally cached data (`'store-or-network'`, or `'store-and-network'`), the following will occur: + +* The query will check if any of its locally-required data is missing. In this case, *it isn't*. Specifically, the query is only directly selecting the `name` field, and that field *is* available in the store. + * Relay considers data to be missing only if it is declared locally and missing. In other words, data that is selected within fragment spreads does not affect whether the outer query or fragment is determined to have missing data. +* Given that the query doesn't have any data missing, it will render, and then attempt to render the child `UsernameComponent`. +* When the `UsernameComponent` attempts to render the `UsernameComponent_user` fragment, Relay will notice that some of the data required to render is missing; specifically, the `username` is missing. At this point, since `UsernameComponent` has missing data, it will suspend rendering until the network request completes. Note that regardless of which `fetchPolicy` you choose, a network request will always be started if any piece of data for the full query, i.e. including fragments, is missing. + + +At this point, when `UsernameComponent` suspends due to the missing `username`, ideally we should still be able to render the `User`'s `name` immediately, since it's locally cached. However, since we aren't using a `Suspense` component to catch the fragment's suspension, the suspension will bubble up and the entire `App` component will be suspended. + +In order to achieve the desired effect of rendering the `name` when it's available even if the `username` is missing, we just need to wrap the `UsernameComponent` in `Suspense,` to *allow* the other parts of `App` to continue rendering: + +```js +/** + * HomeTab.react.js + * + * Query Component + */ + +const React = require('React'); +const {Suspense} = require('React'); +const {graphql, usePreloadedQuery} = require('react-relay'); + +const UsernameComponent = require('./UsernameComponent.react'); + + +function HomeTab() { + const data = usePreloadedQuery( + graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + ...UsernameComponent_user + } + } + `, + props.queryRef, + ); + + return ( + <> +

{data.user?.name}

+ + {/* + Wrap the UserComponent in Suspense to allow other parts of the + App to be rendered even if the username is missing. + */} + }> + + + + ); +} +``` + + + +The process that we described above works the same way for nested fragments (i.e. fragments included within other fragments). This means that if the data required to render a fragment is locally cached, the fragment component will be able to render, regardless of whether data for any of its child or descendant fragments is missing. If data for a child fragment is missing, we can wrap it in a `Suspense` component to allow other fragments and parts of the app to continue rendering. + +As mentioned in our motivating example, this is desirable because it can allows us to skip loading states entirely. More specifically, the ability to render data that is partially available allows us to render intermediate UI states that more closely resemble the final rendered state. + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/staleness-of-data.md b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/staleness-of-data.md new file mode 100644 index 0000000000000..639a696917510 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/reusing-cached-data/staleness-of-data.md @@ -0,0 +1,116 @@ +--- +id: staleness-of-data +title: Staleness of Data +slug: /guided-tour/reusing-cached-data/staleness-of-data/ +description: Relay guide to the staleness of data +keywords: +- staleness +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbPushViews from './fb/FbPushViews.md'; + +Assuming our data is [present in the store](../presence-of-data/), we still need to consider the staleness of such data. + +By default, Relay will not consider data in the store to be stale (regardless of how long it has been in the cache), unless it's explicitly marked as stale using our data invalidation APIs or if it is older than the query cache expiration time. + +Marking data as stale is useful for cases when we explicitly know that some data is no longer fresh (for example after executing a [Mutation](../../updating-data/graphql-mutations/)). + +Relay exposes the following APIs to mark data as stale within an update to the store: + +## Globally Invalidating the Relay Store + +The coarsest type of data invalidation we can perform is invalidating the *whole* store, meaning that all currently cached data will be considered stale after invalidation. + +To invalidate the store, we can call `invalidateStore()` within an [updater](../../updating-data/graphql-mutations/) function: + +```js +function updater(store) { + store.invalidateStore(); +} +``` + +* Calling `invalidateStore()` will cause *all* data that was written to the store before invalidation occurred to be considered stale, and will require any query to be refetched again the next time it's evaluated. +* Note that an updater function can be specified as part of a [mutation](../../updating-data/graphql-mutations/), [subscription](../../updating-data/graphql-subscriptions/) or just a [local store update](../../updating-data/local-data-updates/). + +## Invalidating Specific Data In The Store + +We can also be more granular about which data we invalidate and only invalidate *specific records* in the store; compared to global invalidation, only queries that reference the invalidated records will be considered stale after invalidation. + +To invalidate a record, we can call `invalidateRecord()` within an [updater](../../updating-data/graphql-mutations/) function: + +```js +function updater(store) { + const user = store.get(''); + if (user != null) { + user.invalidateRecord(); + } +} +``` + +* Calling `invalidateRecord()` on the `user` record will mark *that* specific user in the store as stale. That means that any query that is cached and references that invalidated user will now be considered stale, and will require to be refetched again the next time it's evaluated. +* Note that an updater function can be specified as part of a [mutation](../../updating-data/graphql-mutations/), [subscription](../../updating-data/graphql-subscriptions/) or just a [local store update](../../updating-data/local-data-updates/). + +## Subscribing to Data Invalidation + +Just marking the store or records as stale will cause queries to be refetched they next time they are evaluated; so for example, the next time you navigate back to a page that renders a stale query, the query will be refetched even if the data is cached, since the query references stale data. + +This is useful for a lot of use cases, but there are some times when we'd like to immediately refetch some data upon invalidation, for example: + +* When invalidating data that is already visible in the current page. Since no navigation is occurring, we won't re-evaluate the queries for the current page, so even if some data is stale, it won't be immediately refetched and we will be showing stale data. +* When invalidating data that is rendered on a previous view that was never unmounted; since the view wasn't unmounted, if we navigate back, the queries for that view won't be re-evaluated, meaning that even if some is stale, it won't be refetched and we will be showing stale data. + + + +To support these use cases, Relay exposes the `useSubscribeToInvalidationState` hook: + +```js +function ProfilePage(props) { + // Example of querying data for the current page for a given user + const data = usePreloadedQuery( + graphql`...`, + props.preloadedQuery, + ) + + // Here we subscribe to changes in invalidation state for the given user ID. + // Whenever the user with that ID is marked as stale, the provided callback will + // be executed + useSubscribeToInvalidationState([props.userID], () => { + // Here we can do things like: + // - re-evaluate the query by passing a new preloadedQuery to usePreloadedQuery. + // - imperatively refetch any data + // - render a loading spinner or gray out the page to indicate that refetch + // is happening. + }) + + return (...); +} +``` + +* `useSubscribeToInvalidationState` takes an array of ids, and a callback. Whenever any of the records for those ids are marked as stale, the provided callback will fire. +* Inside the callback, we can react accordingly and refetch and/or update any current views that are rendering stale data. As an example, we could re-execute the top-level `usePreloadedQuery` by keeping the `preloadedQuery` in state and setting a new one here; since that query is stale at that point, the query will be refetched even if the data is cached in the store. + + +## Query Cache Expiration Time + +In addition, the query cache expiration time affects whether certain operations (i.e. a query and variables) can be fulfilled with data that is already present in the store, i.e. whether the data for a query has become stale. + + A stale query is one which can be fulfilled with records from the store, and + +* the time since it was last fetched is greater than the query cache expiration time, or +* which contains at least one record that was invalidated. + +This staleness check occurs when a new request is made (e.g. in a call to `loadQuery`). Components which reference stale data will continue to be able to render that data; however, any additional requests which would be fulfilled using stale data will go to the network. + +In order to configure the query cache expiration time, we can specify the `queryCacheExpirationTime` option to the Relay Store: + +```js +const store = new Store(source, {queryCacheExpirationTime: 5 * 60 * 1000 }); +``` + +If the query cache expiration time is not provided, staleness checks only look at whether the referenced records have been invalidated. + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/client-only-data.md b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/client-only-data.md new file mode 100644 index 0000000000000..1c7f1c81195c4 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/client-only-data.md @@ -0,0 +1,115 @@ +--- +id: client-only-data +title: Client-only data +slug: /guided-tour/updating-data/client-only-data/ +description: Relay guide to client-only data +keywords: +- client-only +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbClientOnlyDataDir from './fb/FbClientOnlyDataDir.md'; + +## Client-Only Data (Client Schema Extensions) + +Relay provides the ability to extend the GraphQL schema *on the client* (i.e. in the browser), via client schema extensions, in order to model data that only needs to be created, read and updated on the client. This can be useful to add small pieces of information to data that is fetched from the server, or to entirely model client-specific state to be stored and managed by Relay. + +Client schema extensions allows you to modify existing types on the schema (e.g. by adding new fields to a type), or to create entirely new types that only exist in the client. + + +### Extending Existing Types + + + +In order to extend an existing type, add a `.graphql` file to the appropriate schema extension directory (depending on the repo): + + + + + +In order to extend an existing type, add a `.graphql` file to your appropriate source (`--src`) directory: + + + + +```graphql +extend type Comment { + is_new_comment: Boolean +} +``` + + + + + + + + + +* In this example, we're using the `extend` keyword to extend an existing type, and we're adding a new field, `is_new_comment` to the existing `Comment` type, which we will be able to [read](#reading-client-only-data) in our components, and [update](#updating-client-only-data) when necessary using normal Relay APIs; you might imagine that we might use this field to render a different visual treatment for a comment if it's new, and we might set it when creating a new comment. + + + +### Adding New Types + +You can define types using the same regular GraphQL syntax, by defining it inside a `.graphql` file in `html/js/relay/schema/`: + + +```graphql +# You can define more than one type in a single file +enum FetchStatus { + FETCHED + PENDING + ERRORED +} + + +type FetchState { + # You can reuse client types to define other types + status: FetchStatus + + # You can also reference regular server types + started_by: User! +} + +extend type Item { + # You can extend server types with client-only types + fetch_state: FetchState +} +``` + +* In this contrived example, we're defining 2 new client-only types, and `enum` and a regular `type`. Note that they can reference themselves as normal, and reference regular server defined types. Also note that we can extend server types and add fields that are of our client-only types. +* As mentioned previously, we will be able to [read](#reading-client-only-data) and [update](#updating-client-only-data) this data normally via Relay APIs. + + + +### Reading Client-Only Data + +We can read client-only data be selecting it inside [fragments](../../rendering/fragments/) or [queries](../../rendering/queries/) as normal: + +```js +const data = *useFragment*( + graphql` + fragment CommentComponent_comment on Comment { + + # We can select client-only fields as we would any other field + is_new_comment + + body { + text + } + } + `, + props.user, +); +``` + + + +### Updating Client-Only Data + +In order to update client-only data, you can do so regularly inside [mutation](../graphql-mutations/) or [subscription](../graphql-subscriptions/) updaters, or by using our primitives for doing [local updates](../local-data-updates/) to the store. + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/graphql-mutations.md b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/graphql-mutations.md new file mode 100644 index 0000000000000..b4b3536f608cf --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/graphql-mutations.md @@ -0,0 +1,378 @@ +--- +id: graphql-mutations +title: GraphQL mutations +slug: /guided-tour/updating-data/graphql-mutations/ +description: Relay guide to GraphQL mutations +keywords: +- mutation +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +In GraphQL, data on the server is updated using [GraphQL mutations](https://graphql.org/learn/queries/#mutations). Mutations are read-write server operations, which both modify the data on the backend and allow you to query the modified data in the same request. + +## Writing Mutations + +A GraphQL mutation looks very similar to a query, except that it uses the `mutation` keyword: + +```graphql +mutation FeedbackLikeMutation($input: FeedbackLikeData!) { + feedback_like(data: $input) { + feedback { + id + viewer_does_like + like_count + } + } +} +``` + +* The mutation above modifies the server data to "like" the specified `Feedback` object. +* `feedback_like` is a *mutation root field* (or just *mutation field*) which updates data on the backend. + + + +:::info +You can view mutation root fields in the GraphQL Schema Explorer by opening VSCode @ FB and executing the command "Relay: Open GraphQL Schema Explorer". Then, in the "Schema Explorer Tab", click on "Mutation". + +You can click on the various mutation fields to see their parameters, descriptions and exposed fields. +::: + + + +* A mutation is handled in two separate steps: first, the update is processed on the server, and then the query is executed. This ensures that you only see data that has already been updated as part of your mutation response. + +:::note +Note that queries are processed in the same way. Outer selections are calculated before inner selections. It is simply a matter of convention that top-level mutation fields have side-effects, while other fields tend not to. +::: + +* The mutation field (in this case, `feedback_like`) returns a specific GraphQL type which exposes the data for which we can query in the mutation response. + + + +* [It is a best practice](https://fb.workplace.com/groups/644933736023601/?multi_permalinks=823422684841371) to include the `viewer` object and all updated Ents as part of the mutation response. + + + +* In this case, we're querying for the *updated* feedback object, including the updated `like_count` and the updated value for `viewer_does_like`, indicating whether the current viewer likes the feedback object. + + + +* Check out the [Hack documentation on writing mutations](https://www.internalfb.com/intern/wiki/Graphql-for-hack-developers/mutation-root-fields/) for information on how to add a mutation field to your backend code. + + + +An example of a successful response for the above mutation could look like this: + +```json +{ + "feedback_like": { + "feedback": { + "id": "feedback-id", + "viewer_does_like": true, + "like_count": 1, + } + } +} +``` + +In Relay, we can declare GraphQL mutations using the `graphql` tag too: + +```js +const {graphql} = require('react-relay'); + +const feedbackLikeMutation = graphql` + mutation FeedbackLikeMutation($input: FeedbackLikeData!) { + feedback_like(data: $input) { + feedback { + id + viewer_does_like + like_count + } + } + } +`; +``` + +* Note that mutations can also reference GraphQL [variables](../../rendering/variables/) in the same way queries or fragments do. + +## Using `useMutation` to execute a mutation + +In order to execute a mutation against the server in Relay, we can use the `commitMutation` and [useMutation](../../../api-reference/use-mutation) APIs. Let's take a look at an example using the `useMutation` API: + +```js +import type {FeedbackLikeData, LikeButtonMutation} from 'LikeButtonMutation.graphql'; + +const {useMutation, graphql} = require('react-relay'); + +function LikeButton({ + feedbackId: string, +}) { + const [commitMutation, isMutationInFlight] = useMutation( + graphql` + mutation LikeButtonMutation($input: FeedbackLikeData!) { + feedback_like(data: $input) { + feedback { + viewer_does_like + like_count + } + } + } + ` + ); + + return +} +``` + +Let's distill what's happening here. + +* `useMutation` takes a graphql literal containing a mutation as its only argument. +* It returns a tuple of items: + * a callback (which we call `commitMutation`) which accepts a `UseMutationConfig`, and + * a boolean indicating whether a mutation is in flight. +* In addition, `useMutation` accepts a Flow type parameter. As with queries, the Flow type of the mutation is exported from the file that the Relay compiler generates. + * If this type is provided, the `UseMutationConfig` becomes statically typed as well. **It is a best practice to always provide this type.** +* Now, when `commitMutation` is called with the mutation variables, Relay will make a network request that executes the `feedback_like` field on the server. In this example, this would find the feedback specified by the variables, and record on the backend that the user liked that piece of feedback. +* Once that field is executed, the backend will select the updated Feedback object and select the `viewer_does_like` and `like_count` fields off of it. + * Since the `Feedback` type contains an `id` field, the Relay compiler will automatically add a selection for the `id` field. +* When the mutation response is received, Relay will find a feedback object in the store with a matching `id` and update it with the newly received `viewer_does_like` and `like_count` values. +* If these values have changed as a result, any components which selected these fields off of the feedback object will be re-rendered. Or, to put it colloquially, any component which depends on the updated data will re-render. + +:::note +The name of the type of the parameter `FeedbackLikeData` is derived from the name of the top-level mutation field, i.e. from `feedback_like`. This type is also exported from the generated `graphql.js` file. +::: + +## Refreshing components in response to mutations + +In the previous example, we manually selected `viewer_does_like` and `like_count`. Components that select these fields will be re-rendered, should the value of those fields change. + +However, it is generally better to spread fragments that correspond to components that we want to refresh in response to the mutation. This is because the data selected by components can change. + +Requiring developers to know about all mutations that might affect their components' data (and keeping them up-to-date) is an example of the kind of global reasoning that Relay wants to avoid requiring. + +For example, we might rewrite the mutation as follows: + +```graphql +mutation FeedbackLikeMutation($input: FeedbackLikeData!) { + feedback_like(data: $input) { + feedback { + ...FeedbackDisplay_feedback + ...FeedbackDetail_feedback + } + } +} +``` + +If this mutation is executed, then whatever fields were selected by the `FeedbackDisplay` and `FeedbackDetail` components will be refetched, and those components will remain in a consistent state. + +:::note +Spreading fragments is generally preferable to refetching the data after a mutation has completed, since the updated data can be fetched in a single round trip. +::: + +## Executing a callback when the mutation completes or errors + +We may want to update some state in response to the mutation succeeding or failing. For example, we might want to alert the user if the mutation failed. The `UseMutationConfig` object can include the following fields to handle such cases: + +* `onCompleted`, a callback that is executed when the mutation completes. It is passed the mutation response (stopping at fragment spread boundaries). + * The value passed to `onCompleted` is the the mutation fragment, as read out from the store, **after** updaters and declarative mutation directives are applied. This means that data from within unmasked fragments will not be read, and records that were deleted (e.g. by `@deleteRecord`) may also be null. +* `onError`, a callback that is executed when the mutation errors. It is passed the error that occurred. + +## Declarative mutation directives + +### Manipulating connections in response to mutations + +Relay makes it easy to respond to mutations by adding items to or removing items from connections (i.e. lists). For example, you might want to append a newly created user to a given connection. For more, see [Using declarative directives](../../list-data/updating-connections/#using-declarative-directives). + +### Deleting items in response to mutations + +In addition, you might want to delete an item from the store in response to a mutation. In order to do this, you would add the `@deleteRecord` directive to the deleted ID. For example: + +```graphql +mutation DeletePostMutation($input: DeletePostData!) { + delete_post(data: $input) { + deleted_post { + id @deleteRecord + } + } +} +``` + +## Imperatively modifying local data + +At times, the updates you wish to perform are more complex than just updating the values of fields and cannot be handled by the declarative mutation directives. For such situations, the `UseMutationConfig` accepts an `updater` function which gives you full control over how to update the store. + +This is discussed in more detail in the section on [Imperatively modifying store data](../imperatively-modifying-store-data/). + +## Optimistic updates + +Oftentimes, we don't want to wait for the server to respond before we respond to the user interaction. For example, if a user clicks the "Like" button, we would like to instantly show the affected comment, post, etc. has been liked by the user. + +More generally, in these cases, we want to immediately update the data in our store optimistically, i.e. under the assumption that the mutation will complete successfully. If the mutation ends up not succeeding, we would like to roll back that optimistic update. + +### Optimistic response + +In order to enable this, the `UseMutationConfig` can include an `optimisticResponse` field. + +For this field to be Flow-typed, the call to `useMutation` must be passed a Flow type parameter **and** the mutation must be decorated with a `@raw_response_type` directive. + +In the previous example, we might provide the following optimistic response: + +```js +{ + feedback_like: { + feedback: { + // Even though the id field is not explicitly selected, the + // compiler selected it for us + id: feedbackId, + viewer_does_like: true, + }, + }, +} +``` + +Now, when we call `commitMutation`, this data will be immediately written into the store. The item in the store with the matching id will be updated with a new value of `viewer_does_like`. Any components which have selected this field will be re-rendered. + +When the mutation succeeds or errors, the optimistic response will be rolled back. + +Updating the `like_count` field takes a bit more work. In order to update it, we should also read the **current like count** in the component. + +```js +import type {FeedbackLikeData, LikeButtonMutation} from 'LikeButtonMutation.graphql'; +import type {LikeButton_feedback$fragmentType} from 'LikeButton_feedback.graphql'; + +const {useMutation, graphql} = require('react-relay'); + +function LikeButton({ + feedback: LikeButton_feedback$fragmentType, +}) { + const data = useFragment( + graphql` + fragment LikeButton_feedback on Feedback { + __id + viewer_does_like @required(action: THROW) + like_count @required(action: THROW) + } + `, + feedback + ); + + const [commitMutation, isMutationInFlight] = useMutation( + graphql` + mutation LikeButtonMutation($input: FeedbackLikeData!) + @raw_response_type { + feedback_like(data: $input) { + feedback { + viewer_does_like + like_count + } + } + } + ` + ); + + const changeToLikeCount = data.viewer_does_like ? -1 : 1; + return +} +``` + +:::caution + +You should be careful, and consider using [optimistic updaters](../imperatively-modifying-store-data/#example) if the value of the optimistic response depends on the value of the store and if there can be multiple optimistic responses affecting that store value. + +For example, if **two** optimistic responses each increase the like count by one, and the **first** optimistic updater is rolled back, the second optimistic update will still be applied, and the like count in the store will remain increased by two. + +::: + +:::caution + +Optimistic responses contain **many pitfalls!** + +* An optimistic response can contain the data for the full query response, i.e. including the content of fragment spreads. This means that if a developer selects more fields in components whose fragments are spread in an optimistic response, these components may have inconsistent or partial data during an optimistic update. +* Because the type of the optimistic update includes the contents of all recursively nested fragments, it can be very large. Adding `@raw_response_type` to certain mutations can degrade the performance of the Relay compiler. + +::: + +### Optimistic updaters + +Optimistic responses aren't enough for every case. For example, we may want to optimistically update data that we aren't selecting in the mutation. Or, we may want to add or remove items from a connection (and the declarative mutation directives are insufficient for our use case.) + +For situations like these, the `UseMutationConfig` can contain an `optimisticUpdater` field, which allows developers to imperatively and optimistically update the data in the store. This is discussed in more detail in the section on [Imperatively updating store data](../imperatively-modifying-store-data/). + +## Order of execution of updater functions + +In general, execution of the `updater` and optimistic updates will occur in the following order: + +* If an `optimisticResponse` is provided, that data will be written into the store. +* If an `optimisticUpdater` is provided, Relay will execute it and update the store accordingly. +* If an `optimisticResponse` was provided, the declarative mutation directives present in the mutation will be processed on the optimistic response. +* If the mutation request succeeds: + * Any optimistic update that was applied will be rolled back. + * Relay will write the server response to the store. + * If an `updater` was provided, Relay will execute it and update the store accordingly. The server payload will be available to the `updater` as a root field in the store. + * Relay will process any declarative mutation directives using the server response. + * The `onCompleted` callback will be called. +* If the mutation request fails: + * Any optimistic update was applied will be rolled back. + * The `onError` callback will be called. + +## Invalidating data during a mutation + +The recommended approach when executing a mutation is to request *all* the relevant data that was affected by the mutation back from the server (as part of the mutation body), so that our local Relay store is consistent with the state of the server. + +However, often times it can be unfeasible to know and specify all the possible data the possible data that would be affected for mutations that have large rippling effects (e.g. imagine "blocking a user" or "leaving a group"). + +For these types of mutations, it's often more straightforward to explicitly mark some data as stale (or the whole store), so that Relay knows to refetch it the next time it is rendered. In order to do so, you can use the data invalidation APIs documented in our [Staleness of Data section](../../reusing-cached-data/staleness-of-data/). + + + +## Handling errors + +GraphQL errors can largely be differentiated as: + +1. Operation (query/mutation/subscription) level errors, and +2. Field level errors + +### Surfacing mutation level errors + +If you're surfacing an error in the mutation (eg the server rejects the entire mutation because it's invalid), as long as the error returned is considered a [`CRITICAL`](https://www.internalfb.com/code/www/[b5a08782893a]/flib/graphql/experimental/core/error/GraphQL2ErrorSeverity.php?lines=11) error, you can make use of the `onError` callback from `useMutation` to handle that error in whatever way you see fit for your use case. + +If you control the server resolver, the question you should ask is whether or not throwing a CRITICAL error is the correct behavior for the client. Note though that throwing a CRITICAL error means that Relay will no longer process the interaction, which may not always be what you want if you can still partially update your UI. For example, it's possible that the mutation errored, but still wrote some data to the database, in which case you might still want Relay to process the updated fields. + +In the non-CRITICAL case the mutation may have failed, but some data was successfully returned in the case of partial data and/or the error response if encoded in the schema. Relay will still process this data, update its store, as well as components relying on that data. That is not true for the case where you've returned a CRITICAL error. + +### Surfacing field level errors +Field level errors from the server are generally recommended to be at the [`ERROR`](https://www.internalfb.com/code/www/[9120ab8aa8a5]/flib/graphql/experimental/core/error/GraphQL2ErrorSeverity.php?lines=17) level, because your UI should still be able to process the other fields that were successfully returned. If you want to explicitly handle the field level error, then we still recommend [modeling that](../../rendering/error-states/#accessing-errors-in-graphql-responses) in your schema. + + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/graphql-subscriptions.md b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/graphql-subscriptions.md new file mode 100644 index 0000000000000..8e36ffc74c9ff --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/graphql-subscriptions.md @@ -0,0 +1,279 @@ +--- +id: graphql-subscriptions +title: GraphQL subscriptions +slug: /guided-tour/updating-data/graphql-subscriptions/ +description: Relay guide to GraphQL subscriptions +keywords: +- subscription +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + + + +[GraphQL subscriptions](https://our.internmc.facebook.com/intern/wiki/GraphQL_Subscriptions/) are a mechanism to allow clients to query for data in response to a stream of server-side events. + + + + + +GraphQL subscriptions are a mechanism to allow clients to query for data in response to a stream of server-side events. + + + +A GraphQL subscription looks very similar to a query, except that it uses the `subscription` keyword: + +```graphql +subscription FeedbackLikeSubscription($input: FeedbackLikeSubscribeData!) { + feedback_like_subscribe(data: $input) { + feedback { + like_count + } + } +} +``` + +* Establishing a subscription using this GraphQL snippet will cause the application to be notified whenever an event is emitted from the `feedback_like_subscribe` stream. +* `feedback_like_subscribe` is a *subscription root field* (or just *subscription field*), which sets up the subscription on the backend. + + + +:::info +You can view subscription root fields in the GraphQL Schema Explorer by opening VSCode @ FB and executing the command "Relay: Open GraphQL Schema Explorer". Then, in the "Schema Explorer Tab", click on "Subscription". + +You can click on the various mutation fields to see their parameters, descriptions and exposed fields. +::: + + + +* Like mutations, a subscription is handled in two separate steps. First, a server-side event occurs. Then, the query is executed. + +:::note +Note that the event stream can be completely arbitrary, and can have no relation to the fields selected. In other words, there is no guarantee that the values selected in a subscription will have changed from notification to notification. +::: + +* `feedback_like_subscribe` returns a specific GraphQL type which exposes the data we can query in response to the server-side event. In this case, we're querying for the Feedback object and its updated `like_count`. This allows us to show the like count in real time. + +An example of a subscription payload received by the client could look like this: + +```json +{ + "feedback_like_subscribe": { + "feedback": { + "id": "feedback-id", + "like_count": 321, + } + } +} +``` + +In Relay, we can declare GraphQL subscriptions using the `graphql` tag too: + +```js +const {graphql} = require('react-relay'); + +const feedbackLikeSubscription = graphql` + subscription FeedbackLikeSubscription($input: FeedbackLikeSubscribeData!) { + feedback_like_subscribe(data: $input) { + feedback { + like_count + } + } + } +`; +``` + +* Note that subscriptions can also reference GraphQL [variables](../../rendering/variables/) in the same way queries or fragments do. + +## Using `useSubscription` to create a subscription + +In order to create a subscription in Relay, we can use the `useSubscription` and `requestSubscription` APIs. Let's take a look at an example using the `useSubscription` API: + +```js +import type {Environment} from 'react-relay'; +import type {FeedbackLikeSubscribeData} from 'FeedbackLikeSubscription.graphql'; + +const {graphql, useSubscription} = require('react-relay'); +const {useMemo} = require('React'); + +function useFeedbackSubscription( + input: FeedbackLikeSubscribeData, +) { + const config = useMemo(() => ({ + subscription: graphql` + subscription FeedbackLikeSubscription( + $input: FeedbackLikeSubscribeData! + ) { + feedback_like_subscribe(data: $input) { + feedback { + like_count + } + } + } + `, + variables: {input}, + }), [input]); + + return useSubscription(config); +} +``` + +Let's distill what's happening here. + +* `useSubscription` takes a `GraphlQLSubscriptionConfig` object, which includes the following fields: + * `subscription`: the GraphQL literal containing the subscription, and + * `variables`: the variables with which to establish the subscription. +* In addition, `useSubscription` accepts a Flow type parameter. As with queries, the Flow type of the subscription is exported from the file that the Relay compiler generates. + * If this type is provided, the `GraphQLSubscriptionConfig` becomes statically typed as well. **It is a best practice to always provide this type.** +* Now, when the `useFeedbackSubscription` hook commits, Relay will establish a subscription. + * Unlike with APIs like `useLazyLoadQuery`, Relay will **not** attempt to establish this subscription during the render phase. +* Once it is established, whenever an event occurs, the backend will select the updated Feedback object and select the `like_count` fields off of it. + * Since the `Feedback` type contains an `id` field, the Relay compiler will automatically add a selection for the `id` field. +* When the subscription response is received, Relay will find a feedback object in the store with a matching `id` and update it with the newly received `like_count` value. +* If these values have changed as a result, any components which selected these fields off of the feedback object will be re-rendered. Or, to put it colloquially, any component which depends on the updated data will re-render. + +:::note +The name of the type of the parameter `FeedbackLikeSubscribeData` is derived from the name of the top-level mutation field, i.e. from `feedback_like_subscribe`. This type is also exported from the generated `graphql.js` file. +::: + +:::caution + +The `GraphQLSubscriptionConfig` object passed to `useSubscription` should be memoized! Otherwise, `useSubscription` will dispose the subscription and re-establish it with every render! + +::: + +## Refreshing components in response to subscription events + +In the previous example, we manually selected `like_count`. Components that select this field will be re-rendered, should we receive an updated value. + +However, it is generally better to spread fragments that correspond to the components that we want to refresh in response to the mutation. This is because the data selected by components can change. + +Requiring developers to know about all subscriptions that might fetch their components data (and keeping them up-to-date) is an example of the kind of global reasoning that Relay wants to avoid requiring. + +For example, we might rewrite the subscription as follows: + +```graphql +subscription FeedbackLikeSubscription($input: FeedbackLikeSubscribeData!) { + feedback_like_subscribe(data: $input) { + feedback { + ...FeedbackDisplay_feedback + ...FeedbackDetail_feedback + } + } +} +``` + +Now, whenever a event in the `feedback_like_subscribe` event stream occurred, the data selected by the `FeedbackDisplay` and `FeedbackDetail` components will be refetched, and those components will remain in a consistent state. + +:::note +Spreading fragments is generally preferable to refetching the data in response to subscription events, since the updated data can be fetched in a single round trip. +::: + +## Executing a callback when the subscription fires, errors or is closed by the server + +In addition to writing updated data to the Relay store, we may want to execute a callback whenever a subscription payload is received. We may want to execute a callback if an error is received or if an error is received or if the server ends the subscription. The `GraphQLSubscriptionConfig` can include the following fields to handle such cases: + +* `onNext`, a callback that is executed when a subscription payload is received. It is passed the subscription response (stopping at fragment spread boundaries). +* `onError`, a callback that is executed when the subscription errors. It is passed the error that occured. +* `onCompleted`, a callback that is executed when the server ends the subscription. + +## Declarative mutation directives + +[Declarative mutation directives](../../list-data/updating-connections/#using-declarative-directives) and [`@deleteRecord`](../graphql-mutations/#deleting-items-in-response-to-mutations) work in subscriptions, too. + +### Manipulating connections in response to subscription events + +Relay makes it easy to respond to subscription events by adding items to or removing items from connections (i.e. lists). For example, you might want to append a newly created user to a given connection. For more, see [Using declarative directives](../../list-data/updating-connections/#using-declarative-directives). + +### Deleting items in response to mutations + +In addition, you might want to delete an item from the store in response to a mutation. In order to do this, you would add the `@deleteRecord` directive to the deleted ID. For example: + +```graphql +subscription DeletePostSubscription($input: DeletePostSubscribeData!) { + delete_post_subscribe(data: $input) { + deleted_post { + id @deleteRecord + } + } +} +``` + +## Imperatively modifying local data + +At times, the updates you wish to perform are more complex than just updating the values of fields and cannot be handled by the declarative mutation directives. For such situations, the `GraphQLSubscriptionConfig` accepts an `updater` function which gives you full control over how to update the store. + +This is discussed in more detail in the section on [Imperatively updating store data](../imperatively-modifying-store-data/). + +## Configuring the Network Layer + + + +You will need to Configure your [Network layer](../../../guides/network-layer) to handle subscriptions. + +Usually GraphQL subscriptions are communicated over [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API), here's an example using [graphql-ws](https://github.com/enisdenjo/graphql-ws): + +```javascript +import { + ... + Network, + Observable +} from 'relay-runtime'; +import { createClient } from 'graphql-ws'; + +const wsClient = createClient({ + url:'ws://localhost:3000', +}); + +const subscribe = (operation, variables) => { + return Observable.create((sink) => { + return wsClient.subscribe( + { + operationName: operation.name, + query: operation.text, + variables, + }, + sink, + ); + }); +} + +const network = Network.create(fetchQuery, subscribe); +``` + +Alternatively, the legacy [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) library can be used too: + +```javascript +import { + ... + Network, + Observable +} from 'relay-runtime'; +import { SubscriptionClient } from 'subscriptions-transport-ws'; + +const subscriptionClient = new SubscriptionClient('ws://localhost:3000', { + reconnect: true, +}); + +const subscribe = (request, variables) => { + const subscribeObservable = subscriptionClient.request({ + query: request.text, + operationName: request.name, + variables, + }); + // Important: Convert subscriptions-transport-ws observable type to Relay's + return Observable.from(subscribeObservable); +}; + +const network = Network.create(fetchQuery, subscribe); +``` + + + + +At Facebook, the Network Layer has already been configured to handle GraphQL Subscriptions. For more details on writing subscriptions at Facebook, check out this [guide](../../../guides/writing-subscriptions/). For a guide on setting up subscriptions on the server side, check out this [wiki](https://our.internmc.facebook.com/intern/wiki/GraphQL_Subscriptions/creating-a-new-subscription/). + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-linked-fields.md b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-linked-fields.md new file mode 100644 index 0000000000000..8ca9e7643d250 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-linked-fields.md @@ -0,0 +1,521 @@ +--- +id: imperatively-modifying-linked-fields +title: Imperatively modifying linked fields +slug: /guided-tour/updating-data/imperatively-modifying-linked-fields/ +description: Using readUpdatableQuery to update linked fields in the store +keywords: +- record source +- store +- updater +- typesafe updaters +- readUpdatableQuery +- readUpdatableFragment +- updatable +- assignable +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + + + +:::caution + +Because in TypeScript, [getters and setters cannot have different types](https://github.com/microsoft/TypeScript/issues/43662), and the generated types of getters and setters is not the same, `readUpdatableQuery` is currently unusable with TypeScript. `readUpdatableFragment` is usable, as long as the updatable fragment contains only scalar fields. + +::: + + + +:::note +See also [using readUpdatableQuery to update scalar fields in the store](../imperatively-modifying-store-data). +::: + + +The examples in the [previous section](../imperatively-modifying-store-data/) showed how to use the `readUpdatableQuery` API to update scalar fields like `is_new_comment` and `is_selected`. + +The examples did **not** cover how to assign to linked fields. Let's start with an example of a component which allows the user of the application to update the Viewer's `best_friend` field. + +## Example: setting the viewer's best friend + +In order to assign a viewer's best friend, that viewer must have such a field. It may be defined by the server schema, or it may be defined locally in a schema extension as follows: + +```graphql +extend type Viewer { + best_friend: User, +} +``` + +Next, let's define a fragment and give it the `@assignable` directive, making it an **assignable fragment**. Assignable fragments can only contain a single field, `__typename`. This fragment will be on the `User` type, which is the type of the `best_friend` field. + +```js +// AssignBestFriendButton.react.js +graphql` + fragment AssignBestFriendButton_assignable_user on User @assignable { + __typename + } +`; +``` + +The fragment must be spread at both the source (i.e. on the viewer's new best friend), and at the destination (within the viewer's `best_friend` field in the updatable query). + +Lets define a component with a fragment where we spread `AssignableBestFriendButton_assignable_user`. This user will be the viewer's new best friend. + +```js +// AssignBestFriendButton.react.js +import type {AssignBestFriendButton_user$key} from 'AssignBestFriendButton_user.graphql'; + +const {useFragment} = require('react-relay'); + +export default function AssignBestFriendButton({ + someTypeRef: AssignBestFriendButton_user$key, +}) { + const data = useFragment(graphql` + fragment AssignBestFriendButton_someType on SomeType { + user { + name + ...AssignableBestFriendButton_assignable_user + } + } + `, someTypeRef); + + // We will replace this stub with the real thing below. + const onClick = () => {}; + + return (); +} +``` + +That's great! Now, we have a component that renders a button. Let's fill out that button's click handler by using the `commitLocalUpdate` and `readUpdatableQuery` APIs to assign `viewer.best_friend`. + +* In order to make it valid to assign `data.user` to `best_friend`, we must **also** spread `AssignBestFriendButton_assignable_user` under the `best_friend` field in the viewer in the updatable query or fragment. + +```js +import type {RecordSourceSelectorProxy} from 'react-relay'; + +const {commitLocalUpdate, useRelayEnvironment} = require('react-relay'); + +// ... + +const environment = useRelayEnvironment(); +const onClick = () => { + const updatableData = commitLocalUpdate( + environment, + (store: RecordSourceSelectorProxy) => { + const {updatableData} = store.readUpdatableQuery( + graphql` + query AssignBestFriendButtonUpdatableQuery + @updatable { + viewer { + best_friend { + ...AssignableBestFriendButton_assignable_user + } + } + } + `, + {} + ); + + if (data.user != null && updatableData.viewer != null) { + updatableData.viewer.best_friend = data.user; + } + } + ); +}; +``` + +### Putting it all together + +The full example is as follows: + +```graphql +extend type Viewer { + best_friend: User, +} +``` + +```js +// AssignBestFriendButton.react.js +import type {AssignBestFriendButton_user$key} from 'AssignBestFriendButton_user.graphql'; +import type {RecordSourceSelectorProxy} from 'react-relay'; + +const {commitLocalUpdate, useFragment, useRelayEnvironment} = require('react-relay'); + +graphql` + fragment AssignBestFriendButton_assignable_user on User @assignable { + __typename + } +`; + +export default function AssignBestFriendButton({ + someTypeRef: AssignBestFriendButton_someType$key, +}) { + const data = useFragment(graphql` + fragment AssignBestFriendButton_someType on SomeType { + user { + name + ...AssignableBestFriendButton_assignable_user + } + } + `, someTypeRef); + + const environment = useRelayEnvironment(); + const onClick = () => { + const updatableData = commitLocalUpdate( + environment, + (store: RecordSourceSelectorProxy) => { + const {updatableData} = store.readUpdatableQuery( + graphql` + query AssignBestFriendButtonUpdatableQuery + @updatable { + viewer { + best_friend { + ...AssignableBestFriendButton_assignable_user + } + } + } + `, + {} + ); + + if (data.user != null && updatableData.viewer != null) { + updatableData.viewer.best_friend = data.user; + } + } + ); + }; + + return (); +} +``` + +Let's recap what is happening here. + +* We are writing a component in which clicking a button results in a user is being assigned to `viewer.best_friend`. After this button is clicked, all components which were previously reading the `viewer.best_friend` field will be re-rendered, if necessary. +* The source of the assignment is a user where an **assignable fragment** is spread. +* The target of the assignment is accessed using the `commitLocalUpdate` and `readUpdatableQuery` APIs. +* The query passed to `readUpdatableQuery` must include the `@updatable` directive. +* The target field must have that same **assignable fragment** spread. +* We are checking whether `data.user` is not null before assigning. This isn't strictly necessary. However, if we assign `updatableData.viewer.best_friend = null`, we will be nulling out the linked field in the store! This is (probably) not what you want. + +## Pitfalls + +* Note that there are no guarantees about what fields are present on the assigned user. This means that any consumes an updated field has no guarantee that the required fields were fetched and are present on the assigned object. + + + +:::note + +It is technically feasible to add fields to the assignable fragment, which would have the effect of guaranteeing that certain fields are present in the assigned object. + +If this is a need, please reach out to [Relay Support](https://fb.workplace.com/groups/relay.support). + +::: + + + +## Example: Assigning to a list + +Let's modify the previous example to append the user to a list of best friends. In this example, the following principle is relevant: + +> Every assigned linked field (i.e. the right hand side of the assignment) **must originate in a read-only fragment, query, mutation or subscription**. + +This means that `updatableData.foo = updatableData.foo` is invalid. For the same reason, `updatableData.viewer.best_friends = updatableData.viewer.best_friends.concat([newBestFriend])` is invalid. To work around this restriction, we must select the existing best friends from a read-only fragment, and perform the assignment as follows: `viewer.best_friends = existing_list.concat([newBestFriend])`. + +Consider the following full example: + +```graphql +extend type Viewer { + # We are now defined a "best_friends" field instead of a "best_friend" field + best_friends: [User!], +} +``` + +```js +// AssignBestFriendButton.react.js +import type {AssignBestFriendButton_user$key} from 'AssignBestFriendButton_user.graphql'; +import type {AssignBestFriendButton_viewer$key} from 'AssignBestFriendButton_viewer'; + +import type {RecordSourceSelectorProxy} from 'react-relay'; + +const {commitLocalUpdate, useFragment, useRelayEnvironment} = require('react-relay'); + +graphql` + fragment AssignBestFriendButton_assignable_user on User @assignable { + __typename + } +`; + +export default function AssignBestFriendButton({ + someTypeRef: AssignBestFriendButton_someType$key, + viewerFragmentRef: AssignBestFriendButton_viewer$key, +}) { + const data = useFragment(graphql` + fragment AssignBestFriendButton_someType on SomeType { + user { + name + ...AssignableBestFriendButton_assignable_user + } + } + `, someTypeRef); + + const viewer = useFragment(graphql` + fragment AssignBestFriendButton_viewer on Viewer { + best_friends { + # since viewer.best_friends appears in the right hand side of the assignment + # (i.e. updatableData.viewer.best_friends = viewer.best_friends.concat(...)), + # the best_friends field must contain the correct assignable fragment spread + ...AssignableBestFriendButton_assignable_user + } + } + `, viewerRef); + + const environment = useRelayEnvironment(); + const onClick = () => { + commitLocalUpdate( + environment, + (store: RecordSourceSelectorProxy) => { + const {updatableData} = store.readUpdatableQuery( + graphql` + query AssignBestFriendButtonUpdatableQuery + @updatable { + viewer { + best_friends { + ...AssignableBestFriendButton_assignable_user + } + } + } + `, + {} + ); + + if (data.user != null && updatableData.viewer != null && viewer.best_friends != null) { + updatableData.viewer.best_friends = [ + ...viewer.best_friends, + data.user, + ]; + } + } + ); + }; + + return (); +} +``` + +## Example: assigning from an abstract field to a concrete field + +If you are assigning from an abstract field, e.g. a `Node` to a `User` (which implements `Node`), you must use an inline fragment to refine the `Node` type to `User`. Consider this snippet: + +```js +const data = useFragment(graphql` + fragment AssignBestFriendButton_someType on Query { + node(id: "4") { + ... on User { + __typename + ...AssignableBestFriendButton_assignable_user + } + } + } +`, queryRef); + +const environment = useRelayEnvironment(); +const onClick = () => { + const updatableData = commitLocalUpdate( + environment, + (store: RecordSourceSelectorProxy) => { + const {updatableData} = store.readUpdatableQuery( + graphql` + query AssignBestFriendButtonUpdatableQuery + @updatable { + viewer { + best_friend { + ...AssignableBestFriendButton_assignable_user + } + } + } + `, + {} + ); + + if (data.node != null && data.node.__typename === "User" && updatableData.viewer != null) { + updatableData.viewer.best_friend = data.node; + } + } + ); +}; +``` + +In this snippet, we do two things: + +* We use an inline fragment to refine the `Node` type to the `User` type. Inside of this refinement, we spread the assignable fragment. +* We check that `data.node.__typename === "User"`. This indicates to Flow that within that if block, `data.node` is known to be a user, and therefore `updatableData.viewer.best_friend = data.node` can typecheck. + +## Example: assigning to an interface when the source is guaranteed to implement that interface + +You may wish to assign to a destination field that has an interface type (in this example, `Actor`). If the source field is guaranteed to implement that interface, then assignment is straightforward. + +For example, the source might have the same interface type or have a concrete type (`User`, in this example) that implements that interface. + +Consider the following snippet: + +```js +graphql` + fragment Foo_actor on Actor @assignable { + __typename + } +`; + +const data = useFragment(graphql` + fragment Foo_query on Query { + user { + ...Foo_actor + } + viewer { + actor { + ...Foo_actor + } + } + } +`, queryRef); + +const environment = useRelayEnvironment(); +const onClick = () => { + commitLocalUpdate(environment, store => { + const {updatableData} = store.readUpdatableQuery( + graphql` + query FooUpdatableQuery @updatable { + viewer { + actor { + ...Foo_actor + } + } + } + `, + {} + ); + + // Assigning the user works as you would expect + if (updatableData.viewer != null && data.user != null) { + updatableData.viewer = data.user; + } + + // As does assigning the viewer + if (updatableData.viewer != null && data.viewer?.actor != null) { + updatableData.viewer = data.viewer.actor; + } + }); +}; +``` + +## Example: assigning to an interface when the source is **not** guaranteed to implement that interface + +You may wish to assign to a destination field that has an interface type (in this example, `Actor`). If the source type (e.g. `Node`) is **not** known to implement that interface, then an extra step is involved: validation. + + + +:::note + +With additional changes to Relay's type generation, this can be made simpler. Please reach out to [Robert Balicki](https://www.internalfb.com/profile/view/1238951) if this is a pain point for you. + +::: + + + +In order to understand why, some background is necessary. The flow type for the setter for an interface field might look like: + +```js +set actor(value: ?{ + +__id: string, + +__isFoo_actor: string, + +$fragmentSpreads: Foo_actor$fragmentType, + ... +}): void, +``` + +The important thing to note is that the setter expects an object with a non-null `__isFoo_actor` field. + +When an assignable fragment with an abstract type is spread in a regular fragment, it results in an `__isFoo_actor: string` selection that is not optional if the type is known to implement the interface, and optional otherwise. + +Since a `Node` is **not** guaranteed to implement `Actor`, when the Relay compiler encounters the selection `node(id: "4") { ...Foo_actor }`, it will emit an optional field (`__isFoo_actor?: string`). Attempting to assign this to `updatableData.viewer.actor` will not typecheck! + +### Introducing validators + +The generated file for every generated artifact includes a named `validator` export. In our example, the function is as follows: + +```js +function validate(value/*: { + +__id: string, + +__isFoo_actor?: string, + +$fragmentSpreads: Foo_actor$fragmentType, + ... +}*/)/*: false | { + +__id: string, + +__isFoo_actor: string, + +$fragmentSpreads: Foo_actor$fragmentType, + ... +}*/ { + return value.__isFoo_actor != null ? (value/*: any*/) : false; +} +``` + +In other words, this function checks for the presence of the `__isFoo_actor` field. If it is found, it returns the same object, but with a flow type that is valid for assignment. If not, it returns false. + +### Example + +Let's put this all together in an example: + +```js +import {validate as validateActor} from 'Foo_actor.graphql'; + +graphql` + fragment Foo_actor on Actor @assignable { + __typename + } +`; + +const data = useFragment(graphql` + fragment Foo_query on Query { + node(id: "4") { + ...Foo_actor + } + } +`, queryRef); + +const environment = useRelayEnvironment(); +const onClick = () => { + commitLocalUpdate(environment, store => { + const {updatableData} = store.readUpdatableQuery( + graphql` + query FooUpdatableQuery @updatable { + viewer { + actor { + ...Foo_actor + } + } + } + `, + {} + ); + + if (updatableData.viewer != null && data.node != null) { + const validActor = validateActor(data.node); + if (validActor !== false) { + updatableData.viewer.actor = validActor; + } + } + }); +}; +``` + +### Can flow be used to infer the presence of this field? + +Unfortunately, if you check for the presence of `__isFoo_actor`, Flow does not infer that (on the type level), the field is not optional. Hence, we need to use validators. + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-store-data-legacy.md b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-store-data-legacy.md new file mode 100644 index 0000000000000..4ebb8049ba31c --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-store-data-legacy.md @@ -0,0 +1,142 @@ +--- +id: imperatively-modifying-store-data-unsafe +title: Imperatively modifying store data (unsafe) +slug: /guided-tour/updating-data/imperatively-modifying-store-data-unsafe/ +description: Imperatively modifying store data +keywords: +- record source +- store +- updater +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +Data in Relay stores can be imperatively modified within updater functions. + +## When to use updaters + +### Complex client updates + +You might provide an updater function if the changes to local data are more complex than what can be achieved by simply writing a network response to the store and cannot be handled by the declarative mutation directives. + +### Client schema extensions + +In addition, since the network response necessarily will not include data for fields defined in client schema extensions, you may wish to use an updater to initialize data defined in client schema extensions. + +### Use of other APIs + +Lastly, there are things you can only achieve using updaters, such as invalidating nodes, deleting nodes, finding all connections at a given field, etc. + +### If multiple optimistic responses modify a given store value + +If two optimistic responses affect a given value, and the first optimistic response is rolled back, the second one will remain applied. + +For example, if two optimistic responses each increase a story's like count by one, and the first optimistic response is rolled back, the second optimistic response remains applied. Since the second optimistic response **not recalculated**, the value of the like count will remain increased by two. + +An optimistic updater, on the other hand, would be re-run in this circumstance. + +## When **not** to use updaters + +### To trigger other side effects + +You should use the `onCompleted` callback to trigger other side effects. + +## The various types of updater functions + +The `useMutation` and `commitMutation` APIs accept configuration objects which can include `optimisticUpdater` and `updater` fields. The `requestSubscription` and `useSubscription` APIs accept configuration objects which can include `updater` fields. + +In addition, there is another API (`commitLocalUpdate`) which also accepts an updater function. It will be discussed in the [Other APIs for modifying local data](../local-data-updates/) section. + +## Optimistic updaters vs updaters + +Mutations can have both optimistic and regular updaters. Optimistic updaters are executed when a mutation is triggered. When that mutation completes or errors, the optimistic update is rolled back. At that point, the mutation response is written to the store and regular updaters are executed. See [order of execution of updater functions](../graphql-mutations/#order-of-execution-of-updater-functions). + +Regular updaters are executed when a mutation completes successfully. + +## Example + +Let's consider an example that provides an updater to `commitMutation`. + +```js +import type {Environment} from 'react-relay'; +import type {CommentCreateData, CreateCommentMutation} from 'CreateCommentMutation.graphql'; + +const {commitMutation, graphql} = require('react-relay'); +const {ConnectionHandler} = require('relay-runtime'); + +function commitCommentCreateMutation( + environment: Environment, + feedbackID: string, + input: CommentCreateData, +) { + return commitMutation(environment, { + mutation: graphql` + mutation CreateCommentMutation($input: CommentCreateData!) { + comment_create(input: $input) { + comment_edge { + cursor + node { + body { + text + } + } + } + } + } + `, + variables: {input}, + updater: (store: RecordSourceSelectorProxy, _response: ?CreateCommentMutation$data) => { + // we are not using _response in this example, but it is + // provided and statically typed. + + const feedbackRecord = store.get(feedbackID); + + // Get connection record + const connectionRecord = ConnectionHandler.getConnection( + feedbackRecord, + 'CommentsComponent_comments_connection', + ); + + // Get the payload returned from the server + const payload = store.getRootField('comment_create'); + + // Get the edge inside the payload + const serverEdge = payload.getLinkedRecord('comment_edge'); + + // Build edge for adding to the connection + const newEdge = ConnectionHandler.buildConnectionEdge( + store, + connectionRecord, + serverEdge, + ); + + // Add edge to the end of the connection + ConnectionHandler.insertEdgeAfter( + connectionRecord, + newEdge, + ); + }, + }); +} + +module.exports = {commit: commitCommentCreateMutation}; +``` + +Let's distill this example: + +* The updater receives a `store` argument, which is an instance of a [`RecordSourceSelectorProxy`](../../../api-reference/store/); this interface allows you to *imperatively* write and read data directly to and from the Relay store. This means that you have full control over how to update the store in response to the mutation response: you can *create entirely new records*, or *update or delete existing ones*. +* The updater receives a second `data` argument, which contains the data selected by the mutation fragment. This can be used to retrieve the payload data without interacting with the *`store`*. The type of this mutation response can be imported from the auto-generated `Mutation.graphql.js` file, and is given the name `MutationName$data`. + * The type of this `data` argument is a nullable version of the `$data` type. + * The `data` arguments contains just the data selected directly by the mutation argument. In other words, if another fragment is spread in the mutation, the data from that fragment will not be available within `data` by default. +* In our specific example, we're adding a new comment to our local store after it has successfully been added on the server. Specifically, we're adding a new item to a connection; for more details on the specifics of how that works, check out our section on [adding and removing items from a connection](../../list-data/updating-connections/). + * There is no need for an updater in this example — it would be a great place to use the `@appendEdge` directive instead! +* Note that the mutation response is a *root field* record that can be read from the `store` using the `store.getRootField` API. In our case, we're reading the `comment_create` root field, which is a root field in the mutation response. +* Note that the `root` field of the mutation is different from the `root` of queries, and `store.getRootField` in the mutation updater can only get the record from the mutation response. To get records from the root that's not in the mutation response, use `store.getRoot().getLinkedRecord` instead. +* Once the updater completes, any local data updates caused by the mutation `updater` will automatically cause components subscribed to the data to be notified of the change and re-render. + +## Learn more + +See the full APIs [here](../../../api-reference/store/). + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-store-data.md b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-store-data.md new file mode 100644 index 0000000000000..ccf71d1928048 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/imperatively-modifying-store-data.md @@ -0,0 +1,275 @@ +--- +id: imperatively-modifying-store-data +title: Imperatively modifying store data +slug: /guided-tour/updating-data/imperatively-modifying-store-data/ +description: Using readUpdatableQuery to update scalar fields in the store +keywords: +- record source +- store +- updater +- typesafe updaters +- readUpdatableQuery +- readUpdatableFragment +- updatable +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +:::note +See also [this guide on updating linked fields in the store](../imperatively-modifying-linked-fields). +::: + +Data in Relay stores can be imperatively modified within updater functions. + +## When to use updaters + +### Complex client updates + +You might provide an updater function if the changes to local data are more complex than what can be achieved by simply writing a network response to the store and cannot be handled by the declarative mutation directives. + +### Client schema extensions + +In addition, since the network response necessarily will not include data for fields defined in client schema extensions, you may wish to use an updater to initialize data defined in client schema extensions. + +### Use of other APIs + +Lastly, there are things you can only achieve using updaters, such as invalidating nodes, deleting nodes, finding all connections at a given field, etc. + +### If multiple optimistic responses modify a given store value + +If two optimistic responses affect a given value, and the first optimistic response is rolled back, the second one will remain applied. + +For example, if two optimistic responses each increase a story's like count by one, and the first optimistic response is rolled back, the second optimistic response remains applied. However, it is **not recalculated**, and the value of the like count will remain increased by two. + +## When **not** to use updaters + +### To trigger other side effects + +You should use the `onCompleted` callback to trigger other side effects. `onCompleted` callbacks are guaranteed to be called once, but updaters and optimistic updaters can be called repeatedly. + +## The various types of updater functions + +The `useMutation` and `commitMutation` APIs accept configuration objects which can include `optimisticUpdater` and `updater` fields. The `requestSubscription` and `useSubscription` APIs accept configuration objects which can include `updater` fields. + +In addition, there is another API (`commitLocalUpdate`) which also accepts an updater function. It will be discussed in the [Other APIs for modifying local data](../local-data-updates/) section. + +## Optimistic updaters vs updaters + +Mutations can have both optimistic and regular updaters. Optimistic updaters are executed when a mutation is triggered. When that mutation completes or errors, the optimistic update is rolled back. + +Regular updaters are executed when a mutation completes successfully. + +## Example + +Let's construct an example in which an `is_new_comment` field (which is defined in a schema extension) is set to `true` on a newly created Feedback object in a mutation updater. + +```graphql +# Feedback.graphql +extend type Feedback { + is_new_comment: Boolean +} +``` + +```js +// CreateFeedback.js +import type {Environment} from 'react-relay'; +import type { + FeedbackCreateData, + CreateFeedbackMutation, + CreateFeedbackMutation$data, +} from 'CreateFeedbackMutation.graphql'; + +const {commitMutation, graphql} = require('react-relay'); +const {ConnectionHandler} = require('relay-runtime'); + +function commitCreateFeedbackMutation( + environment: Environment, + input: FeedbackCreateData, +) { + return commitMutation(environment, { + mutation: graphql` + mutation CreateFeedbackMutation($input: FeedbackCreateData!) { + feedback_create(input: $input) { + feedback { + id + # Step 1: in the mutation response, spread an updatable fragment (defined below). + # This updatable fragment will select the fields that we want to update on this + # particular feedback object. + ...CreateFeedback_updatable_feedback + } + } + } + `, + variables: {input}, + + // Step 2: define an updater + updater: (store: RecordSourceSelectorProxy, response: ?CreateCommentMutation$data) => { + // Step 3: Access and nullcheck the feedback object. + // Note that this could also have been achieved with the @required directive. + const feedbackRef = response?.feedback_create?.feedback; + if (feedbackRef == null) { + return; + } + + // Step 3: call store.readUpdatableFragment + const {updatableData} = store.readUpdatableFragment( + // Step 4: Pass it a fragment literal, where the fragment contains the @updatable directive. + // This fragment selects the fields that you wish to update on the feedback object. + // In step 1, we spread this fragment in the query response. + graphql` + fragment CreateFeedback_updatable_feedback on Feedback @updatable { + is_new_comment + } + `, + // Step 5: Pass the fragment reference. + feedbackRef + ); + + // Step 6: Mutate the updatableData object! + updatableData.is_new_comment = true; + }, + }); +} + +module.exports = {commit: commitCreateFeedbackMutation}; +``` + +Let's distill what's going on here. + +* The `updater` accepts two parameters: a `RecordSourceSelectorProxy` and an optional object that is the result of reading out the mutation response. + * The type of this second argument is a nullable version of the `$data` type that is imported from the generated mutation file. + * The second argument contains just the data selected directly by the mutation argument. In other words, it will not contain any fields selected solely by spread fragments. +* This `updater` is executed after the mutation response has been written to the store. +* In this example updater, we do three things: + * First, we spread an updatable fragment in the mutation response. + * Second, we read out the fields selected by this fragment by calling `readUpdatableFragment`. This returns an updatable proxy object. + * Third, we update fields on this updatable proxy. +* Once this updater completes, the updates that have been recorded are written to the store, and all affected components are re-rendered. + +## Example 2: Updating data in response to user interactions + +Let's consider the common case of updating store data in response to a user interaction. In a click handler, let's a toggle an `is_selected` field. This field is defined on Users in a client schema extension. + +```graphql +# User.graphql +extend type User { + is_selected: Boolean +} +``` + +```js +// UserSelectToggle.react.js +import type {RecordSourceSelectorProxy} from 'react-relay'; +import type {UserSelectToggle_viewer$key} from 'UserSelectToggle_viewer.graphql'; + +const {useRelayEnvironment, commitLocalUpdate} = require('react-relay'); + +function UserSelectToggle({ userId, viewerRef }: { + userId: string, + viewerRef: UserSelectToggle_viewer$key, +}) { + const viewer = useFragment(graphql` + fragment UserSelectToggle_viewer on Viewer { + user(user_id: $user_id) { + id + name + is_selected + ...UserSelectToggle_updatable_user + } + } + `, viewerRef); + + const environment = useRelayEnvironment(); + + return +} +``` + +Let's distill what's going on here. + +* In a click handler, we call `commitLocalUpdate`, which accepts a Relay environment and an updater function. **Unlike in the previous examples, this updater does not accept a second parameter** because there is no associated network payload. +* In this updater function, we access get an updatable proxy object by calling `store.readUpdatableFragment`, and toggle the `is_selected` field. +* Like the previous example in which we called `readUpdatableFragment`, this can be rewritten to use the `readUpdatableQuery` API. + +:::note +This example can be rewritten using the `environment.commitPayload` API, albeit without type safety. +::: + +## Alternative API: `readUpdatableQuery`. + +In the previous examples, we used an updatable fragment to access the record whose fields we want to update. This can also be possible to do with an updatable query. + +If we know the path from the root (i.e. the object whose type is `Query`) to the record we wish to modify, we can use the `readUpdatableQuery` API to achieve this. + +For example, we could set the viewer's `name` field in response to an event as follows: + +```js +// NameUpdater.react.js +function NameUpdater({ queryRef }: { + queryRef: NameUpdater_viewer$key, +}) { + const environment = useRelayEnvironment(); + const data = useFragment( + graphql` + fragment NameUpdater_viewer on Viewer { + name + } + `, + queryRef + ); + const [newName, setNewName] = useState(data?.viewer?.name); + const onSubmit = () => { + commitLocalUpdate(environment, store => { + const {updatableData} = store.readUpdatableQuery( + graphql` + query NameUpdaterUpdateQuery @updatable { + viewer { + name + } + } + `, + {} + ); + const viewer = updatableData.viewer; + if (viewer != null) { + viewer.name = newName; + } + }); + }; + + // etc +} +``` + +* This particular example can be rewritten using `readUpdatableFragment`. However, you may prefer `readUpdatableQuery` for several reasons: + * You do not have ready access to a fragment reference, e.g. if the call to `commitLocalUpdate` is not obviously associated with a component. + * You do not have ready access to a fragment where we select the **parent record** of the record we wish to modify (e.g. the `Query` in this example). Due to a known type hole in Relay, **updatable fragments cannot be spread at the top level.** + * You wish to use variables in the updatatable fragment. Currently, updatable fragments reuse the variables that were passed to the query. This means that you cannot, for example, have an updatable fragment with fragment-local variables and call `readUpdatableFragment` multiple times, each time passing different variables. + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/introduction.md b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/introduction.md new file mode 100644 index 0000000000000..50e732fc8134f --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/introduction.md @@ -0,0 +1,24 @@ +--- +id: introduction +title: Introduction +slug: /guided-tour/updating-data/ +description: Relay guide to updating data +keywords: +- updating +- mutation +- useMutation +- commitMutation +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +In previous sections, the guided tour discussed how to fetch data using GraphQL queries. Though [refetching data](../refetching/) can have the *incidental* effect of modifying data in Relay's local store (if the refetched data has changed), we haven't discussed any ways to *intentionally* modify our locally stored data. + +This section will do just that: it will discuss how to update data on the server and how to update our local data store. + +:::note +The **Relay store** is a cache of GraphQL data, associated with a given Relay environment, that we have encountered during the execution of an application. +::: + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/local-data-updates.md b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/local-data-updates.md new file mode 100644 index 0000000000000..1ef8381e2c673 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/local-data-updates.md @@ -0,0 +1,71 @@ +--- +id: local-data-updates +title: Local Data Updates +slug: /guided-tour/updating-data/local-data-updates/ +description: Other APIs for modifying store data +keywords: +- client-only +- commitLocalUpdate +- commitPayload +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbLocalDataUpdatesFlow from './fb/FbLocalDataUpdatesFlow.md'; + +There are a couple of APIs that Relay provides in order to make purely local updates to the Relay store (i.e. updates not tied to a server operation). + +Note that local data updates can be made both on [client-only data](../client-only-data/), or on regular data that was fetched from the server via an operation. + +## commitLocalUpdate + +To make updates using an [`updater`](../imperatively-modifying-store-data/) function, you can use the `commitLocalUpdate` API: + +```js +import type {Environment} from 'react-relay'; + +const {commitLocalUpdate, graphql} = require('react-relay'); + +function commitCommentCreateLocally( + environment: Environment, + feedbackID: string, +) { + return commitLocalUpdate(environment, store => { + // Imperatively mutate the store here + }); +} + +module.exports = {commit: commitCommentCreateLocally}; +``` + +* `commitLocalUpdate` update simply takes an environment and an updater function. + * `updater` takes a *`store`* argument, which is an instance of a [`RecordSourceSelectorProxy`](../../../api-reference/store/); this interface allows you to *imperatively* write and read data directly to and from the Relay store. This means that you have full control over how to update the store: you can *create entirely new records*, or *update or delete existing ones*. + * Unlike regular and optimistic updaters that are accepted by the mutation and subscription APIs, the updater passed to `commitLocalUpdate` does not accept a second parameter. This is because there is no associated network response. +* Note that any local data updates will automatically cause components subscribed to the data to be notified of the change and re-render. + +## commitPayload + +`commitPayload` takes an `OperationDescriptor` and the payload for the query, and writes it to the Relay Store. The payload will be resolved like a normal server response for a query, and will also resolve Data Driven Dependencies that are passed as `JSResource`, `requireDefer`, etc. + +```js +import type {FooQueryRawResponse} from 'FooQuery.graphql' + +const {createOperationDescriptor} = require('relay-runtime'); + +const operationDescriptor = createOperationDescriptor(FooQuery, { + id: 'an-id', + otherVariable: 'value', +}); + +const payload: FooQueryRawResponse = {...}; + +environment.commitPayload(operation, payload); +``` + +* An `OperationDescriptor` can be created by `createOperationDescriptor`; it takes the query and the query variables. +* The payload can be typed using the Flow type generated by adding the directive `@raw_response_type` to the query. +* Note that any local data updates will automatically cause components subscribed to the data to be notified of the change and re-render. + + + + diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/typesafe-updaters-faq.md b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/typesafe-updaters-faq.md new file mode 100644 index 0000000000000..3199f0e1c018b --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/updating-data/typesafe-updaters-faq.md @@ -0,0 +1,95 @@ +--- +id: typesafe-updaters-faq +title: Typesafe updaters FAQ +slug: /guided-tour/updating-data/typesafe-updaters-faq/ +description: Typesafe updater FAQ +keywords: +- typesafe updaters +- readUpdatableQuery +- readUpdatableFragment +- updater +- updatable +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + + + +:::caution + +Because in TypeScript, [getters and setters cannot have different types](https://github.com/microsoft/TypeScript/issues/43662), and the generated types of getters and setters is not the same, `readUpdatableQuery` is currently unusable with TypeScript. `readUpdatableFragment` is usable, as long as the updatable fragment contains only scalar fields. + +::: + + + +# Typesafe Updaters FAQ + + + +:::note + +Is something missing from this Q&A? Are you confused? Would you like help adopting these APIs? Please, reach out to [Robert Balicki](https://fb.workplace.com/profile.php?id=100042823931887). I am happy to help! + +::: + + + +# General + +## What is typesafe updaters? + +Typesafe updaters is the name given to a project to provide a typesafe and ergonomic alternative to the existing APIs for imperatively updating data in the Relay store. + +## Why? + +Relay provides typesafe and ergonomic APIs for fetching and managing data that originates on the server. In addition, Relay provides the ability to define local-only fields in **client schema extensions**. However, the APIs for mutating the data in these fields has hitherto been verbose and not ergonomic, meaning that we could not recommend Relay as a solution for managing local state. + +## What was wrong with the existing APIs? + +The pre-existing APIs are verbose and not typesafe. They make it easy to make a variety of mistakes and require that the developer understand a new set of APIs only when writing updaters. + +Typesafe updaters is a set of APIs that are typesafe and (hopefully) more ergonomic. They leverage well-known Relay idioms (queries, fragments, type refinement) and use getters and setters instead of requiring that the developer learn about a set of methods that are unused elsewhere. + +## How does a developer use typesafe updaters? + +With typesafe updaters, a developers writes an updatable query or a fragment that specifies the data to imperatively update. Then, the developer reads out that data from the store, returning a so-called **updatable proxy**. Then, the developer mutates that updatable proxy. Mutating that updatable proxy using setters (e.g. `updatableData.name = "Godzilla"`) results in calls to the old API, but with added type safety. + +## Why are these labeled `_EXPERIMENTAL`? + +These are de facto not experimental. We encourage you to use them when writing new code! This suffix will be removed soon. + +## What is an updatable query or fragment? + +An updatable query or fragment is a query or fragment that has the `@updatable` directive. + +# Updatable queries and fragments are not fetched + +## Are fields selected in updatable queries and fragments fetched from the server? + +No! The server doesn't know about updatable queries and fragments. Their fields are never fetched. + +Even if you spread an updatable fragment in a regular query or fragment, the fields selected by that updatable fragment are not fetched as part of that request. + +## What if I want to fetch a field and also mutate it? + +You should select that field in both a regular query/fragment **and** in an updatable query/fragment. + +## What are some consequences of this? + +* When you read out updatable data, it can be missing if it isn't present in the store. +* You cannot spread regular fragments in updatable queries/fragments. +* The generated artifact for updatable queries/fragments does not contain a query ID and does not contain a normalization AST (which is used for writing network data to the store.) +* Directives like `@defer`, etc. do not make sense in this context, and are disallowed. + +# Misc + +## Where do I get a `store`? + +The classes `RelayRecordSourceSelectorProxy` and `RelayRecordSourceProxy` contain the methods `readUpdatableQuery` and `readUpdatableFragment`. One can acquire an instance of these classes: + +* In updaters of mutations and subscriptions +* In optimistic updaters of mutations +* When using `RelayModernEnvironment`'s `commitUpdate`, `applyUpdate`, etc. methods. +* When using the standalone `commitLocalUpdate` method. diff --git a/website/versioned_docs/version-v15.0.0/guided-tour/workflow.md b/website/versioned_docs/version-v15.0.0/guided-tour/workflow.md new file mode 100644 index 0000000000000..ce8aa650a3127 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guided-tour/workflow.md @@ -0,0 +1,39 @@ +--- +id: workflow +title: Workflow +slug: /guided-tour/workflow/ +description: Relay guide to workflow +keywords: +- workflow +- compiler +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbWorkflow from './fb/FbWorkflow.md'; + + + + + + + +Before we can get started writing Relay code, we need to make sure to **[setup the Relay Compiler](../../getting-started/installation-and-setup/#set-up-relay-compiler)**. + +The **[Relay Compiler](../../guides/compiler/)** will analyze any `graphql` literals inside your Javascript code, and produce a set of artifacts that are used by Relay at runtime, when the application is running on the browser. + +So whenever we're developing Relay components, for example by writing [Fragments](../rendering/fragments/) or [Queries](../rendering/queries/), we will need to run the Relay Compiler: + +```sh +yarn run relay +``` + +Or we can run it in watch mode, so the artifacts are re-generated as we update our source code: + +```sh +yarn run relay --watch +``` + + + + diff --git a/website/versioned_docs/version-v15.0.0/guides/client-schema-extensions.md b/website/versioned_docs/version-v15.0.0/guides/client-schema-extensions.md new file mode 100644 index 0000000000000..88037e76fb6eb --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guides/client-schema-extensions.md @@ -0,0 +1,207 @@ +--- +id: client-schema-extensions +title: Client Schema Extensions +slug: /guides/client-schema-extensions/ +description: Relay guide to client schema extensions +keywords: +- client +- schema +- extension +- commitLocalUpdate +--- + +import DocsRating from '@site/src/core/DocsRating'; + +:::note +See also [the local data updates](../../guided-tour/updating-data/local-data-updates/) and [client-only data](../../guided-tour/updating-data/client-only-data/) sections of the guided tour. +:::note + +Relay can be used to read and write local data, and act as a single source of truth for _all_ data in your client application. + +The Relay Compiler fully supports client-side extensions of the schema, which allows you to define local fields and types. + +## Table of Contents: + +- [Extending the server schema](#extending-the-server-schema) +- [Querying local state](#querying-local-state) +- [Mutating local state](#mutating-local-state) +- [Initial local state](#initial-local-state) + +## Extending the server schema + +To extend the server schema, create a new `.graphql` file inside your `--src` directory. +Let's call it `./src/clientSchema.graphql`. +This file needs to be in a folder referenced in the `"schemaExtensions"` of your relay config. + +This schema describes what local data can be queried on the client. +It can even be used to extend an existing server schema. + +For example, we can create a new type called `Note`: + +```graphql +type Note { + id: ID! + title: String + body: String +} +``` + +And then extend the server schema type `User`, with a list of `Note`, called `notes`. + +```graphql +extend type User { + notes: [Note] +} +``` + +## Querying local state + +Accessing local data is no different from querying your GraphQL server, although you are required to include at least one server field in the query. +The field can be from the server schema, or it can be schema agnostic, like an introspection field (e.g. `__typename`). + +Here, we use [useLazyLoadQuery](../../api-reference/use-lazy-load-query) to get the current `User` via the `viewer` field, along with their id, name and the local list of notes. + +```javascript +// Example.js +import * as React from 'react'; +import { useLazyLoadQuery, graphql } from 'react-relay'; + +const Example = (props) => { + const data = useLazyLoadQuery(graphql` + query ExampleQuery { + viewer { + id + name + notes { + id + title + body + } + } + } + `, {}); + // ... +} +``` + +## Mutating local state + +All local data lives in the [Relay Store](../../api-reference/store/). + +Updating local state can be done with any `updater` function. + +The `commitLocalUpdate` function is especially ideal for this, because writes to local state are usually executed outside of a mutation. + +To build upon the previous example, let's try creating, updating and deleting a `Note` from the list of `notes` on `User`. + +### Create + +```javascript +import {commitLocalUpdate} from 'react-relay'; + +let tempID = 0; + +function createUserNote(environment) { + commitLocalUpdate(environment, store => { + const user = store.getRoot().getLinkedRecord('viewer'); + const userNoteRecords = user.getLinkedRecords('notes') || []; + + // Create a unique ID. + const dataID = `client:Note:${tempID++}`; + + //Create a new note record. + const newNoteRecord = store.create(dataID, 'Note'); + + // Add the record to the user's list of notes. + user.setLinkedRecords([...userNoteRecords, newNoteRecord], 'notes'); + }); +} +``` + +Note that since this record will be rendered by the `ExampleQuery` via `useLazyLoadQuery`, the query data will automatically be retained and won't be garbage collected. + +If no component is rendering the local data and you want to manually retain it, you can do so by calling `environment.retain()`: + +```javascript +import {createOperationDescriptor, getRequest} from 'relay-runtime'; + +// Create a query that references that record +const localDataQuery = graphql` + query LocalDataQuery { + viewer { + notes { + __typename + } + } + } +`; + +// Create an operation descriptor for the query +const request = getRequest(localDataQuery); +const operation = createOperationDescriptor(request, {} /* variables */); + + +// Tell Relay to retain this operation so any data referenced by it isn't garbage collected +// In this case, all the notes linked to the `viewer` will be retained +const disposable = environment.retain(operation); + + +// Whenever you don't need that data anymore and it's okay for Relay to garbage collect it, +// you can dispose of the retain +disposable.dispose(); +``` + +### Update + +```javascript +import {commitLocalUpdate} from 'react-relay'; + +function updateUserNote(environment, dataID, body, title) { + commitLocalUpdate(environment, store => { + const note = store.get(dataID); + + note.setValue(body, 'body'); + note.setValue(title, 'title') + }); +} +``` + +### Delete + +```javascript +import {commitLocalUpdate} from 'react-relay'; + +function deleteUserNote(environment, dataID) { + commitLocalUpdate(environment, store => { + const user = store.getRoot().getLinkedRecord('viewer'); + const userNoteRecords = user.getLinkedRecords('notes'); + + // Remove the note from the list of user notes. + const newUserNoteRecords = userNoteRecords.filter(x => x.getDataID() !== dataID); + + // Delete the note from the store. + store.delete(dataID); + + // Set the new list of notes. + user.setLinkedRecords(newUserNoteRecords, 'notes'); + }); +} +``` + +## Initial local state + +All new client-side schema fields default to `undefined` value. Often however, you will want to set the initial state before querying local data. +You can use an updater function via `commitLocalUpdate` to prime local state. + +```javascript +import {commitLocalUpdate} from 'react-relay'; + +commitLocalUpdate(environment, store => { + const user = store.getRoot().getLinkedRecord('viewer'); + + // initialize user notes to an empty array. + user.setLinkedRecords([], 'notes'); +}); +``` + + diff --git a/website/versioned_docs/version-v15.0.0/guides/compiler.md b/website/versioned_docs/version-v15.0.0/guides/compiler.md new file mode 100644 index 0000000000000..2f127a23aa20d --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guides/compiler.md @@ -0,0 +1,172 @@ +--- +id: compiler +title: Relay Compiler +slug: /guides/compiler/ +description: Relay guide to the compiler +keywords: +- compiler +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly, OssOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; +import FbRunningCompiler from './fb/FbRunningCompiler.md'; +import FbGraphQLSchema from './fb/FbGraphQLSchema.md'; +import FbImportingGeneratedDefinitions from './fb/FbImportingGeneratedDefinitions.md'; + +## `graphql` + +The `graphql` template tag provided by Relay serves as the mechanism to write queries, fragments, mutations and subscriptions in the [GraphQL](http://graphql.org/learn/) language. For example: + +```javascript +import {graphql} from 'react-relay'; + +graphql` + query MyQuery { + viewer { + id + } + } +`; +``` + +The result of using the `graphql` template tag is a `GraphQLTaggedNode`; a runtime representation of the GraphQL document. + +Note that `graphql` template tags are **never executed at runtime**. Instead, they are compiled ahead of time by the Relay compiler into generated artifacts that live alongside your source code, and which Relay requires to operate at runtime. + + +## Compiler + +Relay uses the Relay Compiler to convert [`graphql`](#graphql) literals into generated files that live alongside your source files. + +A fragment like the following: + +```javascript +graphql` + fragment MyComponent on Type { + field + } +` +``` + +Will cause a generated file to appear in `./__generated__/MyComponent.graphql.js`, +with both runtime artifacts (which help to read and write from the Relay Store) +and [Flow types](https://flow.org/) to help you write type-safe code. + +The Relay Compiler is responsible for generating code as part of a build step which can then be referenced at runtime. By building the query ahead of time, the Relay's runtime is not responsible for generating a query string, and various optimizations can be performed on the query that could be too expensive at runtime (for example, fields that are duplicated in the query can be merged during the build step, to improve efficiency of processing the GraphQL response). + +### GraphQL Schema + + + + + + + +To use the Relay Compiler, you need a `.graphql` [GraphQL Schema](https://graphql.org/learn/schema/) file, describing your GraphQL server's API. Typically these files are local representations of a server source of truth and are not edited directly. For example, we might have a `schema.graphql` like: + +```graphql +schema { + query: Root +} + +type Root { + dictionary: [Word] +} + +type Word { + id: String! + definition: WordDefinition +} + +type WordDefinition { + text: String + image: String +} +``` + + + +### Running the Compiler + + + + + + + +Additionally, you need a directory containing `.js` files that use the `graphql` tag to describe GraphQL queries and fragments. Let's call this `./src`. + +Then run `yarn run relay` as set up before. + +This will create a series of `__generated__` directories that are co-located with the corresponding files containing `graphql` tags. + +For example, given the two files: + +- `src/Components/DictionaryComponent.js` + +```javascript +const DictionaryWordFragment = graphql` + fragment DictionaryComponent_word on Word { + id + definition { + ...DictionaryComponent_definition + } + } +` + +const DictionaryDefinitionFragment = graphql` + fragment DictionaryComponent_definition on WordDefinition { + text + image + } +` +``` + +- `src/Queries/DictionaryQuery.js` + +```javascript +const DictionaryQuery = graphql` + query DictionaryQuery { + dictionary { + ...DictionaryComponent_word + } + } +` +``` + +This would produce three generated files, and two `__generated__` directories: + +- `src/Components/__generated__/DictionaryComponent_word.graphql.js` +- `src/Components/__generated__/DictionaryComponent_definition.graphql.js` +- `src/Queries/__generated__/DictionaryQuery.graphql.js` + + + + +### Importing generated definitions + + + + + + + + +Typically you will not need to import your generated definitions. The [Relay Babel plugin](../../getting-started/installation-and-setup#setup-babel-plugin-relay) will then convert the `graphql` literals in your code into `require()` calls for the generated files. + +However the Relay Compiler also automatically generates [Flow](https://flow.org) types as [type comments](https://flow.org/en/docs/types/comments/). For example, you can import the generated Flow types like so: + +```javascript +import type {DictionaryComponent_word} from './__generated__/DictionaryComponent_word.graphql'; +``` + +More rarely, you may need to access a query, mutation, fragment or subscription from multiple files. In these cases, you can also import it directly: + +```js +import DictionaryComponent_word from './__generated__/DictionaryComponent_word.graphql'; +``` + + + + + diff --git a/website/versioned_docs/version-v15.0.0/guides/graphql-server-specification.md b/website/versioned_docs/version-v15.0.0/guides/graphql-server-specification.md new file mode 100644 index 0000000000000..1fc1f9177b1f0 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guides/graphql-server-specification.md @@ -0,0 +1,447 @@ +--- +id: graphql-server-specification +title: GraphQL Server Specification +slug: /guides/graphql-server-specification/ +description: Relay GraphQL server specification guide +keywords: +- GraphQL +- server +- specification +--- + +import DocsRating from '@site/src/core/DocsRating'; + +The goal of this document is to specify the assumptions that Relay makes about a GraphQL server and demonstrate them through an example GraphQL schema. + +Table of Contents: + +- [Preface](#preface) +- [Schema](#schema) +- [Object Identification](#object-identification) +- [Connections](#connections) +- [Further Reading](#further-reading) + +## Preface + +The two core assumptions that Relay makes about a GraphQL server are that it provides: + +1. A mechanism for refetching an object. +2. A description of how to page through connections. + +This example demonstrates all two of these assumptions. This example is not comprehensive, but it is designed to quickly introduce these core assumptions, to provide some context before diving into the more detailed specification of the library. + +The premise of the example is that we want to use GraphQL to query for information about ships and factions in the original Star Wars trilogy. + +It is assumed that the reader is already familiar with [GraphQL](http://graphql.org/); if not, the README for [GraphQL.js](https://github.com/graphql/graphql-js) is a good place to start. + +It is also assumed that the reader is already familiar with [Star Wars](https://en.wikipedia.org/wiki/Star_Wars); if not, the 1977 version of Star Wars is a good place to start, though the 1997 Special Edition will serve for the purposes of this document. + +## Schema + +The schema described below will be used to demonstrate the functionality that a GraphQL server used by Relay should implement. The two core types are a faction and a ship in the Star Wars universe, where a faction has many ships associated with it. + +```graphql +interface Node { + id: ID! +} + +type Faction implements Node { + id: ID! + name: String + ships: ShipConnection +} + +type Ship implements Node { + id: ID! + name: String +} + +type ShipConnection { + edges: [ShipEdge] + pageInfo: PageInfo! +} + +type ShipEdge { + cursor: String! + node: Ship +} + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} + +type Query { + rebels: Faction + empire: Faction + node(id: ID!): Node +} +``` + +## Object Identification + +Both `Faction` and `Ship` have identifiers that we can use to refetch them. We expose this capability to Relay through the `Node` interface and the `node` field on the root query type. + +The `Node` interface contains a single field, `id`, which is an `ID!`. The `node` root field takes a single argument, an `ID!`, and returns a `Node`. These two work in concert to allow refetching; if we pass the `id` returned in that field to the `node` field, we get the object back. + +Let's see this in action, and query for the ID of the rebels: + +```graphql +query RebelsQuery { + rebels { + id + name + } +} +``` + +returns + +```json +{ + "rebels": { + "id": "RmFjdGlvbjox", + "name": "Alliance to Restore the Republic" + } +} +``` + +So now we know the ID of the Rebels in our system. We can now refetch them: + +```graphql +query RebelsRefetchQuery { + node(id: "RmFjdGlvbjox") { + id + ... on Faction { + name + } + } +} +``` + +returns + +```json +{ + "node": { + "id": "RmFjdGlvbjox", + "name": "Alliance to Restore the Republic" + } +} +``` + +If we do the same thing with the Empire, we'll find that it returns a different ID, and we can refetch it as well: + +```graphql +query EmpireQuery { + empire { + id + name + } +} +``` + +yields + +```json +{ + "empire": { + "id": "RmFjdGlvbjoy", + "name": "Galactic Empire" + } +} +``` + +and + +```graphql +query EmpireRefetchQuery { + node(id: "RmFjdGlvbjoy") { + id + ... on Faction { + name + } + } +} +``` + +yields + +```json +{ + "node": { + "id": "RmFjdGlvbjoy", + "name": "Galactic Empire" + } +} +``` + +The `Node` interface and `node` field assume globally unique IDs for this refetching. A system without globally unique IDs can usually synthesize them by combining the type with the type-specific ID, which is what was done in this example. + +The IDs we got back were base64 strings. IDs are designed to be opaque (the only thing that should be passed to the `id` argument on `node` is the unaltered result of querying `id` on some object in the system), and base64ing a string is a useful convention in GraphQL to remind viewers that the string is an opaque identifier. + +Complete details on how the server should behave are available in the [GraphQL Object Identification](https://graphql.org/learn/global-object-identification/) best practices guide in the GraphQL site. + +## Connections + +A faction has many ships in the Star Wars universe. Relay contains functionality to make manipulating one-to-many relationships easy, using a standardized way of expressing these one-to-many relationships. This standard connection model offers ways of slicing and paginating through the connection. + +Let's take the rebels, and ask for their first ship: + +```graphql +query RebelsShipsQuery { + rebels { + name + ships(first: 1) { + edges { + node { + name + } + } + } + } +} +``` + +yields + +```json +{ + "rebels": { + "name": "Alliance to Restore the Republic", + "ships": { + "edges": [ + { + "node": { + "name": "X-Wing" + } + } + ] + } + } +} +``` + +That used the `first` argument to `ships` to slice the result set down to the first one. But what if we wanted to paginate through it? On each edge, a cursor will be exposed that we can use to paginate. Let's ask for the first two this time, and get the cursor as well: + +``` +query MoreRebelShipsQuery { + rebels { + name + ships(first: 2) { + edges { + cursor + node { + name + } + } + } + } +} +``` + +and we get back + +```json + +{ + "rebels": { + "name": "Alliance to Restore the Republic", + "ships": { + "edges": [ + { + "cursor": "YXJyYXljb25uZWN0aW9uOjA=", + "node": { + "name": "X-Wing" + } + }, + { + "cursor": "YXJyYXljb25uZWN0aW9uOjE=", + "node": { + "name": "Y-Wing" + } + } + ] + } + } +} +``` + +Notice that the cursor is a base64 string. That's the pattern from earlier: the server is reminding us that this is an opaque string. We can pass this string back to the server as the `after` argument to the `ships` field, which will let us ask for the next three ships after the last one in the previous result: + +``` + +query EndOfRebelShipsQuery { + rebels { + name + ships(first: 3 after: "YXJyYXljb25uZWN0aW9uOjE=") { + edges { + cursor + node { + name + } + } + } + } +} +``` + +gives us + +```json + + +{ + "rebels": { + "name": "Alliance to Restore the Republic", + "ships": { + "edges": [ + { + "cursor": "YXJyYXljb25uZWN0aW9uOjI=", + "node": { + "name": "A-Wing" + } + }, + { + "cursor": "YXJyYXljb25uZWN0aW9uOjM=", + "node": { + "name": "Millenium Falcon" + } + }, + { + "cursor": "YXJyYXljb25uZWN0aW9uOjQ=", + "node": { + "name": "Home One" + } + } + ] + } + } +} +``` + +Sweet! Let's keep going and get the next four! + +```graphql +query RebelsQuery { + rebels { + name + ships(first: 4 after: "YXJyYXljb25uZWN0aW9uOjQ=") { + edges { + cursor + node { + name + } + } + } + } +} +``` + +yields + +```json +{ + "rebels": { + "name": "Alliance to Restore the Republic", + "ships": { + "edges": [] + } + } +} +``` + +Hm. There were no more ships; guess there were only five in the system for the rebels. It would have been nice to know that we'd reached the end of the connection, without having to do another round trip in order to verify that. The connection model exposes this capability with a type called `PageInfo`. So let's issue the two queries that got us ships again, but this time ask for `hasNextPage`: + +```graphql +query EndOfRebelShipsQuery { + rebels { + name + originalShips: ships(first: 2) { + edges { + node { + name + } + } + pageInfo { + hasNextPage + } + } + moreShips: ships(first: 3 after: "YXJyYXljb25uZWN0aW9uOjE=") { + edges { + node { + name + } + } + pageInfo { + hasNextPage + } + } + } +} +``` + +and we get back + +```json +{ + "rebels": { + "name": "Alliance to Restore the Republic", + "originalShips": { + "edges": [ + { + "node": { + "name": "X-Wing" + } + }, + { + "node": { + "name": "Y-Wing" + } + } + ], + "pageInfo": { + "hasNextPage": true + } + }, + "moreShips": { + "edges": [ + { + "node": { + "name": "A-Wing" + } + }, + { + "node": { + "name": "Millenium Falcon" + } + }, + { + "node": { + "name": "Home One" + } + } + ], + "pageInfo": { + "hasNextPage": false + } + } + } +} +``` + +So on the first query for ships, GraphQL told us there was a next page, but on the next one, it told us we'd reached the end of the connection. + +Relay uses all of this functionality to build out abstractions around connections, to make these easy to work with efficiently without having to manually manage cursors on the client. + +Complete details on how the server should behave are available in the [GraphQL Cursor Connections](https://relay.dev/graphql/connections.htm) spec. + +## Further Reading + +This concludes the overview of the GraphQL Server Specifications. For the detailed requirements of a Relay-compliant GraphQL server, a more formal description of the [Relay cursor connection](https://relay.dev/graphql/connections.htm) model, the [GraphQL global object identification](https://graphql.org/learn/global-object-identification/) model are all available. + +To see code implementing the specification, the [GraphQL.js Relay library](https://github.com/graphql/graphql-relay-js) provides helper functions for creating nodes and connections; that repository's [`__tests__`](https://github.com/graphql/graphql-relay-js/tree/main/src/__tests__) folder contains an implementation of the above example as integration tests for the repository. + + diff --git a/website/versioned_docs/version-v15.0.0/guides/network-layer.md b/website/versioned_docs/version-v15.0.0/guides/network-layer.md new file mode 100644 index 0000000000000..37e7076fa9552 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guides/network-layer.md @@ -0,0 +1,74 @@ +--- +id: network-layer +title: Network Layer +slug: /guides/network-layer/ +description: Relay guide to the network layer +keywords: +- network +- caching +--- + +import DocsRating from '@site/src/core/DocsRating'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + + + +> In most cases, the network layer is setup for you. You should not need to worry about this step unless you are setting up a new environment. + + + +In order to know how to access your GraphQL server, Relay requires developers to provide an object implementing the `INetwork` interface when creating an instance of a Relay Environment. The environment uses this network layer to execute queries, mutations, and (if your server supports them) subscriptions. This allows developers to use whatever transport (HTTP, WebSockets, etc) and authentication is most appropriate for their application, decoupling the environment from the particulars of each application's network configuration. + +Currently the easiest way to create a network layer is via a helper from the `relay-runtime` package: + +```javascript +import { + Environment, + Network, + RecordSource, + Store, +} from 'relay-runtime'; + +// Define a function that fetches the results of an operation (query/mutation/etc) +// and returns its results as a Promise: +function fetchQuery( + operation, + variables, + cacheConfig, + uploadables, +) { + return fetch('/graphql', { + method: 'POST', + headers: { + // Add authentication and other headers here + 'content-type': 'application/json' + }, + body: JSON.stringify({ + query: operation.text, // GraphQL text from input + variables, + }), + }).then(response => { + return response.json(); + }); +} + +// Create a network layer from the fetch function +const network = Network.create(fetchQuery); +const store = new Store(new RecordSource()) + +const environment = new Environment({ + network, + store + // ... other options +}); + +export default environment; +``` + +Note that this is a basic example to help you get started. This example could be extended with additional features such as request/response caching (enabled e.g. when `cacheConfig.force` is false) and uploading form data for mutations (the `uploadables` parameter). + +## Caching + +The Relay store will cache data from queries that are currently retained. See the section on [reusing cached data](../../guided-tour/reusing-cached-data/) of the guided tour. + + diff --git a/website/versioned_docs/version-v15.0.0/guides/persisted-queries.md b/website/versioned_docs/version-v15.0.0/guides/persisted-queries.md new file mode 100644 index 0000000000000..6f82d191e71aa --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guides/persisted-queries.md @@ -0,0 +1,326 @@ +--- +id: persisted-queries +title: Persisted Queries +slug: /guides/persisted-queries/ +description: Relay guide to persisted queries +keywords: +- persisted +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly, OssOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + + + +> Persistence is handled by the `relay` command for you. You likely do not need to worry about the contents of this guide. + + + +The relay compiler supports persisted queries. This is useful because: + +- The client operation text becomes just an md5 hash which is usually shorter than the real + query string. This saves upload bytes from the client to the server. + +- The server can now allowlist queries which improves security by restricting the operations + that can be executed by a client. + + + +## Usage on the client + +### The `persistConfig` option + +In your relay configiration section in `package.json` you'll need specify +"persistConfig". + +``` +"scripts": { + "relay": "relay-compiler", + "relay-persisting": "node relayLocalPersisting.js" +}, +"relay": { + "src": "./src", + "schema": "./schema.graphql", + "persistConfig": { + "url": "http://localhost:2999", + "params": {} + } +} +``` + +Specifiying `persistConfig` in the config will do the following: + +1. It converts all query and mutation operation texts to md5 hashes. + + For example without `persistConfig`, a generated `ConcreteRequest` might look + like below: + + ```javascript + const node/*: ConcreteRequest*/ = (function(){ + //... excluded for brevity + return { + "kind": "Request", + "operationKind": "query", + "name": "TodoItemRefetchQuery", + "id": null, // NOTE: id is null + "text": "query TodoItemRefetchQuery(\n $itemID: ID!\n) {\n node(id: $itemID) {\n ...TodoItem_item_2FOrhs\n }\n}\n\nfragment TodoItem_item_2FOrhs on Todo {\n text\n isComplete\n}\n", + //... excluded for brevity + }; + })(); + + ``` + + With `persistConfig` this becomes: + + ```javascript + const node/*: ConcreteRequest*/ = (function(){ + //... excluded for brevity + return { + "kind": "Request", + "operationKind": "query", + "name": "TodoItemRefetchQuery", + "id": "3be4abb81fa595e25eb725b2c6a87508", // NOTE: id is now an md5 hash + // of the query text + "text": null, // NOTE: text is null now + //... excluded for brevity + }; + })(); + + ``` + +2. It will send an HTTP POST request with a `text` parameter to the +specified `url`. +You can also add additional request body parameters via the `params` option. + +``` +"scripts": { + "relay": "relay-compiler" +}, +"relay": { + "src": "./src", + "schema": "./schema.graphql", + "persistConfig": { + "url": "http://localhost:2999", + "params": {} + } +} +``` + +### Local Persisted Queries + +With the following config, you can generate a local JSON file which contains a map of `operation_id => full operation text`. + +``` +"scripts": { + "relay": "relay-compiler" +}, +"relay": { + "src": "./src", + "schema": "./schema.graphql", + "persistConfig": { + "file": "./persisted_queries.json", + "algorithm": "MD5" // this can be one of MD5, SHA256, SHA1 + } +} +``` + +Ideally, you'll take this file and ship it to your server at deploy time so your server knows about all the queries it could possibly receive. If you don't want to do that, you'll have to implement the [Automatic Persisted Queries handshake](https://www.apollographql.com/docs/apollo-server/performance/apq/). + +#### Tradeoffs + +- ✅ If your server's persisted query datastore gets wiped, you can recover automatically through your client's requests. +- ❌ When there's a cache miss, it'll cost you an extra round trip to the server. +- ❌ You'll have to ship your `persisted_queries.json` file to the browser which will increase your bundle size. + +### Example implemetation of `relayLocalPersisting.js` + +Here's an example of a simple persist server that will save query text to the `queryMap.json` file. + + +```javascript +const http = require('http'); +const crypto = require('crypto'); +const fs = require('fs'); + +function md5(input) { + return crypto.createHash('md5').update(input).digest('hex'); +} + +class QueryMap { + constructor(fileMapName) { + this._fileMapName = fileMapName; + this._queryMap = new Map(JSON.parse(fs.readFileSync(this._fileMapName))); + } + + _flush() { + const data = JSON.stringify(Array.from(this._queryMap.entries())); + fs.writeFileSync(this._fileMapName, data); + } + + saveQuery(text) { + const id = md5(text); + this._queryMap.set(id, text); + this._flush(); + return id; + } +} + +const queryMap = new QueryMap('./queryMap.json'); + +async function requestListener(req, res) { + if (req.method === 'POST') { + const buffers = []; + for await (const chunk of req) { + buffers.push(chunk); + } + const data = Buffer.concat(buffers).toString(); + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + try { + if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') { + throw new Error( + 'Only "application/x-www-form-urlencoded" requests are supported.' + ); + } + const text = new URLSearchParams(data).get('text'); + if (text == null) { + throw new Error('Expected to have `text` parameter in the POST.'); + } + const id = queryMap.saveQuery(text); + res.end(JSON.stringify({"id": id})); + } catch (e) { + console.error(e); + res.writeHead(400); + res.end(`Unable to save query: ${e}.`); + } + } else { + res.writeHead(400); + res.end("Request is not supported.") + } +} + +const PORT = 2999; +const server = http.createServer(requestListener); +server.listen(PORT); + +console.log(`Relay persisting server listening on ${PORT} port.`); +``` + +The example above writes the complete query map file to `./queryMap.json`. +To use this, you'll need to update `package.json`: + + +``` +"scripts": { + "persist-server": "node ./relayLocalPersisting.js", + "relay": "relay-compiler" +} +``` + + + +### Network layer changes + +You'll need to modify your network layer fetch implementation to pass an ID parameter in the POST body (e.g., `doc_id`) instead of a query parameter: + +```javascript +function fetchQuery(operation, variables) { + return fetch('/graphql', { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + doc_id: operation.id, // NOTE: pass md5 hash to the server + // query: operation.text, // this is now obsolete because text is null + variables, + }), + }).then(response => { + return response.json(); + }); +} +``` + + +## Executing Persisted Queries on the Server + + + +Your server should then look up the query referenced by `doc_id` when responding to this request. + + + + + +To execute client requests that send persisted queries instead of query text, your server will need to be able +to lookup the query text corresponding to each ID. Typically this will involve saving the output of the `queryMap.json` JSON file to a database or some other storage mechanism, and retrieving the corresponding text for the ID specified by a client. + +Additionally, your implementation of `relayLocalPersisting.js` could directly save queries to the database or other storage. + +For universal applications where the client and server code are in one project, this is not an issue since you can place +the query map file in a common location accessible to both the client and the server. + +### Compile time push + +For applications where the client and server projects are separate, one option is to have an additional npm run script +to push the query map at compile time to a location accessible by your server: + +```javascript +"scripts": { + "push-queries": "node ./pushQueries.js", + "persist-server": "node ./relayLocalPersisting.js", + "relay": "relay-compiler && npm run push-queries" +} +``` + +Some possibilities of what you can do in `./pushQueries.js`: + +- `git push` to your server repo. + +- Save the query maps to a database. + +### Run time push + +A second more complex option is to push your query maps to the server at runtime, without the server knowing the query IDs at the start. +The client optimistically sends a query ID to the server, which does not have the query map. The server then in turn requests +for the full query text from the client so it can cache the query map for subsequent requests. This is a more complex approach +requiring the client and server to interact to exchange the query maps. + +### Simple server example + +Once your server has access to the query map, you can perform the mapping. The solution varies depending on the server and +database technologies you use, so we'll just cover the most common and basic example here. + +If you use `express-graphql` and have access to the query map file, you can import it directly and +perform the matching using the `persistedQueries` middleware from [express-graphql-persisted-queries](https://github.com/kyarik/express-graphql-persisted-queries). + +```javascript +import express from 'express'; +import {graphqlHTTP} from 'express-graphql'; +import {persistedQueries} from 'express-graphql-persisted-queries'; +import queryMap from './path/to/queryMap.json'; + +const app = express(); + +app.use( + '/graphql', + persistedQueries({ + queryMap, + queryIdKey: 'doc_id', + }), + graphqlHTTP({schema}), +); +``` + +## Using `persistConfig` and `--watch` + +It is possible to continuously generate the query map files by using the `persistConfig` and `--watch` options simultaneously. +This only makes sense for universal applications i.e. if your client and server code are in a single project +and you run them both together on localhost during development. Furthermore, in order for the server to pick up changes +to the `queryMap.json`, you'll need to have server side hot-reloading set up. The details on how to set this up +are out of the scope of this document. + + + + diff --git a/website/versioned_docs/version-v15.0.0/guides/relay-resolvers.md b/website/versioned_docs/version-v15.0.0/guides/relay-resolvers.md new file mode 100644 index 0000000000000..1a3ae6f1ff69c --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guides/relay-resolvers.md @@ -0,0 +1,249 @@ +--- +id: relay-resolvers +title: "Relay Resolvers" +slug: /guides/relay-resolvers/ +description: Relay guide to Relay Resolvers +keywords: +- resolvers +- derived +- selectors +- reactive +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +Relay Resolvers is an experimental Relay feature which enables modeling derived state as client-only fields in Relay’s GraphQL graph. Similar to server [resolvers](https://graphql.org/learn/execution/), a Relay Resolver is a function which defines how to compute the value of a GraphQL field. However, unlike server resolvers, Relay Resolvers are evaluated reactively on the client. A Relay Resolver reads fields off of its parent object and returns a derived result. If any of those fields change, Relay will automatically reevaluate the resolver. + +Relay Resolvers are particularly valuable in apps which store client state in Relay via [client schema extensions](https://relay.dev/docs/guides/client-schema-extensions/), since they allow you to compose together client data, server data — and even other Relay Resolver fields — into fields which update reactively as the underlying data changes. + +Relay Resolvers were originally conceived of as an alternative to Flux-style [selectors](https://redux.js.org/usage/deriving-data-selectors) and can be thought of as providing similar capabilities. + +Concretely, Relay Resolvers are defined as functions annotated with a special docblock syntax. The Relay compiler will automatically recognize these docblocks in any JavaScript file and use them to extend the schema that is available within your project. + +Let’s look at an example Relay Resolver: + +```jsx +import type {UserGreetingResolver$key} from 'UserGreetingResolver.graphql'; +import {graphql} from 'relay-runtime'; +import {readFragment} from 'relay-runtime/store/ResolverFragments'; + +/** + * @RelayResolver + * + * @onType User + * @fieldName greeting + * @rootFragment UserGreetingResolver + * + * A greeting for the user which includes their name and title. + */ +export default function userGreetingResolver(userKey: UserGreetingResolver$key): string { + const user = readFragment(graphql` + fragment UserGreetingResolver on User { + honorific + last_name + }`, userKey); + + return `Hello ${user.honorific} ${user.last_name}!`; +} +``` + +This resolver adds a new field `greeting` to the `User` object type. It reads the `honorific` and `last_name` fields off of the parent `User` and derives a greeting string. The new `greeting` field may now be used by any Relay component throughout your project which has access to a `User`. + +Consuming this new field looks identical to consuming a field defined in the server schema: + +```jsx +function MyGreeting({userKey}) { + const user = useFragment(` + fragment MyGreeting on User { + greeting + }`, userKey); + return

{user.greeting}

; +} +``` + +## Docblock Fields + +The Relay compiler looks for the following fields in any docblocks that includes `@RelayResolver`: + +- `@RelayResolver` (required) +- `@onType` or `@onInterface` (required) The GraphQL type/interface on which the new field should be exposed +- `@fieldName` (required) The name of the new field +- `@rootFragment` (required) The name of the fragment read by `readFragment` +- `@deprecated` (optional) Indicates that the field is [deprecated](https://spec.graphql.org/June2018/#sec--deprecated). May be optionally followed text giving the reason that the field is deprecated. + +The docblock may also contain free text. This free text will be used as the field’s human-readable description, which will be surfaced in Relay’s editor support on hover and in autocomplete results. + +## Relay Resolver Signature + +In order for Relay to be able to call a Relay Resolver, it must conform to a set of conventions: + +1. The resolver function must accept a single argument, which is the key for its root fragment. +2. The resolver function must be the default export of its module (only one resolver per module) +3. The resolver must read its fragment using the special `readFragment` function. +4. The resolver function must be pure +5. The resolver’s return value must be immutable + +Unlike server resolvers, Relay Resolvers may return any JavaScript value. This includes classes, functions and arrays. However, we generally encourage having Relay Resolvers return scalar values and only returning more complex JavaScript values (like functions) as an escape hatch. + + +## Lint Rule + +In many cases, the contents of the docblock can be derived from the javascript implementation. In those cases, the [`relay-resolvers`](https://www.internalfb.com/eslint/relay-resolvers) ESLint rule rule will offer auto-fixes to derive the docblock from the implementation and ensure that the two remain in sync. The lint rule also enforces a naming convention for resolver function and modules names. + + +## How They Work + +When parsing your project, the Relay compiler looks for `@RelayResolver` docblocks and uses them to add special fields to the GraphQL schema. If a query or fragment references one of these fields, Relay’s generated artifact for that query or fragment will automatically include an `import` of the resolver function. *Note that this can happen recursively if the Relay Resolver field you are reading itself reads one or more Relay Resolver fields.* + +When the field is first read by a component, Relay will evaluate the Relay Resolver function and cache the result. Other components that read the same field will read the same cached value. If at any point any of the fields that the resolver reads (via its root fragment) change, Relay will reevaluate the resolver. If the return value changes (determined by `===` equality) Relay will propagate that change to all components (and other Relay Resolvers) that are currently reading the field. + +## Error Handling + +In order to make product code as robust as possible, Relay Resolvers follow the GraphQL spec’s documented [best practice](https://graphql.org/learn/best-practices/#nullability) of returning null when a field resolver errors. Instead of throwing, errors thrown by Relay Resolvers will be logged to your environment's configured `requiredFieldLogger` with an event of kind `"relay_resolver.error"`. If you make use of Relay Resolves you should be sure to configure your environment with a `requiredFieldLogger` which reports those events to whatever system you use for tracking runtime errors. + +If your component requires a non-null value in order to render, and can’t provide a reasonable fallback experience, you can annotate the field access with `@required`. + +## Passing arguments to resolver fields + +For resolvers (and live resolvers) we support two ways of defining field arguments: + +1. GraphQL: Arguments that are defined via @argumentDefinitions on the resolver's fragment. +2. JS Runtime: Arguments that can be passed directly to the resolver function. +3. You can also combine these, and define arguments on the fragment and on the resolver's field itself, Relay will validate the naming (these arguments have to have different names), and pass GraphQL arguments to fragment, and JS arguments to the resolver's function. + + +Let’s look at the example 1: + +## Defining Resolver field with Fragment Arguments + +```js +/** +* @RelayResolver +* @fieldName **my_resolver_field** +* @onType **MyType** +* @rootFragment myResolverFragment +*/ +function myResolver(key) { + const data = readFragment(graphql` + fragment myResolverFragment on MyType + @argumentDefinitions(**my_arg**: {type: "Float!"}) { + field_with_arg(arg: $my_arg) { + __typename + } + } + `, key); + + return data.field_with_arg.__typename; +} +``` + +### Using Resolver field with arguments for Fragment + +This resolver will extend the **MyType** with the new field **my_resolver_field(my_arg: Float!)** and the fragment arguments for **myResolverFragment** can be passed directly to this field. + +```js +const data = useLazyLoadQuery(graphql` + query myQuery($id: ID, $my_arg: Float!) { + node(id: $id) { + ... on MyType { + my_resolver_field(my_arg: $my_arg) + } + } + } +`, { id: "some id", my_arg: 2.5 }); +``` + +For these fragment arguments relay will pass then all queries/fragments where the resolver field is used to the resolver’s fragment. + + +### Defining Resolver field with Runtime (JS) Arguments + +Relay resolvers also support runtime arguments that are not visible/passed to fragments, but are passed to the resolver function itself. + +You can define these fragments using GraphQL’s [Schema Definition Language](https://graphql.org/learn/schema/) in the **@fieldName** + +```js +/** +* @RelayResolver +* @fieldName **my_resolver_field(my_arg: String, my_other_arg: Int)** +* @onType **MyType** +* @rootFragment myResolverFragment +*/ +function myResolver(key, args) { + if (args.my_other_arg === 0) { + return "The other arg is 0"; + } + + const data = readFragment(graphql` + fragment myResolverFragment on MyType + some_field + } + `, key); + return data.some_field.concat(args.my_arg); +} +``` + +### Using Resolver field with runtime arguments + +This resolver will extend **MyType** with the new field **my_resolver_field(my_arg: String, my_other_arg: Int).** + +```js +const data = useLazyLoadQuery(graphql` + query myQuery($id: ID, $my_arg: String!) { + node(id: $id) { + ... on MyType { + my_resolver_field(my_arg: $my_arg, my_other_arg: 1) + } + } + } +`, { id: "some id", my_arg: "hello world!"}); +``` + +### Defining Resolver field with Combined Arguments + +We can also combine both of these approaches and define field arguments both on the resolver’s fragment and on the field itself: + +```js +/** +* @RelayResolver +* @fieldName **my_resolver_field(my_js_arg: String)** +* @onType **MyType** +* @rootFragment myResolverFragment +*/ +function myResolver(key, args) { + const data = readFragment(graphql` + fragment myResolverFragment on MyType + @argumentDefinitions(**my_gql_arg**: {type: "Float!"}) { + field_with_arg(arg: $my_arg) { + __typename + } + } + `, key); + + return `Hello ${args.my_js_arg}, ${data.field_with_arg.__typename}`; +} +``` + +### Using Resolver field with combined arguments + +Relay will extend the **MyType** with the new resolver field that has two arguments: **my_resolver_field(my_js_arg: String, my_gql_arg: Float!) + +** +Example query: + +```js +const data = useLazyLoadQuery(graphql` + query myQuery($id: ID, $my_arg: String!) { + node(id: $id) { + ... on MyType { + my_resolver_field(my_js_arg: "World", my_qql_arg: 2.5) + } + } + } +`, { id: "some id" }); +``` + +## Current Limitations + +- Relay Resolvers are still considered experimental. To use them you must ensure that the `ENABLE_RELAY_RESOLVERS` runtime feature flag is enabled, and that the `enable_relay_resolver_transform` feature flag is enabled in your project’s Relay config file. diff --git a/website/versioned_docs/version-v15.0.0/guides/required-directive.md b/website/versioned_docs/version-v15.0.0/guides/required-directive.md new file mode 100644 index 0000000000000..f6e373f343759 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guides/required-directive.md @@ -0,0 +1,234 @@ +--- +id: required-directive +title: "@required Directive" +slug: /guides/required-directive/ +description: Relay guide to @required +keywords: +- required +- directive +- optional +- nullthrows +--- + +import DocsRating from '@site/src/core/DocsRating'; + +The `@required` directive can be added to fields in your Relay queries to declare how null values should be handled at runtime. You can think of it as saying "if this field is ever null, its parent field is invalid and should be null". + +When you have a GraphQL schema where many fields are nullable, a considerable amount of product code is needed to handle each field's potential "nullness" before the underlying data can be used. With `@required`, Relay can handle some types of null checks before it returns data to your component, which means that **any field you annotate with** **`@required`** **will become non-nullable in the generated types for your response**. + +If a `@required` field is null at runtime, Relay will "bubble" that nullness up to the field's parent. For example, given this query: + +```graphql +query MyQuery { + viewer { + name @required(action: LOG) + age + } +} +``` + +If `name` is null, relay would return `{ viewer: null }`. You can think of `@required` in this instance as saying "`viewer` is useless without a `name`". + +## Action + +The `@required` directive has a required `action` argument which has three possible values: + +### `NONE` (expected) + +This field is expected to be null sometimes. + +### `LOG` (recoverable) + +This value is not expected to ever be null, but the component **can still render** if it is. If a field with `action: LOG` is null, the Relay environment logger will receive an event that looks like this: + +```javascript +{ + name: 'read.missing_required_field', + owner: string, // MyFragmentOrQueryName + fieldPath: string, // path.to.my.field +}; +``` + +### `THROW` (unrecoverable) + +This value should not be null, and the component **cannot render without it**. If a field with `action: THROW` is null at runtime, the component which reads that field **will throw during render**. The error message includes both the owner and field path. Only use this option if your component is contained within an [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary). + +## Locality + +A field's `@required` status is **local to the fragment where it is specified**. This allows you to add add/remove the directive without having to think about anything outside the scope of your component. + +This choice reflects the fact that some components may be able to recover better from missing data than others. For example, a `` component could probably render something sensible even if the restaurant's address is missing, but a `` component might not. + +However, all usages of the `@required` directive on the same field in a single fragment must be consistent with their usage. This situation mostly occurs when selecting fields in inline fragments. For example, the following fragment would fail to compile: + +```graphql +fragment UserInfo on User { + job { + ... on Actor { + certifications + } + ... on Lawyer { + certifications @required(action: LOG) + } + } +} +``` + +The Relay compiler will give you an error like `All references to a field must have matching @required declarations.`. To fix this, either set the `@required` directive on each of the fields selected in the inline fragment or remove the directive entirely. + +## Chaining + +`@required` directives can be chained to make a deeply nested field accessible after just one null check: + +```javascript +const user = useFragment(graphql` + fragment MyUser on User { + name @required(action: LOG) + profile_picture @required(action: LOG) { + url @required(action: LOG) + } + }`, key); + if(user == null) { + return null; + } + return {user.name} +``` + +**Note**: If you use `@required` on a top level field of a fragment, the object returned from `useFragment` itself may become nullable. The generated types will reflect this. + +When chaining `@required` directives, the Relay compiler will help you from unintentionally creating a chain with a more severe action than intended. Consider the following fragment + +```graphql +fragment MyUser on User { + profile_picture @required(action: THROW) { + url @required(action: LOG) + } +} +``` + +In this example we want the component to THROW if the `profile_picture` field is null but we only want to LOG an error if the `url` field is null. But recall, Relay will "bubble" nullness up to the parent field, if the `url` field is null it will then cause the `profile_picture` field to become null as well. And once that happens, the component will THROW. If you implement a pattern like this, the Relay compiler will give you an error + +``` +A @required field may not have an `action` less severe than that of its @required parent. This @required directive should probably have `action: LOG` so that it can match its parent +``` + +To fix this, either change the `profile_picture` to use `action: LOG` or change the `url` field to use `action: THROW`. + +## Caveats with Connections + +There are currently some limitations in using the `@required` and `@connection` directives together. When you use the `@connection` directive, Relay automatically inserts some additional fields into the connection, and those fields won't be generated with the `@required` directive. This can result in inconsistencies if you use the `@required` directive on fields in a Connection type. Consider the following example: + +```graphql +fragment FriendsList on User @refetchable(queryName: "FriendsListQuery") { + friends(after: $cursor, first: $count) @connection(key: "FriendsList_friends") { + edges { + node @required(action: LOG) { + job @required(action: LOG) { + title @required(action: LOG) + } + } + } + } +} +``` + +Any usages of `@required` on the `node` field or any of its direct child fields will cause the Relay compiler to give you an error saying `All references to a field must have matching @required declarations.`. In order to bypass this you'll need to remove the `@required` directives on those fields. + +In the above example, we'd need to remove the `@required` directives on both the `node` and `job` fields, but the usage on the `title` field would not create an error. + +```graphql +fragment FriendsList on User @refetchable(queryName: "FriendsListQuery") { + friends(after: $cursor, first: $count) @connection(key: "FriendsList_friends") { + edges { + node { + job { + title @required(action: LOG) + } + } + } + } +} +``` + +## FAQ + +### Why did @required make a non-nullable field/root nullable? + +When using the `LOG` or `NONE` actions, Relay will "bubble" a missing field up to its parent field or fragment root. This means that adding `@required(action: LOG)` (for example) to a child of a non-nullable fragment root will cause the type of the fragment root to become nullable. + +### What happens if you use `@required` in a plural field + +If a `@required(action: LOG)` field is missing in a plural field, the _item_ in the list will be returned as null. It will _not_ cause the entire array to become null.. If you have any question about how it will behave, you can inspect the generated Flow types. + +### Why are @required fields in an inline fragment still nullable? + +Imagine a fragment like this: + +```graphql +fragment MyFrag on Actor { + ... on User { + name @required(action: THROW) + } +} +``` + +It's possible that your `Actor` will not be a `User` and therefore not include a `name`. To represent that in types, we generate a Flow type that looks like this: `{name?: string}`. + +If you encounter this issue, you can add a `__typename` like this: + +```graphql +fragment MyFrag on Actor { + __typename + ... on User { + name @required(action: THROW) + } +} +``` + +In this situation Relay will generate a union type like: `{__typename: 'User', name: string} | {__typename: '%ignore this%}`. Now you can check the `__typename` field to narrow your object's type down to one that has a non-nullable `name`. + + +Example diff showing the adoption of this strategy: D24370183 + + +### Why not implement this at the schema/server level? + +The "requiredness" of a field is actually a product decision and not a schema question. Therefore we need to implement the handling of it at the product level. Individual components need to be able to decide for themselves how to handle a missing value. + +For example, if a notification is trying to show the price for a Marketplace listing, it could probably just omit the price and still render. If payment flow for that same listing is missing the price, it should probably blow up. + +Another issue is that changes to the server schema are much more difficult to ship since they affect all existing clients across all platforms. + +Basically every value returned by Relay is nullable. This is intentional since we want to be able to handle field-level errors whenever possible. If we lean into KillsParentOnException we would end up wanting to make basically every field use it and our apps would be becomes more brittle since errors which used to be small, become large. + + + +_Extracted from [this comment thread](https://fb.workplace.com/groups/cometeng/permalink/937671436726844/?comment_id=937681186725869)._ +_Further discussion in [this comment thread](https://fb.workplace.com/groups/cometeng/permalink/937671436726844/?comment_id=938335873327067)._ + + +### Can `(action: NONE)` be the default? + +On one hand action: NONE makes the most sense as a default (omitted action == no action). However, we are aware that whichever value we choose as the default will be considered the default action for engineers to choose since it's the path of least resistance. + +We actually believe that in most cases LOG is the most ideal choice. It gives the component a chance to gracefully recover while also giving us signal that a part of our app is rendering in a sub-optimal way. + +We debated making LOG the default action for that reason, but I think that's confusing as well. + +So, for now we are planning to not offer a default argument. After all, it's still much less to write out than the equivalent manual null checks. Once we see how people use it we will consider what value (if any) should be the default. + + + +### Does @required change anything about the logger project field? + +When using recoverableViolation or unrecoverableViolation, the second argument is the FBLogger project name ([defined on Comet here](https://fburl.com/diffusion/rn99dl4s)): + +```javascript +recoverableViolation('My error string', 'my_logger_project'); +``` + +When you switch to using `@required`, any `THROW` or `LOG` actions will log to the `relay-required` logger project instead ([see here in logview](https://fburl.com/logview/l40t7cjv)). + +For most teams, this shouldn't be an issue; care has been taken to ensure tasks still get routed to the correct owner of the file that is using `@required`. However, if your team has any queries that utilize the logger project field, you may want to consider the implications. + + diff --git a/website/versioned_docs/version-v15.0.0/guides/testing-relay-components.md b/website/versioned_docs/version-v15.0.0/guides/testing-relay-components.md new file mode 100644 index 0000000000000..2cd077fc60371 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guides/testing-relay-components.md @@ -0,0 +1,584 @@ +--- +id: testing-relay-components +title: Testing Relay Components +slug: /guides/testing-relay-components/ +description: Relay guide to testing Relay components +keywords: +- testing +- createMockEnvironment +- RelayMockEnvironment +- MockPayloadGenerator +- relay_test_operation +- queuePendingOperation +- resolver +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly, OssOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +## Abstract + +The purpose of this document is to cover the Relay APIs for testing Relay components. + +The content is focused mostly on jest unit-tests (testing individual components) and integration tests (testing a combination of components). But these testing tools may be applied in different cases: screenshot-tests, production smoke-tests, "Redbox" tests, fuzz-tests, e2e test, etc. + +What are the benefits of writing jest tests: + +* In general, it improves the stability of the system. Flow helps with catching a various set of Javascript errors, but it is still possible to introduce regressions to the components. Unit-tests help find, reproduce, and fix regressions, and prevent them in the future. +* It simplifies the refactoring process: when properly written (testing public interface, not implementation) - tests help with changing the internal implementation of the components. +* It may speed up and improve the development workflow. Some people may call it Test Driven Development (TM). But essentially it's just writing tests for public interfaces of your components, and then writing the components that implement those interfaces. Jest —watch mode really shines in this case. +* It will simplify the on-boarding process for new developers. Having tests helps new developers ramp up on the new code base, allowing them to fix bugs and deliver features. + +One thing to notice: while jest unit- and integration tests will help improve the stability of the system, they should be considered one part of a bigger stability infrastructure with multiple layers of automated testing: flow, e2e, screenshot, "Redbox", performance tests. + +## Testing with Relay + +Testing applications that use Relay may be challenging, because of the additional data fetching layer that wraps the actual product code. + +And it's not always easy to understand the mechanics of all processes that are happening behind Relay, and how to properly handle interactions with the framework. + +Fortunately, there are tools that aim to simplify the process of writing tests for Relay components, by providing imperative APIs for controlling the request/response flow and additional API for mock data generation. + +There are two main modules that you may use in your tests: + +* `createMockEnvironment(options): RelayMockEnvironment` +* `MockPayloadGenerator` and the `@relay_test_operation` directive + + +With `createMockEnvironment,` you will be able to create an instance of `RelayMockEnvironment`, a Relay environment specifically for your tests. The instance created by `createMockEnvironment` implements the Relay Environment Interface and it also has an additional Mock layer, with methods that allow you to resolve/reject and control the flow of operations (queries/mutations/subscriptions). + +The main purpose of `MockPayloadGenerator` is to improve the process of creating and maintaining the mock data for tested components. + +One of the patterns you may see in the tests for Relay components: 95% of the test code is the test preparation—the gigantic mock object with dummy data, manually created, or just a copy of a sample server response that needs to be passed as the network response. And the remaining 5% is actual test code. As a result, people don't test much. It's hard to create and manage all these dummy payloads for different cases. Hence, writing tests is time-consuming and tests are sometimes painful to maintain. + +With the `MockPayloadGenerator` and `@relay_test_operation`, we want to get rid of this pattern and switch the developer's focus from the preparation of the test to the actual testing. + + +## RelayMockEnvironment API Overview + +RelayMockEnvironment is a special version of Relay Environment with additional API methods for controlling the operation flow: resolving and rejection operations, providing incremental payloads for subscriptions, working with the cache. + +* Methods for finding operations executed on the environment + * `getAllOperations()` - get all operation executed during the test by the current time + * `findOperation(findFn => boolean) `- find particular operation in the list of all executed operations, this method will throw, if operation is not available. Maybe useful to find a particular operation when multiple operations executed at the same time + * `getMostRecentOperation() -` return the most recent operation, this method will throw if no operations were executed prior this call. +* Methods for resolving or rejecting operations + * `nextValue(request | operation, data)` - provide payload for operation(request), but not complete request. Practically useful when testing incremental updates and subscriptions + * `complete(request | operation)` - complete the operation, no more payloads are expected for this operation, when it's completed. + * `resolve(request | operation, data)` - resolve the request with provided GraphQL response. Essentially, it's nextValue(...) and complete(...) + * `reject(request | operation, error)` - reject the request with particular error + * `resolveMostRecentOperation(operation => data)` - resolve and getMostRecentOperation work together + * `rejectMostRecentOperation(operation => error)` - reject and getMostRecentOperation work together + * `queueOperationResolver(operation => data | error)` - adds an OperationResolver function to the queue. The passed resolver will be used to resolve/reject operations as they appear + * `queuePendingOperation(query, variables)` - in order for the `usePreloadedQuery` hook to not suspend, one must call these functions: + * `queueOperationResolver(resolver)` + * `queuePendingOperation(query, variables)` + * `preloadQuery(mockEnvironment, query, variables)` with the same `query` and `variables` that were passed to `queuePendingOperation`. `preloadQuery` must be called after `queuePendingOperation`. +* Additional utility methods + * `isLoading(request | operation)` - will return `true` if operations has not been completed, yet. + * `cachePayload(request | operation, variables, payload)` - will add payload to QueryResponse cache + * `clearCache() `- will clear QueryResponse cache + +## Mock Payload Generator and the `@relay_test_operation` Directive + +`MockPayloadGenerator` may drastically simplify the process of creating and maintaining mock data for your tests. `MockPayloadGenerator` can generate dummy data for the selection that you have in your operation. There is an API to modify the generated data - Mock Resolvers. With Mock Resolvers, you may adjust the data for your needs. Mock Resolvers are defined as an object where **keys are names of GraphQL types (`ID`, `String`, `User`, `Comment`, etc),** and values are functions that return the default data for the type. + +Example of a simple Mock Resolver: + +```js +{ + ID() { + // Return mock value for a scalar filed with type ID + return 'my-id'; + }, + String() { + // Every scalar field with type String will have this default value + return "Lorem Ipsum" + } +} +``` + + +It is possible to define more resolvers for Object types + +```js +{ + // This will be the default values for User object in the query response + User() { + return { + id: 4, + name: "Mark", + profile_picture: { + uri: "http://my-image...", + }, + }; + }, +} +``` + + + +### Mock Resolver Context + +The first argument of the MockResolver is the object that contains Mock Resolver Context. It is possible to return dynamic values from mock resolvers based on the context - for instance, name or alias of the field, a path in the selection, arguments, or parent type. + + +```js +{ + String(context) { + if (context.name === 'zip') { + return '94025'; + } + if (context.path != null && context.path.join('.') === 'node.actor.name') { + return 'Current Actor Name'; + } + if (context.parentType === 'Image' && context.name === 'uri') { + return 'http://my-image.url'; + } + } +} +``` + +### ID Generation + +The second argument of the Mock Resolver is a function that will generate a sequence of integers, useful to generate unique ids in the tests + +```js +{ + // will generate strings "my-id-1", "my-id-2", etc. + ID(_, generateId) { + return `my-id-${generateId()}`; + }, +} +``` + +### Float, Integer, Boolean, etc... + +Please note, that for production queries we don't have full type information for Scalar fields - like Boolean, Integer, Float. And in the MockResolvers, they map to String. You can use `context` to adjust return values, based on the field name, alias, etc. + +### @relay_test_operation + +Most of GraphQL type information for a specific field in the selection is not available during Relay runtime. By default, Relay, cannot get type information for a scalar field in the selection, or an interface type of the object. + +Operation with the @relay_test_operation directive will have additional metadata that will contain GraphQL type info for fields in the operation's selection. And it will improve the quality of the generated data. You also will be able to define Mock resolvers for Scalar (not only ID and String) and Abstract types: + +```javascript +{ + Float() { + return 123.456; + }, + Boolean(context) { + if (context.name === 'can_edit') { + return true; + } + return false; + }, + Node() { + return { + __typename: 'User', + id: 'my-user-id', + }; + } +} +``` + +## Examples + +### Relay Component Test + +Using `createMockEnvironment` and `MockPayloadGenerator` allows writing concise tests for components that use Relay hooks. Both those modules can be imported from `relay-test-utils` + + +```javascript +// Say you have a component with the useLazyLoadQuery or a QueryRenderer +const MyAwesomeViewRoot = require('MyAwesomeViewRoot'); +const { + createMockEnvironment, + MockPayloadGenerator, +} = require('relay-test-utils'); + +// Relay may trigger 3 different states +// for this component: Loading, Error, Data Loaded +// Here is examples of tests for those states. +test('Loading State', () => { + const environment = createMockEnvironment(); + const renderer = ReactTestRenderer.create( + , + ); + + // Here we just verify that the spinner is rendered + expect( + renderer.root.find(node => node.props['data-testid'] === 'spinner'), + ).toBeDefined(); +}); + +test('Data Render', () => { + const environment = createMockEnvironment(); + const renderer = ReactTestRenderer.create( + , + ); + + // Wrapping in ReactTestRenderer.act will ensure that components + // are fully updated to their final state. + ReactTestRenderer.act(() => { + environment.mock.resolveMostRecentOperation(operation => + MockPayloadGenerator.generate(operation), + ); + }); + + // At this point operation will be resolved + // and the data for a query will be available in the store + expect( + renderer.root.find(node => node.props['data-testid'] === 'myButton'), + ).toBeDefined(); +}); + +test('Error State', () => { + const environment = createMockEnvironment(); + const renderer = ReactTestRenderer.create( + , + ); + + // Wrapping in ReactTestRenderer.act will ensure that components + // are fully updated to their final state. + ReactTestRenderer.act(() => { + // Error can be simulated with `rejectMostRecentOperation` + environment.mock.rejectMostRecentOperation(new Error('Uh-oh')); + }); + + expect( + renderer.root.find(item => (item.props.testID = 'errorMessage')), + ).toBeDefined(); +}); +``` + + + +### Fragment Component Tests + +Essentially, in the example above, `resolveMostRecentOperation` will generate data for all child fragment containers (pagination, refetch). But, usually the root component may have many child fragment components and you may want to exercise a specific component that uses `useFragment`. The solution for that would be to wrap your fragment container with the `useLazyLoadQuery` component that renders a Query that spreads fragments from your fragment component: + +```javascript +test('Fragment', () => { + const environment = createMockEnvironment(); + const TestRenderer = () => { + const data = useLazyLoadQuery( + graphql` + query TestQuery @relay_test_operation { + myData: node(id: "test-id") { + # Spread the fragment you want to test here + ...MyFragment + } + } + `, + {}, + ); + return + }; + + const renderer = ReactTestRenderer.create( + + + + + + ); + + // Wrapping in ReactTestRenderer.act will ensure that components + // are fully updated to their final state. + ReactTestRenderer.act(() => { + environment.mock.resolveMostRecentOperation(operation => + MockPayloadGenerator.generate(operation), + ); + }); + + expect(renderer).toMatchSnapshot(); +}); +``` + +### Pagination Component Test + +Essentially, tests for pagination components (e.g. using `usePaginationFragment`) are not different from fragment component tests. But we can do more here, we can actually see how the pagination works - we can assert the behavior of our components when performing pagination (load more, refetch). + +```js +// Pagination Example +test('`Pagination` Container', () => { + const environment = createMockEnvironment(); + const TestRenderer = () => { + const data = useLazyLoadQuery( + graphql` + query TestQuery @relay_test_operation { + myConnection: node(id: "test-id") { + connection { + # Spread the pagination fragment you want to test here + ...MyConnectionFragment + } + } + } + `, + {}, + ); + return + }; + + const renderer = ReactTestRenderer.create( + + + + + + ); + + // Wrapping in ReactTestRenderer.act will ensure that components + // are fully updated to their final state. + ReactTestRenderer.act(() => { + environment.mock.resolveMostRecentOperation(operation => + MockPayloadGenerator.generate(operation, { + ID(_, generateId) { + // Why we're doing this? + // To make sure that we will generate a different set of ID + // for elements on first page and the second page. + return `first-page-id-${generateId()}`; + }, + PageInfo() { + return { + has_next_page: true, + }; + }, + }), + ); + }); + + // Let's find a `loadMore` button and click on it to initiate pagination request, for example + const loadMore = renderer.root.find(node => node.props['data-testid'] === 'loadMore') + expect(loadMore.props.disabled).toBe(false); + loadMore.props.onClick(); + + // Wrapping in ReactTestRenderer.act will ensure that components + // are fully updated to their final state. + ReactTestRenderer.act(() => { + environment.mock.resolveMostRecentOperation(operation => + MockPayloadGenerator.generate(operation, { + ID(_, generateId) { + // See, the second page IDs will be different + return `second-page-id-${generateId()}`; + }, + PageInfo() { + return { + // And the button should be disabled, now. Probably. + has_next_page: false, + }; + }, + }), + ); + }); + + expect(loadMore.props.disabled).toBe(true); +}); +``` + +### Refetch Component + +We can use similar approach here with wrapping the component with a query. And for the sake of completeness, we will add an example here: + +```js +test('Refetch Container', () => { + const environment = createMockEnvironment(); + const TestRenderer = () => { + const data = useLazyLoadQuery( + graphql` + query TestQuery @relay_test_operation { + myData: node(id: "test-id") { + # Spread the pagination fragment you want to test here + ...MyRefetchableFragment + } + } + `, + {}, + ); + return + }; + + const renderer = ReactTestRenderer.create( + + + + + + ); + + ReactTestRenderer.act(() => { + environment.mock.resolveMostRecentOperation(operation => + MockPayloadGenerator.generate(operation), + ); + }); + + // Assuming we have refetch button in the Container + const refetchButton = renderer.root.find(node => node.props['data-testid'] === 'refetch'); + + // This should trigger the `refetch` + refetchButton.props.onClick(); + + ReactTestRenderer.act(() => { + environment.mock.resolveMostRecentOperation(operation => + MockPayloadGenerator.generate(operation, { + // We can customize mock resolvers, to change the output of the refetch query + }), + ); + }); + + expect(renderer).toMatchSnapshot(); +}); +``` + + + +### Mutations + +Mutations themselves are operations, so we can test them independently (unit-test) for a specific mutation, or in combination with the view from which this mutation is called. + +:::note +the `useMutation` API is an improvement over calling `commitMutation` directly. +::: + +```js +// Say, you have a mutation function +function sendMutation(environment, onCompleted, onError, variables) + commitMutation(environment, { + mutation: graphql`...`, + onCompleted, + onError, + variables, + }); +} + +// Example test may be written like so +test('it should send mutation', () => { + const environment = createMockEnvironment(); + const onCompleted = jest.fn(); + sendMutation(environment, onCompleted, jest.fn(), {}); + const operation = environment.mock.getMostRecentOperation(); + + ReactTestRenderer.act(() => { + environment.mock.resolve( + operation, + MockPayloadGenerator.generate(operation) + ); + }); + + expect(onCompleted).toBeCalled(); +}); +``` + +### Subscription + +> The `useSubscription` API is an improvement over calling `requestSubscription` directly. + +We can test subscriptions similarly to how we test mutations. + +```js +// Example subscribe function +function subscribe(environment, onNext, onError, variables) + requestSubscription(environment, { + subscription: graphql`...`, + onNext, + onError, + variables, + }); +} + +// Example test may be written like so +test('it should subscribe', () => { + const environment = createMockEnvironment(); + const onNext = jest.fn(); + subscribe(environment, onNext, jest.fn(), {}); + const operation = environment.mock.getMostRecentOperation(); + + ReactTestRenderer.act(() => { + environment.mock.nextValue( + operation, + MockPayloadGenerator.generate(operation) + ); + }); + + expect(onNext).toBeCalled(); +}); +``` + + + +### Example with `queueOperationResolver` + + +With `queueOperationResolver` it is possible to define responses for operations that will be executed on the environment + +```javascript +// Say you have a component with the QueryRenderer +const MyAwesomeViewRoot = require('MyAwesomeViewRoot'); +const { + createMockEnvironment, + MockPayloadGenerator, +} = require('relay-test-utils'); + +test('Data Render', () => { + const environment = createMockEnvironment(); + environment.mock.queueOperationResolver(operation => + MockPayloadGenerator.generate(operation), + ); + + const renderer = ReactTestRenderer.create( + , + ); + + // At this point operation will be resolved + // and the data for a query will be available in the store + expect( + renderer.root.find(node => node.props['data-testid'] === 'myButton'), + ).toBeDefined(); +}); + +test('Error State', () => { + const environment = createMockEnvironment(); + environment.mock.queueOperationResolver(() => + new Error('Uh-oh'), + ); + const renderer = ReactTestRenderer.create( + , + ); + + expect( + renderer.root.find(item => (item.props.testID = 'errorMessage')), + ).toBeDefined(); +}); +``` + +### With Relay Hooks + +The examples in this guide should work for testing components both with Relay Hooks, Containers or Renderers. When writing tests that involve the `usePreloadedQuery` hook, please also see the `queuePendingOperation` note above. + +### toMatchSnaphot(...) + +Even though in all of the examples here you can see assertions with `toMatchSnapshot()`, we keep it that way just to make examples concise. But it's not the recommended way to test your components. + +**[React Testing Library](https://testing-library.com/react)** is a set of helpers that let you test React components without relying on their implementation details. This approach makes refactoring a breeze and also nudges you towards best practices for accessibility. Although it doesn't provide a way to "shallowly" render a component without its children, a test runner like Jest lets you do this by [mocking](https://reactjs.org/docs/testing-recipes.html#mocking-modules). + + + +### More Examples + + + +As a reference implementation I've put working examples here: +https://phabricator.internmc.facebook.com/diffusion/FBS/browse/master/xplat/js/RKJSModules/Libraries/Relay/oss/relay-test-utils/__tests__/RelayMockEnvironmentWithComponents-test.js + + + + + +The best source of example tests is in [the relay-experimental package](https://github.com/facebook/relay/tree/main/packages/relay-experimental/__tests__). + + + +Testing is good. You should definitely do it. + + diff --git a/website/versioned_docs/version-v15.0.0/guides/testing-relay-with-preloaded-queries.md b/website/versioned_docs/version-v15.0.0/guides/testing-relay-with-preloaded-queries.md new file mode 100644 index 0000000000000..3dba45a6bcb97 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guides/testing-relay-with-preloaded-queries.md @@ -0,0 +1,163 @@ +--- +id: testing-relay-with-preloaded-queries +title: Testing Relay with Preloaded Queries +slug: /guides/testing-relay-with-preloaded-queries/ +description: Relay guide to testing with preloaded queries +keywords: +- testing +- preloaded +- usePreloadedQuery +- queueOperationResolver +- queuePendingOperation +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly, OssOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +Components that use preloaded queries (`useQueryLoader` and `usePreloadedQuery` hooks) require slightly different and more convoluted test setup. + +In short, there are two steps that need to be performed **before rendering the component** + +1. Configure the query resolver to generate the response via `environment.mock.queueOperationResolver` +2. Record a pending queue invocation via `environment.mock.queuePendingOperation` + +## Symptoms that something is wrong + +1. The test doesn't do what is expected from it. +2. The query seems to be blocking instead of executing + 1. E.g. the `Suspend` doesn't switch from "waiting" to "data loaded" state +3. If you add the `console.log` before and after `usePreloadedQuery`, only the "before" call is hit + +## TL;DR + +```javascript +const {RelayEnvironmentProvider} = require('react-relay'); +const { MockPayloadGenerator, createMockEnvironment } = require('relay-test-utils'); +const {render} = require('testing-library-react'); +// at the time of writing, act is not re-exported by our internal testing-library-react +// but is re-exported by the "external" version +const {act} = require('ReactTestUtils'); +test("...", () => { + // arrange + const environment = createMockEnvironment(); + environment.mock.queueOperationResolver(operation => { + return MockPayloadGenerator.generate(operation, { + CurrencyAmount() { + return { + formatted_amount: "1234$", + }; + }, + }); + }); + const query = YourComponentGraphQLQueryGoesHere; // can be the same, or just identical + const variables = { + // ACTUAL variables for the invocation goes here + }; + environment.mock.queuePendingOperation(YourComponentGraphQLQuery, variables); + + // act + const {getByTestId, ..otherStuffYouMightNeed} = render( + + + + ); + // trigger the loading - click a button, emit an event, etc. or ... + act(() => jest.runAllImmediates()); // ... if loadQuery is in the useEffect() + // assert + // your assertions go here +}); +``` + +### Configure the query resolver to generate the response + +This is done via `environment.mock.queueOperationResolver(operation)` call, but getting it right might be tricky. + +The crux of this call is to return a mocked graphql result in a very particular format (as `MockResolvers` type, to be precise). This is done via a second parameter to `generate` - it is an object, whose keys are GraphQL types that we want to mock. (See [`mock-payload-generator`](../testing-relay-components/#mock-payload-generator-and-the-relay_test_operation-directive)). + +Continuing on the above example: + +```js +return MockPayloadGenerator.generate(operation, { + CurrencyAmount() { // <-- the GraphQL type + return { + formatted_amount: "response_value" <-- CurrencyAmount fields, selected in the query + }; + } +}); +``` +The tricky thing here is to obtain the name of the GraphQL type and fields to return. This can be done in two ways: + +* Call `console.log(JSON.stringify(operation, null, 2))` and look for the `concreteType` that corresponds to what we want to mock. Then look at the sibling `selections` array, which describes the fields that are selected from that object. + + + +* This is somewhat intense - P139017123 is the output for [this query](https://fburl.com/diffusion/irqurgj9). Rule of thumb - one nested call in the query produces one nested object in the output. +* Look up the type in the graphiql (bunnylol graphiql), then specify the fields listed on the query. + +:::note +The type you need seems to be the type returned by the *innermost function call* (or calls, if you have multiple functions called in one query - see D23078476). This needs to be confirmed - in both example diffs the target types was also leafs. +::: + + + + +It is **possible** to return different data for different query variables via [Mock Resolver Context](../testing-relay-components/#mock-resolver-context). The query variables will be available on the `context.args`, but only to the *innermost function call* (for the query above, only `offer_ids` are available) + +```javascript +CurrencyAmount(context) { + console.log(JSON.stringify(context, null, 2)); // <-- + return { formatted_amount: mockResponse } +} +// <-- logs { ...snip..., "name": "subtotal_price_for_offers", args: { offer_ids: [...] } } +``` +### Record a pending queue invocation + +This is more straightforward - it is done via a call to `environment.mock.queuePendingOperation(query, variables)` + +* `Query` needs to match the query issues by the component. Simplest (and most robust against query changes) is to export the query from the component module and use it in the test, but having an *identical* (but not the same) query works as well. +* `variables` has to match the variables that will be used in this test invocation. + * Beware of nested objects and arrays - they are compared via `areEqual` ([invocation code](https://github.com/facebook/relay/blob/046f758c6b411608371d4cc2f0a594ced331864e/packages/relay-test-utils/RelayModernMockEnvironment.js#L233)) + * Arrays are compared by values (not by reference), but the order of elements matter + * Nested objects - performs deep compare, order of keys is not relevant (this is not confirmed - please update this doc if you used a graphql query with "deep" structure*)* + + + +### Example diffs + +* [D23078476](https://internalfb.com/intern/diff/D23078476) +* [D23101739](https://www.internalfb.com/diff/D23101739) + + + +## Troubleshooting + +* `console.log`, `console.log` everywhere! Recommended places: + * component: before and after `useQueryLoader, usePreloadedQuery, loadQuery` + * test: in `queueOperationResolver` callback + * library: in `RelayModernMockEnvironment.execute`, after the `const currentOperation = ...` call ([here](https://github.com/facebook/relay/blob/046f758c6b411608371d4cc2f0a594ced331864e/packages/relay-test-utils/RelayModernMockEnvironment.js#L230)) +* If `loadQuery` is not called - make sure to issue the triggering event. Depending on your component implementation it could be a user-action (like button click or key press), javascript event (via event emitter mechanisms) or a simple "delayed execution" with `useEffect`. + * The `useEffect` case is probably easiest to miss - make sure to call `act(() => jest.runAllImmediates())` **after** rendering the component +* If "before" `usePreloadedQuery` is hit, but "after" is not - the query suspends. This entire guide is written to resolve it - you might want to re-read it. But most likely it is either: + * Used a different query - the query resolver would not be called, `currentOperation` will be `null` + * Query variables don't match - the query resolver would not be called, `currentOperation` will be `null` (make sure to inspect the `variables`). + * Also, make sure arrays are in the same order, if any (or better yet, use sets, if at all possible). +* If data returned rom the query is not what you expect, make sure you're generating the right graphql type. + * You can tell you're mocking the wrong one if the return values look something like `` + + +:::note +Make sure the component and the test use the same environment (i.e. there's no `` somewhere nested in your test React tree. +::: + + +## Epilogue + +Examples here use `testing-library-react`, but it works with the `react-test-renderer` as well. + + + +See [D23078476](https://www.internalfb.com/diff/D23078476). + + + + diff --git a/website/versioned_docs/version-v15.0.0/guides/type-emission.md b/website/versioned_docs/version-v15.0.0/guides/type-emission.md new file mode 100644 index 0000000000000..3171df6369ca3 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/guides/type-emission.md @@ -0,0 +1,414 @@ +--- +id: type-emission +title: Type Emission +slug: /guides/type-emission/ +description: Relay guide to type emission +keywords: +- type emission +--- + +import DocsRating from '@site/src/core/DocsRating'; +import {FbInternalOnly, OssOnly, fbContent} from 'docusaurus-plugin-internaldocs-fb/internal'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +As part of its normal work, the [**Relay Compiler**](../compiler) will emit type information for your language of choice that helps you write type-safe application code. These types are included in the artifacts that `relay-compiler` generates to describe your operations and fragments. + +## Operation variables + +The shape of the variables object used for query, mutation, or subscription operations. + +In this example the emitted type-information would require the variables object to contain an `artistID` key with a non-null string. + + + + +```javascript +/** + * export type ExampleQuery$variables = { + * +artistID: string, + * } + * export type ExampleQuery$data = { + * +artist: { + * +name: ?string, + * } + * } + * export type ExampleQuery = { + * +variables: ExampleQuery$variables, + * +response: ExampleQuery$data, + * } + */ + +const data = useLazyLoadQuery( + graphql` + query ExampleQuery($artistID: ID!) { + artist(id: $artistID) { + name + } + } + `, + // variables are expected to be of type ExampleQuery$variables + {artistID: 'banksy'}, +); +``` + + + + + +```javascript +/** + * export type ExampleQuery$variables = { + * readonly artistID: string + * } + * export type ExampleQuery$data = { + * readonly artist?: { + * readonly name?: string + * } + * } + * export type ExampleQuery = { + * readonly variables: ExampleQuery$variables + * readonly response: ExampleQuery$data + * } + */ +const data = useLazyLoadQuery( + graphql` + query ExampleQuery($artistID: ID!) { + artist(id: $artistID) { + name + } + } + `, + // variables are expected to be of type ExampleQuery$variables + {artistID: 'banksy'}, +); +``` + + + + +## Operation and fragment data + +The shape of the data selected in a operation or fragment, following the [data-masking] rules. That is, excluding any data selected by fragment spreads. + +In this example the emitted type-information describes the response data which is returned by `useLazyLoadQuery` (or `usePreloadedQuery`). + + + + +```javascript +/** + * export type ExampleQuery$variables = { + * +artistID: string, + * } + * export type ExampleQuery$data = { + * +artist: { + * +name: ?string, + * } + * } + * export type ExampleQuery = { + * +variables: ExampleQuery$variables, + * +response: ExampleQuery$data, + * } + */ + +// data is of type ExampleQuery$data +const data = useLazyLoadQuery( + graphql` + query ExampleQuery($artistID: ID!) { + artist(id: $artistID) { + name + } + } + `, + {artistID: 'banksy'}, +); + +return props.artist &&
{props.artist.name} is great!
+``` + +
+ + + +```javascript +/** + * export type ExampleQuery$variables = { + * readonly artistID: string + * } + * export type ExampleQuery$data = { + * readonly artist?: { + * readonly name?: string + * } + * } + * export type ExampleQuery = { + * readonly variables: ExampleQuery$variables + * readonly response: ExampleQuery$data + * } + */ + +// data is of type ExampleQuery$data +const data = useLazyLoadQuery( + graphql` + query ExampleQuery($artistID: ID!) { + artist(id: $artistID) { + name + } + } + `, + {artistID: 'banksy'}, +); + +return props.artist &&
{props.artist.name} is great!
+``` + +
+
+ + +Similarly, in this example the emitted type-information describes the type of the prop to match the type of the fragment reference `useFragment` expects to receive. + + + + +```javascript +/** + * export type ExampleFragmentComponent_artist$data = { + * +name: string + * } + * + * export type ExampleFragmentComponent_artist$key = { ... } + */ + +import type { ExampleFragmentComponent_artist$key } from "__generated__/ExampleFragmentComponent_artist.graphql" + +type Props = { + artist: ExampleFragmentComponent_artist$key, +}; + +export default ExampleFragmentComponent(props) { + // data is of type ExampleFragmentComponent_artist$data + const data = useFragment( + graphql` + fragment ExampleFragmentComponent_artist on Artist { + biography + } + `, + props.artist, + ); + + return
About the artist: {props.artist.biography}
; +} +``` + +
+ + + +```javascript +/** + * export type ExampleFragmentComponent_artist$data = { + * readonly name: string + * } + * + * export type ExampleFragmentComponent_artist$key = { ... } + */ + +import { ExampleFragmentComponent_artist$key } from "__generated__/ExampleFragmentComponent_artist.graphql" + +interface Props { + artist: ExampleFragmentComponent_artist$key, +}; + +export default ExampleFragmentComponent(props: Props) { + // data is of type ExampleFragmentComponent_artist$data + const data = useFragment( + graphql` + fragment ExampleFragmentComponent_artist on Artist { + biography + } + `, + props.artist, + ); + + return
About the artist: {props.artist.biography}
; +} +``` + +
+
+ +## Fragment references + +The opaque identifier described in [data-masking] that a child container expects to receive from its parent, which represents the child container’s fragment spread inside the parent’s fragment. + + + +:::important +Please read [this important caveat](#single-artifact-directory) about actually enabling type-safe fragment reference checking. +::: + + + +Consider a component that [composes](../../guided-tour/rendering/fragments/#composing-fragments) the above fragment component example. In this example, the emitted type-information of the child component receives a unique opaque identifier type, called a fragment reference, which the type-information emitted for the parent’s fragment references in the location where the child’s fragment is spread. Thus ensuring that the child’s fragment is spread into the parent’s fragment _and_ the correct fragment reference is passed to the child component at runtime. + + + + +```javascript +import { ExampleFragmentComponent } from "./ExampleFragmentComponent" + +/** + * import type { ExampleFragmentComponent_artist$fragmentType } from "ExampleFragmentComponent_artist.graphql"; + * + * export type ExampleQuery$data = { + * +artist: ?{ + * +name: ?string, + * +$fragmentSpreads: ExampleFragmentComponent_artist$fragmentType, + * } + * }; + * export type ExampleQuery$variables = { + * +artistID: string, + * } + * export type ExampleQuery = { + * +variables: ExampleQuery$variables, + * +response: ExampleQuery$data, + * } + */ + +// data is of type ExampleQuery$data +const data = useLazyLoadQuery( + graphql` + query ExampleQuery($artistID: ID!) { + artist(id: $artistID) { + name + ...ExampleFragmentComponent_artist + } + } + `, + {artistID: 'banksy'}, +); + +// Here only `data.artist.name` is directly visible, +// the marker prop $fragmentSpreads indicates that `data.artist` +// can be used for the component expecting this fragment spread. +return ; +``` + + + + + +```javascript +import { ExampleFragmentComponent } from "./ExampleFragmentComponent" + +/** + * import { ExampleFragmentComponent_artist$fragmentType } from "ExampleFragmentComponent_artist.graphql"; + * + * export type ExampleQuery$data = { + * readonly artist?: { + * readonly name: ?string, + * readonly " $fragmentSpreads": ExampleFragmentComponent_artist$fragmentType + * } + * } + * export type ExampleQuery$variables = { + * readonly artistID: string + * } + * export type ExampleQuery = { + * readonly variables: ExampleQuery$variables + * readonly response: ExampleQuery$data + * } + */ + +// data is of type ExampleQuery$data +const data = useLazyLoadQuery( + graphql` + query ExampleQuery($artistID: ID!) { + artist(id: $artistID) { + name + ...ExampleFragmentComponent_artist + } + } + `, + {artistID: 'banksy'}, +); + +// Here only `data.artist.name` is directly visible, +// the marker prop $fragmentSpreads indicates that `data.artist` +// can be used for the component expecting this fragment spread. +return ; +``` + + + + + + +## Single artifact directory + +An important caveat to note is that by default strict fragment reference type-information will _not_ be emitted, instead they will be typed as `any` and would allow you to pass in any data to the child component. + +To enable this feature, you will have to tell the compiler to store all the artifacts in a single directory, by specifing the `artifactDirectory` in the +compiler configuration: + +``` +{ + // package.json + "relay": { + "artifactDirectory": "./src/__generated__", + ... + }, + ... +} +``` + +…and additionally inform the babel plugin in your `.babelrc` config where to look for the artifacts: + +```json +{ + "plugins": [ + ["relay", { "artifactDirectory": "./src/__generated__" }] + ] +} +``` + +It is recommended to alias this directory in your module resolution configuration such that you don’t need to specify relative paths in your source files. This is what is also done in the above examples, where artifacts are imported from a `__generated__` alias, rather than relative paths like `../../../../__generated__`. + +### Background information + +The reason is that `relay-compiler` and its artifact emission is stateless. Meaning that it does not keep track of locations of original source files and where the compiler previously saved the accompanying artifact on disk. Thus, if the compiler were to emit artifacts that try to import fragment reference types from _other_ artifacts, the compiler would: + +- first need to know where on disk that other artifact exists; +- and update imports when the other artifact changes location on disk. + +Facebook uses a module system called [Haste], in which all source files are considered in a flat namespace. This means that an import declaration does not need to specify the path to another module and thus there is no need for the compiler to ever consider the above issues. I.e. an import only needs to specify the basename of the module filename and Haste takes care of actually finding the right module at import time. Outside of Facebook, however, usage of the Haste module system is non-existent nor encouraged, thus the decision to not import fragment reference types but instead type them as `any`. + +At its simplest, we can consider Haste as a single directory that contains all module files, thus all module imports always being safe to import using relative sibling paths. This is what is achieved by the single artifact directory feature. Rather than co-locating artifacts with their source files, all artifacts are stored in a single directory, allowing the compiler to emit imports of fragment reference types. + + + +[data-masking]: ../../principles-and-architecture/thinking-in-relay#data-masking + +[Haste]: https://twitter.com/dan_abramov/status/758655309212704768 + + diff --git a/website/versioned_docs/version-v15.0.0/home.md b/website/versioned_docs/version-v15.0.0/home.md new file mode 100644 index 0000000000000..77e006c7a0b04 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/home.md @@ -0,0 +1,65 @@ +--- +id: home +title: Home +slug: / +description: Relay documentation landing page +keywords: +- relay +- graphql +- data +- introduction +- home +--- + +# Relay Docs + +import DocsRating from '@site/src/core/DocsRating'; +import {OssOnly, FbInternalOnly} from 'docusaurus-plugin-internaldocs-fb/internal'; + +Relay is a data management library for React that lets you fetch and update data with GraphQL. It embodies years of learning to give you **outstanding performance by default** while keeping your code **stable and maintainable**. + +Relay brings the composability of React components to data fetching. Each component declares its own data needs, and Relay combines them into efficient pre-loadable queries. Every aspect of its design is to make the natural way of writing components also the most performant. + +## Features + +* Declarative data: Just declare what data each component needs and Relay will handle the loading states. +* Co-location and composability: Each component declares its own data needs; Relay combines them into efficient queries. When you re-use a component on a different screen, your queries are automatically updated. +* Pre-fetching: Relay analyses your code so you can start fetching queries before your code even downloads or runs. +* UI patterns: Relay implements loading states, pagination, refetching, optimistic updates, rollbacks, and other common UI behaviors that are tricky to get right. +* Consistent updates: Relay maintains a normalized data store, so components that observe the same data stay in sync even if they reach it by different queries. +* Streaming and deferred data: Declaratively defer parts of your query and Relay will progressively re-render your UI as the data streams in. +* Great developer experience: Relay provides autocompletion and go-to-definition for your GraphQL schema. +* Type safety: Relay generates type definitions so that mistakes are caught statically, not at runtime. +* Manage local data: Use the same API for server data and local client state. +* Hyper-optimized runtime: Relay is relentlessly optimized. Its JIT-friendly runtime processes incoming data faster by statically determining what payloads to expect. + +## Stack + +Relay works on the Web and on React Native — it is used extensively at Meta in both environments. It is framework-agnostic and works with Next, React Router, Create React App, etc. It works with both TypeScript and Flow. + +Relay is completely tied to GraphQL, so if you cannot use GraphQL then it's not the right choice for you. + +Relay has a UI-agnostic layer that fetches and manages data, and a React-specific layer that handles loading states, pagination, and other UI paradigms. It is mainly supported when used with React, although you can access your Relay data outside of React if you need to. The React-specific parts of Relay are based on Suspense, so there are some limitations if you're stuck on an older version of React. + +## Where to Go from Here + + + +
+Start with the tutorial — it will take you step-by-step through building a Relay app. +
+ + +- An overview of the **[prerequisites](./getting-started/prerequisites/)** for using Relay, and an **[installation and setup guide](./getting-started/installation-and-setup/)**. +- The **[API reference](./api-reference/relay-environment-provider/)**, for a reference of our APIs including a detailed overview of their inputs and outputs. + +
+ + + +- Start with the **[tutorial](./tutorial/intro/)** — it will take you step-by-step through building a Relay app. +- The **[API reference](./api-reference/relay-environment-provider/)**, for a reference of our APIs including a detailed overview of their inputs and outputs. + + + + diff --git a/website/versioned_docs/version-v15.0.0/migration-and-compatibility/relay-hooks-and-legacy-container-apis.md b/website/versioned_docs/version-v15.0.0/migration-and-compatibility/relay-hooks-and-legacy-container-apis.md new file mode 100644 index 0000000000000..fc9d1dc248aea --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/migration-and-compatibility/relay-hooks-and-legacy-container-apis.md @@ -0,0 +1,564 @@ +--- +id: relay-hooks-and-legacy-container-apis +title: Relay Hooks and Legacy Container APIs +slug: /migration-and-compatibility/relay-hooks-and-legacy-container-apis/ +description: Relay guide to compatibility between hooks and containers +keywords: +- migration +- compatibility +- container +- QueryRenderer +- FragmentContainer +- RefetchContainer +- PaginationContainer +--- + +import DocsRating from '@site/src/core/DocsRating'; + +## Compatibility between Relay Hooks and Containers + +Relay Hooks are fully compatible with Relay's [container-based APIs](../../api-reference/legacy-apis/), meaning that containers can render components that use Hooks, and vice-versa. + +This means that you can adopt Relay Hooks incrementally, either by using them exclusively for new code, or by migrating specific parts of your app, without affecting the rest of your existing application. + + +## Migrating existing container-based code + +As we've mentioned, migrating existing code to Relay Hooks is ***not*** required, and **container-based code will continue to work**. + +However, in this section we will go over common migration patterns you can follow if you do choose to migrate container-based code to Relay Hooks. + + +### `QueryRenderer` → `useLazyLoadQuery` + +Converting from a `QueryRenderer` to the [`useLazyLoadQuery`](../../api-reference/use-lazy-load-query/) Hook is the most straightforward conversion, and will have a similar behavior of fetching the specified query *during render.* + +To convert a `QueryRenderer` to `useLazyLoadQuery`, you need to take the following steps: + +1. Render a [`RelayEnvironmentProvider`](../../api-reference/relay-environment-provider/) where the QueryRenderer was, or above it. Usually, we recommend rendering the `RelayEnvironmentProvider` at the very root of your app: + +```js + + + +``` + + +2. Convert the `QueryRenderer` into `useLazyLoadQuery`: + +**Before:** + +```js +import * as React from 'React'; +import {graphql, QueryRenderer} from 'react-relay'; + +export default function Home() { + return ( + { + if (error) { + return ; + } + if (!props) { + return ; + } + return

{props.user?.name}

+ }} + /> + ); +} +``` + + +**After:** +Fetch and render the query: + +```js +import * as React from 'React'; +import {graphql, useLazyLoadQuery} from 'react-relay'; + +export default function Home() { + const data = useLazyLoadQuery( + graphql` + query HomeQuery($id: ID!) { + user(id: $id) { + name + } + } + `, + {id: 4}, + ); + + return

{data.user?.name}

; +} +``` + +[Loading states](../../guided-tour/rendering/loading-states/) and [error states](../../guided-tour/rendering/error-states/) are handled by Suspense and Error Boundaries: + +```js + + }> + + + +``` + + + +### `QueryRenderer` → `useQueryLoader` + `usePreloadedQuery` + +Unlike `useLazyLoadQuery`, using [`useQueryLoader`](../../api-reference/use-query-loader/) in combination with [`usePreloadedQuery`](../../api-reference/use-preloaded-query/) will start fetching the data *ahead* of render, following the "render-as-you-fetch" pattern. This means that the data fetch will start sooner, and potentially speed up the time it takes to show content to users. + +To make best use of this pattern, query loading is usually integrated at the router level, or other parts of your UI infra. To see a full example, see our [`issue-tracker`](https://github.com/relayjs/relay-examples/blob/main/issue-tracker/src/routes.js) example app. + + +To convert a `QueryRenderer` to `useQueryLoader`, you need to take the following steps: + +1. Render a [`RelayEnvironmentProvider`](../../api-reference/relay-environment-provider/) where the QueryRenderer was, or above it. Usually, we recommend rendering the `RelayEnvironmentProvider` at the very root of your app: + +```js + + + +``` + +2. Convert the `QueryRenderer` into `useQueryLoader` + `usePreloadedQuery`: + +**Before:** + +```js +import * as React from 'React'; +import {graphql, QueryRenderer} from 'react-relay'; + +export default function UserPopover() { + return ( + { + if (error) { + return ; + } + if (!props) { + return ; + } + return

{props.user?.name}

+ }} + /> + ); +} +``` + + +**After:** +Render the preloaded query: + +```js +import * as React from 'React'; +import {graphql, usePreloadedQuery} from 'react-relay'; + +export default function UserPopover(props) { + const data = usePreloadedQuery( + graphql` + query UserPopoverQuery($id: ID!) { + user(id: $id) { + name + } + } + `, + props.queryRef, + ); + + return

{data.user?.name}

; +} +``` + + +Load the query with `loadQuery` from `useQueryLoader`. This part of the code would usually be integrated in your routing, or other parts of your UI infra: + +```js +import * as React from 'React'; +import {useQueryLoader} from 'react-relay'; + +// Import the query defined in the UserPopover component +import UserPopoverQuery from '__generated__/UserPopoverQuery.graphql'; + +// This is *NOT* a real-world example, only used +// to illustrate usage. + +export default function UserPopoverButton(props) { + const [queryRef, loadQuery] = useQueryLoader(UserPopoverQuery) + + const handleClick = useCallback(() => { + // Load the query in the event handler, onClick + loadQuery({id: props.userID}) + }, [loadQuery, props.userID]); + + return ( + <> + + + ); +} + +export default createRefetchContainer( + CommentBody, + { + user: graphql` + fragment CommentBody_comment on Comment { + body(lang: $lang) { + text + } + } + `, + }, + + // This option is no longer required, the refetch query + // will automatically be generated by Relay using the @refetchable + // directive. + graphql` + query AppQuery($id: ID!, lang: Lang) { + node(id: $id) { + ...CommentBody_comment + } + } + `, +); +``` + +**After:** + +```js +import * as React from 'React'; +import {graphql, useRefetchableFragment} from 'react-relay'; + +export default function CommentBody(props: Props) { + const [data, refetch] = useRefetchableFragment( + graphql` + fragment CommentBody_comment on Comment + @refetchable(queryName: "CommentBodyRefetchQuery") { + body(lang: $lang) { + text + } + } + `, + props.comment, + ); + + const handleClick = useCallback(() => { + refetch({lang: 'SPANISH'}); + }, [refetch]); + + return ( + <> +

{data.body?.text}

+ + + ); +} +``` + + + +### Pagination Container → `usePaginationFragment` + +The pagination API for [`usePaginationFragment`](../../api-reference/use-pagination-fragment/) has been greatly simplified and reduced compared to the former PaginationContainer. Migration will require mapping inputs into the new API. + +**Before:** + +```js +import * as React from 'React'; +import {graphql, createPaginationContainer} from 'react-relay'; + +class UserContainerComponent extends React.Component { + render(): React.Node { + const isLoading = this.props.relay.isLoading() || this.state.loading; + const hasMore = this.props.relay.hasMore(); + + return ( + <> + + + + ); + } + + loadMore() { + if ( + !this.props.relay.hasMore() || + this.props.relay.isLoading() || + this.state.loading + ) { + return; + } + + this.setState({loading: true}); + + this.props.relay.loadMore(5, () => this.setState({loading: false})); + } +} + +export default createPaginationContainer( + UserContainerComponent, + { + user: graphql` + fragment UserContainerComponent_user on User + @argumentDefinitions(count: {type: "Int!"}, cursor: {type: "ID"}) + @refetchable(queryName: "UserComponentRefetchQuery") { + friends(first: $count, after: $cursor) + @connection(key: "UserComponent_user_friends") { + edges { + node { + name + } + } + } + } + `, + }, + { + // This option is no longer necessary, usePaginationFragment supports + // bi-directional pagination out of the box. + direction: 'forward', + + // This option is no longer required, and will be automatically + // determined by usePaginationFragment + getConnectionFromProps(props: Props) { + return props.user?.friends; + }, + + // This option is no longer required, and will be automatically + // determined by usePaginationFragment + getFragmentVariables(vars, count) { + return {...vars, count}; + }, + + // This option is no longer required, and will be automatically + // determined by usePaginationFragment + getVariables(props: Props, {count, cursor}) { + return { + cursor, + count, + }; + }, + + // This option is no longer required, the pagination query + // will automatically be generated by Relay using the @refetchable + // directive. + query: graphql` + query UserContainerComponentQuery { + viewer { + actor { + ... on User { + ...UserContainerComponent_user @arguments(count: 10) + } + } + } + } + `, + }, +); +``` + + +**After:** + +```js +import * as React from 'React'; +import {graphql, usePaginationFragment} from 'react-relay'; + +export default function UserComponent(props: Props) { + const {data, loadNext, hasNext, isLoadingNext} = usePaginationFragment( + graphql` + fragment UserComponent_user on User + @refetchable(queryName: "UserComponentRefetchQuery") { + friends(first: $count, after: $after) + @connection(key: "UserComponent_user_friends") { + edges { + node { + name + } + } + } + } + `, + props.user, + ); + + const handleClick = useCallback(() => { + loadNext(5) + }, [loadNext]) + + return ( + <> + + + + ); +} +``` + + + + +* * * + +### QueryRenderer → useEntryPointLoader + EntryPointContainer + +TODO + + + +### commitMutation → useMutation + +TODO + + +### requestSubscription → useSubscription + +TODO + + diff --git a/website/versioned_docs/version-v15.0.0/migration-and-compatibility/suspense-compatibility.md b/website/versioned_docs/version-v15.0.0/migration-and-compatibility/suspense-compatibility.md new file mode 100644 index 0000000000000..75930e4f02b52 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/migration-and-compatibility/suspense-compatibility.md @@ -0,0 +1,36 @@ +--- +id: suspense-compatibility +title: Suspense Compatibility +slug: /migration-and-compatibility/suspense-compatibility/ +description: Relay guide to suspense compatibility +keywords: +- suspense +- container +--- + +import DocsRating from '@site/src/core/DocsRating'; + +## What about Suspense? + +Relay Hooks uses React Suspense for [specifying loading states](../../guided-tour/rendering/loading-states/), so you might be wondering: Why is that the case if Suspense for Data Fetching is still not supported? Does this mean that Suspense for Data Fetching is officially supported now in React 17? + +## Is Suspense for Data Fetching ready yet? + +The short answer is: **NO**. + +**Support, general guidance, and requirements for usage of Suspense for Data Fetching are still not ready**, and the React team is still defining what this guidance will be for upcoming React releases. + +With that said, even though there are still things to figure out before Suspense for Data Fetching can be broadly implemented and adopted, we released Relay Hooks on React 17 for a few reasons: + +* Relay was a very early adopter of Suspense, and collaborated with React on the research of Suspense for Data Fetching. It was one of the first testing grounds for using Suspense in production, and helped inform some of its design decisions. As such, there are still parts of our Suspense *implementation* that reflect those early learnings (which aren't yet fully documented) and which aren't quite where we want them to be. Although we know there are still likely changes to be made in the implementation, and that there will be some limitations when Suspense is used in React 17, we know Relay Hooks are on the right trajectory for upcoming releases of React, and those changes can be streamlined and allow us to release Relay Hooks a bit earlier. +* The Relay Hooks APIs represent the APIs we want to deliver long-term for Relay and which we believe are an improvement over our previous APIs. Even though their underlying implementation is still changing and will likely change more as the Suspense for Data Fetching guidance is documented and finalized by the React team, the Relay Hooks APIs themselves are stable. They have been widely adopted internally at Facebook, and have been in use in production for over a year, so we are confident that they work. We want to allow the community to start adopting them, and be able to get external feedback from the community as well. + + +## What does it mean for me if I start using Relay Hooks in React 17? + +What this means for users adopting Relay Hooks is: + +* There will be some limitations when using Suspense in React 17, which we've documented in [our docs](../../guided-tour/refetching/refetching-queries-with-different-data/#if-you-need-to-avoid-suspense). Specifically, the current release includes a subset of features that work with both synchronous rendering and concurrent rendering. In order to fully support Suspense for Data Fetching, we also need features such as concurrently rendering suspended trees, and transitioning to new trees when data is refetched. The APIs we've currently released will allow us to support concurrent rendering with the same APIs in future versions of React. +* When a future version of React is released that fully supports concurrent rendering and Suspense for Data Fetching, Relay will also make a new major release alongside the React release. That release will likely include breaking changes that we will document for the upgrade. + + diff --git a/website/versioned_docs/version-v15.0.0/migration-and-compatibility/upgrading-to-relay-hooks.md b/website/versioned_docs/version-v15.0.0/migration-and-compatibility/upgrading-to-relay-hooks.md new file mode 100644 index 0000000000000..f9619a27907bb --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/migration-and-compatibility/upgrading-to-relay-hooks.md @@ -0,0 +1,38 @@ +--- +id: upgrading-to-relay-hooks +title: Upgrading to Relay Hooks +slug: /migration-and-compatibility/ +description: Relay guide to upgrading to Relay hooks +keywords: +- upgrade +- hooks +--- + +[Relay Hooks](/blog/2021/03/09/introducing-relay-hooks) is a set of new Hooks-based APIs for using Relay with React that improves upon the existing container-based APIs. + +In this we will cover how to start using Relay Hooks, what you need to know about compatibility, and how to migrate existing container-based code to Hooks if you choose to do so. However, note that migrating existing code to Relay Hooks is ***not*** required, and **container-based code will continue to work**. + +## Accessing Relay Hooks + +Make sure the latest versions of React and Relay are installed, and that you’ve followed additional setup in our [Installation & Setup](../getting-started/installation-and-setup/) guide: + +``` +yarn add react react-dom react-relay +``` + +Then, you can import Relay Hooks from the **`react-relay`** module, or if you only want to include Relay Hooks in your bundle, you can import them from **`react-relay/hooks`**: + +```js +import {graphql, useFragment} from 'react-relay'; // or 'react-relay/hooks' + +// ... +``` + +## Next Steps + +Check out the following guides in this section: +* [Suspense Compatibility](./suspense-compatibility/) +* [Relay Hooks and Legacy Container APIs](./relay-hooks-and-legacy-container-apis/) + + +For more documentation on the APIs themselves, check out our [API Reference](../api-reference/relay-environment-provider) or our [Guided Tour](../guided-tour/). diff --git a/website/versioned_docs/version-v15.0.0/principles-and-architecture/architecture-overview.md b/website/versioned_docs/version-v15.0.0/principles-and-architecture/architecture-overview.md new file mode 100644 index 0000000000000..fdf24c7641935 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/principles-and-architecture/architecture-overview.md @@ -0,0 +1,24 @@ +--- +id: architecture-overview +title: Architecture Overview +slug: /principles-and-architecture/architecture-overview/ +description: Relay architecture overview guide +keywords: +- architecture +--- + +import DocsRating from '@site/src/core/DocsRating'; + +This document, together with [Runtime Architecture](../runtime-architecture/) and [Compiler Architecture](../compiler-architecture/), describes the high-level architecture of Relay. The intended audience includes developers interested in contributing to Relay, developers hoping to utilize the building blocks of Relay to create higher-level APIs, and anyone interested in understanding more about Relay internals. For developers wanting to learn more about _using_ Relay to build products, the [Guided Tour](../../guided-tour/) is the best resource. + +## Core Modules + +Relay is composed of three core parts: + +- **Relay Compiler:** A GraphQL to GraphQL optimizing _compiler_, providing general utilities for transforming and optimizing queries as well as generating build artifacts. A novel feature of the compiler is that it facilitates experimentation with new GraphQL features - in the form of custom directives - by making it easy to translate code using these directives into standard, spec-compliant GraphQL. +- **Relay Runtime:** A full-featured, high-performance GraphQL _runtime_ that can be used to build higher-level client APIs. The runtime features a normalized object cache, optimized "write" and "read" operations, a generic abstraction for incrementally fetching field data (such as for pagination), garbage collection for removing unreferenced cache entries, optimistic mutations with arbitrary logic, support for building subscriptions and live queries, and more. +- **React/Relay:** A high-level _product API_ that integrates the Relay Runtime with React. This is the primary public interface to Relay for most product developers, featuring APIs to fetch the data for a query or define data dependencies for reusable components (e.g. `useFragment`). + +Note that these modules are _loosely coupled_. For example, the compiler emits representations of queries in a well-defined format that the runtime consumes, such that the compiler implementation can be swapped out if desired. React/Relay relies only on the well-documented public interface of the runtime, such that the actual implementation can be swapped out. We hope that this loose coupling will allow the community to explore new use-cases such as the development of specialized product APIs using the Relay runtime or integrations of the runtime with view libraries other than React. + + diff --git a/website/versioned_docs/version-v15.0.0/principles-and-architecture/compiler-architecture.md b/website/versioned_docs/version-v15.0.0/principles-and-architecture/compiler-architecture.md new file mode 100644 index 0000000000000..f872264b8082c --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/principles-and-architecture/compiler-architecture.md @@ -0,0 +1,106 @@ +--- +id: compiler-architecture +title: Compiler Architecture +slug: /principles-and-architecture/compiler-architecture/ +description: Relay compiler architecture guide +keywords: +- compiler +- architecture +- transform +--- + +import DocsRating from '@site/src/core/DocsRating'; + +The compiler is a set of modules designed to extract GraphQL documents from across a codebase, transform/optimize them, and generate build artifacts. Examples of common types of artifacts include optimized GraphQL to persist to your server, runtime representations of the queries for use with GraphQL clients such as the Relay runtime, or generated source code for use with GraphQL frameworks for compiled languages (Java/Swift/etc). + +## Data Flow + +The high-level flow of data through the compiler is represented in the following diagram: + +``` + + ┌─────────────┐┌─────────────┐ + │ GraphQL ││ Schema │ + └─────────────┘└─────────────┘ + │ │ parse + └───────┬──────┘ + ▼ + ┌────────────────────────────┐ + │ CompilerContext │ + │ │ + │ ┌─────┐ ┌─────┐ ┌─────┐ │──┐ + │ │ IR │ │ IR │ │ ... │ │ │ + │ └─────┘ └─────┘ └─────┘ │ │ + └────────────────────────────┘ │ transform/ + │ │ ▲ │ optimize + │ │ └────────────┘ + │ │ + │ └──────────┐ + │ print │ codegen + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ GraphQL │ │ Artifacts │ + └─────────────┘ └─────────────┘ +``` + +1. GraphQL text is extracted from source files and "parsed" into an intermediate representation (IR) using information from the schema. +2. The set of IR documents forms a CompilerContext, which is then transformed and optimized. +3. Finally, GraphQL is printed (e.g. to files, saved to a database, etc) and any artifacts are generated. + +## Data Types & Modules + +The compiler module is composed of a set of core building blocks as well as a helper that packages them together in an easy to use API. Some of the main data types and modules in the compiler are as follows: + +- `IR` (Intermediate Representation): an (effectively immutable) representation of a GraphQL document (query, fragment, field, etc) as a tree structure, including type information from a schema. Compared to the standard GraphQL AST (produced by e.g. `graphql-js`) the main difference is that it encodes more of the semantics of GraphQL. For example, conditional branches (`@include` and `@skip`) are represented directly, making it easier to target optimizations for these directives (One such optimization is to merge sibling fields with the same condition, potentially reducing the number of conditionals that must be evaluated at runtime). +- `CompilerContext`: an immutable representation of a corpus of GraphQL documents. It contains the schema and a mapping of document names to document representations (as IR, see above). +- `Transform`: a "map"-like function that accepts a `CompilerContext` as input and returns a new, modified context as output. Examples below. +- `Parser`: Converts a GraphQL schema and raw GraphQL text into typed IR objects. +- `Printer`: a function that accepts IR and converts it to a GraphQL string. + +The `RelayCompiler` module is a helper class that demonstrates one way of combining these primitives. It takes IR transforms, and given IR definitions, constructs a CompilerContext from them, transforming them, and generating output artifacts intended for use with Relay runtime. + +## Transforms + +One of the main goals of the compiler is to provide a consistent platform for writing tools that transform or optimize GraphQL. This includes the ability to experiment with new directives by transforming them away at compile time. Transform functions should typically perform a single type of modification - it's expected that an app will have multiple transforms configured in the compiler instance. + +Here are a few examples of some of the included transforms: + +- `FlattenTransform`: Reduces extraneous levels of indirection in a query, inlining fields from anonymous fragments wherever they match the parent type. This can be beneficial when generating code to read the results of a query or process query results, as it reduces duplicate field processing. For example: + +``` +# before: `id` is processed twice +foo { # type FooType + id + ... on FooType { # matches the parent type, so this is extraneous + id + } + } + + # after: `id` is processed once + foo { + id + } +``` + +- `SkipRedundantNodeTransform`: A more advanced version of flattening, this eliminates more complex cases of field duplication such as when a field is fetched both unconditionally and conditionally, or is fetched by two different sub-fragments. For example: + +``` +# before: `id` processed up to 2x +foo { + bar { + id + } + ... on FooType @include(if: $cond) { # can't be flattened due to conditional + id # but this field is guaranteed to be fetched regardless + } +} + +# after: `id` processed at most once +foo { + bar { + id + } +} +``` + + diff --git a/website/versioned_docs/version-v15.0.0/principles-and-architecture/runtime-architecture.md b/website/versioned_docs/version-v15.0.0/principles-and-architecture/runtime-architecture.md new file mode 100644 index 0000000000000..9e1e84abcd706 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/principles-and-architecture/runtime-architecture.md @@ -0,0 +1,249 @@ +--- +id: runtime-architecture +title: Runtime Architecture +slug: /principles-and-architecture/runtime-architecture/ +description: Relay runtime architecture guide +keywords: +- runtime +- architecture +- store +- DataID +- Record +- RecordSource +--- + +import DocsRating from '@site/src/core/DocsRating'; + +The Relay runtime is a full-featured GraphQL client that is designed for high performance even on low-end mobile devices and is capable of scaling to large, complex apps. The runtime API is not intended to be used directly in product code, but rather to provide a foundation for building higher-level product APIs such as React/Relay. This foundation includes: + +- A normalized, in-memory object graph/cache. +- An optimized "write" operation for updating the cache with the results of queries/mutations/subscriptions. +- A mechanism for reading data from the cache and subscribing for updates when these results change due to a mutation, subscription update, etc. +- Garbage collection to evict entries from the cache when they can no longer be referenced by any view. +- A generic mechanism for intercepting data prior to publishing it to the cache and either synthesizing new data or merging new and existing data together (which among other things enables the creation of a variety of pagination schemes). +- Mutations with optimistic updates and the ability to update the cache with arbitrary logic. +- Support for live queries where supported by the network/server. +- Core primitives to enable subscriptions. +- Core primitives for building offline/persisted caching. + +## Data Types + +- `DataID` (type): A globally unique or client-generated identifier for a record, stored as a string. +- `Record` (type): A representation of a distinct data entity with an identity, type, and fields. Note that the actual runtime representation is opaque to the system: all accesses to `Record` objects (including record creation) is mediated through the `RelayModernRecord` module. This allows the representation itself to be changed in a single place (e.g. to use `Map`s or a custom class). It is important that other code does not assume that `Record`s will always be plain objects. +- `RecordSource` (type): A collection of records keyed by their data ID, used both to represent the cache and updates to it. For example the store's record cache is a `RecordSource` and the results of queries/mutations/subscriptions are normalized into `RecordSource`s that are published to a store. Sources also define methods for asynchronously loading records in order to (eventually) support offline use-cases. Currently the only implementation of this interface is `RelayInMemoryRecordSource`; future implementations may add support for loading records from disk. +- `Store` (type): The source of truth for an instance of `RelayRuntime`, holding the canonical set of records in the form of a `RecordSource` (though this is not required). Currently the only implementation is `RelayModernStore`. +- `Network` (type): Provides methods for fetching query data from and executing mutations against an external data source. +- `Environment` (type): Represents an encapsulated environment combining a `Store` and `Network`, providing a high-level API for interacting with both. This is the main public API of `RelayRuntime`. + +Types for working with queries and their results include: + +- `Selector` (type): A selector defines the starting point for a traversal into the graph for the purposes of targeting a subgraph, combining a GraphQL fragment, variables, and the Data ID for the root object from which traversal should progress. Intuitively, this "selects" a portion of the object graph. +- `Snapshot` (type): The (immutable) results of executing a `Selector` at a given point in time. This includes the selector itself, the results of executing it, and a list of the Data IDs from which data was retrieved (useful in determining when these results might change). + +## Data Model + +Relay Runtime is designed for use with GraphQL schemas that describe **object graphs** in which objects have a type, an identity, and a set of fields with values. Objects may reference each other, which is represented by fields whose values are one or more other objects in the graph [1]. To distinguish from JavaScript `Object`s, these units of data are referred to as `Record`s. Relay represents both its internal cache as well as query/mutation/etc results as a mapping of **data ID**s to **records**. The data ID is the unique (with respect to the cache) identifier for a record - it may be the value of an actual `id` field or based on the path to the record from the nearest object with an `id` (such path-based ids are called **client ids**). Each `Record` stores its data ID, type, and any fields that have been fetched. Multiple records are stored together as a `RecordSource`: a mapping of data IDs to `Record` instances. + +For example, a user and their address might be represented as follows: + +``` + +// GraphQL Fragment +fragment on User { + id + name + address { + city + } +} + +// Response +{ + id: '842472', + name: 'Joe', + address: { + city: 'Seattle', + } +} + +// Normalized Representation +RecordSource { + '842472': Record { + __id: '842472', + __typename: 'User', // the type is known statically from the fragment + id: '842472', + name: 'Joe', + address: {__ref: 'client:842472:address'}, // link to another record + }, + 'client:842472:address': Record { + // A client ID, derived from the path from parent & parent's ID + __id: 'client:842472:address', + __typename: 'Address', + city: 'Seattle', + } +} +``` + +[1] Note that GraphQL itself does not impose this constraint, and Relay Runtime may also be used for schemas that do not conform to it. For example, both systems can be used to query a single denormalized table. However, many of the features that Relay Runtime provides, such as caching and normalization, work best when the data is represented as a normalized graph with stable identities for discrete pieces of information. + +### Store Operations + +The `Store` is the source of truth for application data and provides the following core operations. + +- `lookup(selector: Selector): Snapshot`: Reads the results of a selector from the store, returning the value given the data currently in the store. + +- `subscribe(snapshot: Snapshot, callback: (snapshot: Snapshot) => void): Disposable`: Subscribe to changes to the results of a selector. The callback is called when data has been published to the store that would cause the results of the snapshot's selector to change. + +- `publish(source: RecordSource): void`: Update the store with new information. All updates to the store are expressed in this form, including the results of queries/mutation/subscriptions as well as optimistic mutation updates. All of those operations internally create a new `RecordSource` instance and ultimately publish it to the store. Note that `publish()` does _not_ immediately update any `subscribe()`-ers. Internally, the store compares the new `RecordSource` with its internal source, updating it as necessary: + - Records that exist only in the published source are added to the store. + - Records that exist in both are merged into a new record (inputs unchanged), with the result added to the store. + - Records that are null in the published source are deleted (set to null) in the store. + - Records with a special sentinel value are removed from the store. This supports un-publishing optimistically created records. + +- `notify(): void`: Calls any `subscribe()`-ers whose results have changed due to intervening `publish()`-es. Separating `publish()` and `notify()` allows for multiple payloads to be published before performing any downstream update logic (such as rendering). + +- `retain(selector: Selector): Disposable`: Ensure that all the records necessary to fulfill the given selector are retained in-memory. The records will not be eligible for garbage collection until the returned reference is disposed. + +### Example Data Flow: Fetching Query Data + +``` + + ┌───────────────────────┐ + │ Query │ + └───────────────────────┘ + │ + ▼ + ┌ ─ ─ ─ ┐ + fetch ◀────────────▶ Server + └ ─ ─ ─ ┘ + │ + ┌─────┴───────┐ + ▼ ▼ + ┌──────────┐ ┌──────────┐ + │ Query │ │ Response │ + └──────────┘ └──────────┘ + │ │ + └─────┬───────┘ + │ + ▼ + normalize + │ + ▼ + ┌───────────────────────┐ + │ RecordSource │ + │ │ + │┌──────┐┌──────┐┌─────┐│ + ││Record││Record││ ... ││ + │└──────┘└──────┘└─────┘│ + └───────────────────────┘ + +``` + +1. The query is fetched from the network. +2. The query and response are traversed together, extracting the results into `Record` objects which are added to a fresh `RecordSource`. + +This fresh `RecordSource` would then be published to the store: + +``` + + publish + │ + ▼ + ┌───────────────────────────┐ + │ Store │ + │ ┌───────────────────────┐ │ + │ │ RecordSource │ │ + │ │ │ │ + │ │┌──────┐┌──────┐┌─────┐│ │ + │ ││Record││Record││ ... ││ │ <--- records are updated + │ │└──────┘└──────┘└─────┘│ │ + │ └───────────────────────┘ │ + │ ┌───────────────────────┐ │ + │ │ Subscriptions │ │ + │ │ │ │ + │ │┌──────┐┌──────┐┌─────┐│ │ + │ ││ Sub. ││ Sub. ││ ... ││ │ <--- subscriptions do not fire yet + │ │└──────┘└──────┘└─────┘│ │ + │ └───────────────────────┘ │ + └───────────────────────────┘ + +``` + +Publishing the results updates the store but does _not_ immediately notify any subscribers. This is accomplished by calling `notify()`... + +``` + + notify + │ + ▼ + ┌───────────────────────────┐ + │ Store │ + │ ┌───────────────────────┐ │ + │ │ RecordSource │ │ + │ │ │ │ + │ │┌──────┐┌──────┐┌─────┐│ │ + │ ││Record││Record││ ... ││ │ + │ │└──────┘└──────┘└─────┘│ │ + │ └───────────────────────┘ │ + │ ┌───────────────────────┐ │ + │ │ Subscriptions │ │ + │ │ │ │ + │ │┌──────┐┌──────┐┌─────┐│ │ + │ ││ Sub.││ Sub.││ ...││ │ <--- affected subscriptions fire + │ │└──────┘└──────┘└─────┘│ │ + │ └───┼───────┼───────┼───┘ │ + └─────┼───────┼───────┼─────┘ + │ │ │ + ▼ │ │ + callback │ │ + ▼ │ + callback │ + ▼ + callback + +``` + +...which calls the callbacks for any `subscribe()`-ers whose results have changed. Each subscription is checked as follows: + +1. First, the list of data IDs that have changed since the last `notify()` is compared against data IDs listed in the subscription's latest `Snapshot`. If there is no overlap, the subscription's results cannot possibly have changed (if you imagine the graph visually, there is no overlap between the part of the graph that changed and the part that is selected). In this case the subscription is ignored, otherwise processing continues. +2. Second, any subscriptions that do have overlapping data IDs are re-read, and the new/previous results are compared. If the result has not changed, the subscription is ignored (this can occur if a field of a record changed that is not relevant to the subscription's selector), otherwise processing continues. +3. Finally, subscriptions whose data actually changed are notified via their callback. + +### Example Data Flow: Reading and Observing the Store + +Products access the store primarily via `lookup()` and `subscribe()`. Lookup reads the initial results of a fragment, and subscribe observes that result for any changes. Note that the output of `lookup()` - a `Snapshot` - is the input to `subscribe()`. This is important because the snapshot contains important information that can be used to optimize the subscription - if `subscribe()` accepted only a `Selector`, it would have to re-read the results in order to know what to subscribe to, which is less efficient. + +Therefore a typical data flow is as follows - note that this flow is managed automatically by higher-level APIs such as React/Relay. First a component will lookup the results of a selector against a record source (e.g. the store's canonical source): + +``` + + ┌───────────────────────┐ ┌──────────────┐ + │ RecordSource │ │ │ + │ │ │ │ + │┌──────┐┌──────┐┌─────┐│ │ Selector │ + ││Record││Record││ ... ││ │ │ + │└──────┘└──────┘└─────┘│ │ │ + └───────────────────────┘ └──────────────┘ + │ │ + │ │ + └──────────────┬────────────┘ + │ + │ lookup + │ (read) + │ + ▼ + ┌─────────────┐ + │ │ + │ Snapshot │ + │ │ + └─────────────┘ + │ + │ render, etc + │ + ▼ + +``` + +Next, it will `subscribe()` using this snapshot in order to be notified of any changes - see the above diagram for `publish()` and `notify()`. + + diff --git a/website/versioned_docs/version-v15.0.0/principles-and-architecture/thinking-in-graphql.md b/website/versioned_docs/version-v15.0.0/principles-and-architecture/thinking-in-graphql.md new file mode 100644 index 0000000000000..fc787bd18b318 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/principles-and-architecture/thinking-in-graphql.md @@ -0,0 +1,309 @@ +--- +id: thinking-in-graphql +title: Thinking in GraphQL +slug: /principles-and-architecture/thinking-in-graphql/ +description: Relay guide to thinking in GraphQL +keywords: +- GraphQL +--- + +import DocsRating from '@site/src/core/DocsRating'; + +GraphQL presents new ways for clients to fetch data by focusing on the needs of product developers and client applications. It provides a way for developers to specify the precise data needed for a view and enables a client to fetch that data in a single network request. Compared to traditional approaches such as REST, GraphQL helps applications to fetch data more efficiently (compared to resource-oriented REST approaches) and avoid duplication of server logic (which can occur with custom endpoints). Furthermore, GraphQL helps developers to decouple product code and server logic. For example, a product can fetch more or less information without requiring a change to every relevant server endpoint. It's a great way to fetch data. + +In this article we'll explore what it means to build a GraphQL client framework and how this compares to clients for more traditional REST systems. Along the way we'll look at the design decisions behind Relay and see that it's not just a GraphQL client but also a framework for _declarative data-fetching_. Let's start at the beginning and fetch some data! + +## Fetching Data + +Imagine we have a simple application that fetches a list of stories, and some details about each one. Here's how that might look in resource-oriented REST: + +```javascript +// Fetch the list of story IDs but not their details: +rest.get('/stories').then(stories => + // This resolves to a list of items with linked resources: + // `[ { href: "http://.../story/1" }, ... ]` + Promise.all(stories.map(story => + rest.get(story.href) // Follow the links + )) +).then(stories => { + // This resolves to a list of story items: + // `[ { id: "...", text: "..." } ]` + console.log(stories); +}); +``` + +Note that this approach requires _n+1_ requests to the server: 1 to fetch the list, and _n_ to fetch each item. With GraphQL we can fetch the same data in a single network request to the server (without creating a custom endpoint that we'd then have to maintain): + +```javascript +graphql.get(`query { stories { id, text } }`).then( + stories => { + // A list of story items: + // `[ { id: "...", text: "..." } ]` + console.log(stories); + } +); +``` + +So far we're just using GraphQL as a more efficient version of typical REST approaches. Note two important benefits in the GraphQL version: + +- All data is fetched in a single round trip. +- The client and server are decoupled: the client specifies the data needed instead of _relying on_ the server endpoint to return the correct data. + +For a simple application that's already a nice improvement. + +## Client Caching + +Repeatedly refetching information from the server can get quite slow. For example, navigating from the list of stories, to a list item, and back to the list of stories means we have to refetch the whole list. We'll solve this with the standard solution: _caching_. + +In a resource-oriented REST system, we can maintain a **response cache** based on URIs: + +```javascript +var _cache = new Map(); +rest.get = uri => { + if (!_cache.has(uri)) { + _cache.set(uri, fetch(uri)); + } + return _cache.get(uri); +}; +``` + +Response-caching can also be applied to GraphQL. A basic approach would work similarly to the REST version. The text of the query itself can be used as a cache key: + +```javascript +var _cache = new Map(); +graphql.get = queryText => { + if (!_cache.has(queryText)) { + _cache.set(queryText, fetchGraphQL(queryText)); + } + return _cache.get(queryText); +}; +``` + +Now, requests for previously cached data can be answered immediately without making a network request. This is a practical approach to improving the perceived performance of an application. However, this method of caching can cause problems with data consistency. + +## Cache Consistency + +With GraphQL it is very common for the results of multiple queries to overlap. However, our response cache from the previous section doesn't account for this overlap — it caches based on distinct queries. For example, if we issue a query to fetch stories: + +```graphql +query { stories { id, text, likeCount } } +``` + +and then later refetch one of the stories whose `likeCount` has since been incremented: + +```graphql +query { story(id: "123") { id, text, likeCount } } +``` + +We'll now see different `likeCount`s depending on how the story is accessed. A view that uses the first query will see an outdated count, while a view using the second query will see the updated count. + +### Caching A Graph + +The solution to caching GraphQL is to normalize the hierarchical response into a flat collection of **records**. Relay implements this cache as a map from IDs to records. Each record is a map from field names to field values. Records may also link to other records (allowing it to describe a cyclic graph), and these links are stored as a special value type that references back into the top-level map. With this approach each server record is stored _once_ regardless of how it is fetched. + +Here's an example query that fetches a story's text and its author's name: + +```graphql +query { + story(id: "1") { + text, + author { + name + } + } +} +``` + +And here's a possible response: + +```json +{ + "query": { + "story": { + "text": "Relay is open-source!", + "author": { + "name": "Jan" + } + } + } +} +``` + +Although the response is hierarchical, we'll cache it by flattening all the records. Here is an example of how Relay would cache this query response: + +```javascript +Map { + // `story(id: "1")` + 1: Map { + text: 'Relay is open-source!', + author: Link(2), + }, + // `story.author` + 2: Map { + name: 'Jan', + }, +}; +``` + +This is only a simple example: in reality the cache must handle one-to-many associations and pagination (among other things). + +### Using The Cache + +So how do we use this cache? Let's look at two operations: writing to the cache when a response is received, and reading from the cache to determine if a query can be fulfilled locally (the equivalent to `_cache.has(key)` above, but for a graph). + +### Populating The Cache + +Populating the cache involves walking a hierarchical GraphQL response and creating or updating normalized cache records. At first it may seem that the response alone is sufficient to process the response, but in fact this is only true for very simple queries. Consider `user(id: "456") { photo(size: 32) { uri } }` — how should we store `photo`? Using `photo` as the field name in the cache won't work because a different query might fetch the same field but with different argument values (e.g. `photo(size: 64) {...}`). A similar issue occurs with pagination. If we fetch the 11th to 20th stories with `stories(first: 10, offset: 10)`, these new results should be _appended_ to the existing list. + +Therefore, a normalized response cache for GraphQL requires processing payloads and queries in parallel. For example, the `photo` field from above might be cached with a generated field name such as `photo_size(32)` in order to uniquely identify the field and its argument values. + +### Reading From Cache + +To read from the cache we can walk a query and resolve each field. But wait: that sounds _exactly_ like what a GraphQL server does when it processes a query. And it is! Reading from the cache is a special case of an executor where a) there's no need for user-defined field functions because all results come from a fixed data structure and b) results are always synchronous — we either have the data cached or we don't. + +Relay implements several variations of **query traversal**: operations that walk a query alongside some other data such as the cache or a response payload. For example, when a query is fetched Relay performs a "diff" traversal to determine what fields are missing (much like React diffs virtual DOM trees). This can reduce the amount of data fetched in many common cases and even allow Relay to avoid network requests at all when queries are fully cached. + +### Cache Updates + +Note that this normalized cache structure allows overlapping results to be cached without duplication. Each record is stored once regardless of how it is fetched. Let's return to the earlier example of inconsistent data and see how this cache helps in that scenario. + +The first query was for a list of stories: + +```graphql +query { stories { id, text, likeCount } } +``` + +With a normalized response cache, a record would be created for each story in the list. The `stories` field would store links to each of these records. + +The second query refetched the information for one of those stories: + +```graphql +query { story(id: "123") { id, text, likeCount } } +``` + +When this response is normalized, Relay can detect that this result overlaps with existing data based on its `id`. Rather than create a new record, Relay will update the existing `123` record. The new `likeCount` is therefore available to _both_ queries, as well as any other query that might reference this story. + +## Data/View Consistency + +A normalized cache ensures that the _cache_ is consistent. But what about our views? Ideally, our React views would always reflect the current information from the cache. + +Consider rendering the text and comments of a story along with the corresponding author names and photos. Here's the GraphQL query: + +```graphql +query { + story(id: "1") { + text, + author { name, photo }, + comments { + text, + author { name, photo } + } + } +} +``` + +After initially fetching this story our cache might be as follows. Note that the story and comment both link to the same record as `author`: + +``` +// Note: This is pseudo-code for `Map` initialization to make the structure +// more obvious. +Map { + // `story(id: "1")` + 1: Map { + text: 'got GraphQL?', + author: Link(2), + comments: [Link(3)], + }, + // `story.author` + 2: Map { + name: 'Yuzhi', + photo: 'http://.../photo1.jpg', + }, + // `story.comments[0]` + 3: Map { + text: 'Here\'s how to get one!', + author: Link(2), + }, +} +``` + +The author of this story also commented on it — quite common. Now imagine that some other view fetches new information about the author, and her profile photo has changed to a new URI. Here's the _only_ part of our cached data that changes: + +``` +Map { + ... + 2: Map { + ... + photo: 'http://.../photo2.jpg', + }, +} +``` + +The value of the `photo` field has changed; and therefore the record `2` has also changed. And that's it. Nothing else in the _cache_ is affected. But clearly our _view_ needs to reflect the update: both instances of the author in the UI (as story author and comment author) need to show the new photo. + +A standard response is to "just use immutable data structures" — but let's see what would happen if we did: + +``` +ImmutableMap { + 1: ImmutableMap // same as before + 2: ImmutableMap { + ... // other fields unchanged + photo: 'http://.../photo2.jpg', + }, + 3: ImmutableMap // same as before +} +``` + +If we replace `2` with a new immutable record, we'll also get a new immutable instance of the cache object. However, records `1` and `3` are untouched. Because the data is normalized, we can't tell that `story`'s contents have changed just by looking at the `story` record alone. + +### Achieving View Consistency + +There are a variety of solutions for keeping views up to date with a flattened cache. The approach that Relay takes is to maintain a mapping from each UI view to the set of IDs it references. In this case, the story view would subscribe to updates on the story (`1`), the author (`2`), and the comments (`3` and any others). When writing data into the cache, Relay tracks which IDs are affected and notifies _only_ the views that are subscribed to those IDs. The affected views re-render, and unaffected views opt-out of re-rendering for better performance (Relay provides a safe but effective default `shouldComponentUpdate`). Without this strategy, every view would re-render for even the tiniest change. + +Note that this solution will also work for _writes_: any update to the cache will notify the affected views, and writes are just another thing that updates the cache. + +## Mutations + +So far we've looked at the process of querying data and keeping views up to date, but we haven't looked at writes. In GraphQL, writes are called **mutations**. We can think of them as queries with side effects. Here's an example of calling a mutation that might mark a given story as being liked by the current user: + +```graphql +// Give a human-readable name and define the types of the inputs, +// in this case the id of the story to mark as liked. +mutation StoryLike($storyID: String) { + // Call the mutation field and trigger its side effects + storyLike(storyID: $storyID) { + // Define fields to re-fetch after the mutation completes + likeCount + } +} +``` + +Notice that we're querying for data that _may_ have changed as a result of the mutation. An obvious question is: why can't the server just tell us what changed? The answer is: it's complicated. GraphQL abstracts over _any_ data storage layer (or an aggregation of multiple sources), and works with any programming language. Furthermore, the goal of GraphQL is to provide data in a form that is useful to product developers building a view. + +We've found that it's common for the GraphQL schema to differ slightly or even substantially from the form in which data is stored on disk. Put simply: there isn't always a 1:1 correspondence between data changes in your underlying _data storage_ (disk) and data changes in your _product-visible schema_ (GraphQL). The perfect example of this is privacy: returning a user-facing field such as `age` might require accessing numerous records in our data-storage layer to determine if the active user is even allowed to _see_ that `age` (Are we friends? Is my age shared? Did I block you? etc.). + +Given these real-world constraints, the approach in GraphQL is for clients to query for things that may change after a mutation. But what exactly do we put in that query? During the development of Relay we explored several ideas — let's look at them briefly in order to understand why Relay uses the approach that it does: + +- Option 1: Re-fetch everything that the app has ever queried. Even though only a small subset of this data will actually change, we'll still have to wait for the server to execute the _entire_ query, wait to download the results, and wait to process them again. This is very inefficient. + +- Option 2: Re-fetch only the queries required by actively rendered views. This is a slight improvement over option 1. However, cached data that _isn't_ currently being viewed won't be updated. Unless this data is somehow marked as stale or evicted from the cache subsequent queries will read outdated information. + +- Option 3: Re-fetch a fixed list of fields that _may_ change after the mutation. We'll call this list a **fat query**. We found this to also be inefficient because typical applications only render a subset of the fat query, but this approach would require fetching all of those fields. + +- Option 4 (Relay): Re-fetch the intersection of what may change (the fat query) and the data in the cache. In addition to the cache of data Relay also remembers the queries used to fetch each item. These are called **tracked queries**. By intersecting the tracked and fat queries, Relay can query exactly the set of information the application needs to update and nothing more. + +## Data-Fetching APIs + +So far we looked at the lower-level aspects of data-fetching and saw how various familiar concepts translate to GraphQL. Next, let's step back and look at some higher-level concerns that product developers often face around data-fetching: + +- Fetching all the data for a view hierarchy. +- Managing asynchronous state transitions and coordinating concurrent requests. +- Managing errors. +- Retrying failed requests. +- Updating the local cache after receiving query/mutation responses. +- Queuing mutations to avoid race conditions. +- Optimistically updating the UI while waiting for the server to respond to mutations. + +We've found that typical approaches to data-fetching — with imperative APIs — force developers to deal with too much of this non-essential complexity. For example, consider _optimistic UI updates_. This is a way of giving the user feedback while waiting for a server response. The logic of _what_ to do can be quite clear: when the user clicks "like", mark the story as being liked and send the request to the server. But the implementation is often much more complex. Imperative approaches require us to implement all of those steps: reach into the UI and toggle the button, initiate a network request, retry it if necessary, show an error if it fails (and untoggle the button), etc. The same goes for data-fetching: specifying _what_ data we need often dictates _how_ and _when_ it is fetched. Next, we'll explore our approach to solving these concerns with **Relay**. + + diff --git a/website/versioned_docs/version-v15.0.0/principles-and-architecture/thinking-in-relay.md b/website/versioned_docs/version-v15.0.0/principles-and-architecture/thinking-in-relay.md new file mode 100644 index 0000000000000..f07d9c7959488 --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/principles-and-architecture/thinking-in-relay.md @@ -0,0 +1,104 @@ +--- +id: thinking-in-relay +title: Thinking in Relay +slug: /principles-and-architecture/thinking-in-relay/ +description: Relay guide to thinking in Relay +--- + +import DocsRating from '@site/src/core/DocsRating'; + +Relay's approach to data-fetching is heavily inspired by our experience with React. In particular, React breaks complex interfaces into reusable **components**, allowing developers to reason about discrete units of an application in isolation, and reducing the coupling between disparate parts of an application. Even more important is that these components are **declarative**: they allow developers to specify _what_ the UI should look like for a given state, and not have to worry about _how_ to show that UI. Unlike previous approaches that used imperative commands to manipulate native views (e.g. the DOM), React uses a UI description to automatically determine the necessary commands. + +Let's look at some product use-cases to understand how we incorporated these ideas into Relay. We'll assume a basic familiarity with React. + +## Fetching Data For a View + +In our experience, the overwhelming majority of products want one specific behavior: fetch all the data for a view hierarchy while displaying a loading indicator, and then render the entire view once the data is available. + +One solution is to have a root component declare and fetch the data required by it and all of its children. However, this would introduce coupling: any change to a child component would require changing any root component that might render it! This coupling could mean a greater chance for bugs and slow the pace of development. + +Another logical approach is to have each component declare and fetch the data it requires. This sounds great. However, the problem is that a component may render different children based on the data it received. So, nested components will be unable to render and begin fetching their data until parent components' queries have completed. In other words, *this forces data fetching to proceed in stages:* first render the root and fetch the data it needs, then render its children and fetch their data, and so on until you reach leaf components. Rendering would require multiple slow, serial roundtrips. + +Relay combines the advantages of both of these approaches by allowing components to specify what data they require, but to coalesce those requirements into a single query that fetches the data for an entire subtree of components. In other words, it determines *statically* (i.e. before your application runs; at the time you write your code) the requirements for an entire view! + +This is achieved with the help of GraphQL. Functional components use one or more GraphQL fragments to describe their data requirements. These fragments are then nested within other fragments, and ultimately within queries. And when such a query is fetched, Relay will make a single network request for it and all of its nested fragments. In other words, the Relay runtime is then able to make a *single network request* for all of the data required by a view! + +Let's dive deeper to understand how Relay achieves this feat. + +## Specifying the data requirements of a component + +With Relay, the data requirements for a component are specified with fragments. Fragments are named snippets of GraphQL that specify which fields to select from an object of a particular type. Fragments are written within GraphQL literals. For example, the following declares a GraphQL literal containing a fragment which selects an author's name and photo url: + +```javascript +// AuthorDetails.react.js +const authorDetailsFragment = graphql` + fragment AuthorDetails_author on Author { + name + photo { + url + } + } +`; +``` + +This data is then read out from the store by calling the `useFragment(...)` hook in a functional React component. The actual author from which to read this data is determined by the second parameter passed to `useFragment`. For example: + +```javascript +// AuthorDetails.react.js +export default function AuthorDetails(props) { + const data = useFragment(authorDetailsFragment, props.author); + // ... +} +``` + +This second parameter (`props.author`) is a fragment reference. Fragment references are obtained by **spreading** a fragment into another fragment or query. Fragments cannot be fetched directly. Instead, all fragments must ultimately be spread (either directly or transitively) into a query for the data to be fetched. + +Let's take a look at one such query. + +## Queries + +In order to fetch that data, we might declare a query which spreads `AuthorDetails_author` as follows: + +```javascript +// Story.react.js +const storyQuery = graphql` + query StoryQuery($storyID: ID!) { + story(id: $storyID) { + title + author { + ...AuthorDetails_author + } + } + } +`; +``` + +Now, we can fetch the query by calling `const data = useLazyLoadQuery(storyQuery, {storyID})`. At this point, `data.story.author` (if it is present; all fields are nullable by default) will be a fragment reference that we can pass to `AuthorDetails`. For example: + +```javascript +// Story.react.js +function Story(props) { + const data = useLazyLoadQuery(storyQuery, props.storyId); + + return (<> + {data?.story.title} + {data?.story?.author && } + ); +} +``` + +Note what has happened here. We made a single network request which contained the data required by *both* the `Story` component *and* the `AuthorDetails` component! When that data was available, the entire view could render in a single pass. + +## Data Masking + +With typical approaches to data-fetching we found that it was common for two components to have _implicit dependencies_. For example `` might use some data without directly ensuring that the data was fetched. This data would often be fetched by some other part of the system, such as ``. Then when we changed `` and removed that data-fetching logic, `` would suddenly and inexplicably break. These types of bugs are not always immediately apparent, especially in larger applications developed by larger teams. Manual and automated testing can only help so much: this is exactly the type of systematic problem that is better solved by a framework. + +We've seen that Relay ensures that the data for a view is fetched all at once. But Relay also provide another benefit that isn't immediately obvious: **data masking**. Relay only allows components to access data they specifically ask for in GraphQL fragments, and nothing more. So if one component queries for a Story's `title`, and another for its `text`, each can see _only_ the field that they asked for. In fact, components can't even see the data requested by their _children_: that would also break encapsulation. + +Relay also goes further: it uses opaque identifiers on `props` to validate that we've explicitly fetched the data for a component before rendering it. If `` renders `` but forgets to spread its fragment, Relay will warn that the data for `` is missing. In fact, Relay will warn _even if_ some other component happened to fetch the same data required by ``. This warning tells us that although things _might_ work now, they're highly likely to break later. + +# Conclusion + +GraphQL provides a powerful tool for building efficient, decoupled client applications. Relay builds on this functionality to provide a framework for **declarative data-fetching**. By separating _what_ data to fetch from _how_ it is fetched, Relay helps developers build applications that are robust, transparent, and performant by default. It's a great complement to the component-centric way of thinking championed by React. While each of these technologies — React, Relay, and GraphQL — are powerful on their own, the combination is a **UI platform** that allows us to _move fast_ and _ship high-quality apps at scale_. + + diff --git a/website/versioned_docs/version-v15.0.0/principles-and-architecture/videos.md b/website/versioned_docs/version-v15.0.0/principles-and-architecture/videos.md new file mode 100644 index 0000000000000..27826e2c3197c --- /dev/null +++ b/website/versioned_docs/version-v15.0.0/principles-and-architecture/videos.md @@ -0,0 +1,50 @@ +--- +id: videos +title: Videos +slug: /principles-and-architecture/videos/ +description: Relay videos +--- + +import DocsRating from '@site/src/core/DocsRating'; + +## React Conf 2021 + +### Re-introducing Relay | Robert Balicki + + + +## Facebook F8 2017 + +### [The Evolution of React and GraphQL at Facebook and Beyond](https://developers.facebook.com/videos/f8-2017/the-evolution-of-react-and-graphql-at-facebook-and-beyond/) + +