relay-utils
This package contains utilities for working with Relay (modern) in general, and the Relay store + updates to the store in particular.
Looking for contributions for TypeScript definitions. I don't actively use TypeScript myself, but I'll gladly accept PRs for TS type definitions.
Installation
yarn add relay-utils
Table of Contents
- collectConnectionNodes - Type safe way of collecting all nodes from a connection.
- createRelayDataId - Create Relay data IDs.
- resolveNestedRecord - Resolves a record in the store from a path.
- setFieldsOnRecord - Sets fields of provided object on a record.
- createAndAddNodeToStore - Creates a node of given type and shape and adds it to the store.
- createAndAddEdgeToConnections - Creates an edge for a node and adds that edge + node to one or more connections.
- createAndAddEdgeToLinkedRecords - Same as
createAndAddEdgeToConnection
but for linked records. - removeNodeFromStore - Removes a node and cleans it + edges from connections/linked records.
- createMissingFieldsHandler - Creates handlers for resolving missing fields in the store.
Usage
NOTE A lot of these helpers assume that you're using graphql-relay-js
style data IDs that are a combination of a database id and a typename.
Please file an issue if you don't and would like to use this package still.
collectConnectionNodes
Takes a connection, collects all nodes from the edges, and filters out nulls.
Example
Whenever you use a connection (with or without the @connection
-annotation)...
fragment SomeFragment_user on User { pets(first: 10) @connection(key: "SomeFragment_user_pets") { pageInfo { hasNextPage endCursor } edges { node { id name } } }}
...and you want to map over or just use the connection nodes, you can use collectConnectionNodes
:
type Props = | user: SomeFragment_user|; const MyFragmentComponent = { // This is fully type safe, so pets is now $ReadOnlyArray<{| +id: string, +name: string |}> const pets = ; return pets;};
createRelayDataId
A simple function to create data IDs for Relay. Useful for a number of scenarios.
Example
Imagine you have a field in your schema for each type that represents the database id for that object, like this:
type Pet { id: ID! # The Relay data id from the server _id: ID! # Your internal database ID that you use when querying for just this node, using it in mutations etc}
Now, imagine you want to look for a Pet
in the store without passing its data id all the way to your updater when you already have the _id
database id:
const updater = { // This gets the Pet with database id petInput.id const petNode = store;};
Signature
: string
graphql-generate-flow-schema-assets
for type safety
Hint: Pair with graphql-generate-flow-schema-assets
generates Flow types for your enums and object types from your schema.graphql
. You can use that output to create your own, type safe variant of createRelayDataId
like this:
; // Ensures only object types in your schema can be passed to the creator function: string { return ;}
resolveNestedRecord
Tries to resolve linked records from a path
you give it.
Example
For this data structure:
type RootQuery { viewer: User} type User { name: string favoritePet: Pet} type Pet { owner: User!} query ViewerFavoritePetQuery { viewer { favoritePet { owner { name } } }}
...this accessor would work:
const updater = { const petOwner = ; // You can use it on any record const viewerNode = store; const petOwner = ;};
Signature
: ?RecordProxy
setFieldsOnRecord
Simple helper to set values of object on record.
Example
; ;
Signature
: void
createAndAddNodeToStore
Creates a node of a given type and shape, and adds it to the store.
Example
; ;
Signature
: RecordProxy
createAndAddEdgeToConnections
It's pretty common that GraphQL API:s return just the newly created node on mutations that lets you create nodes. Imagine something like:
mutation AddPetMutation($input: AddPetInput!) { addPet(input: $input) { addedPet { id name } }}
Now, often you'll end up in a scenario where you want to attach that node to a list of some sort, like a connection or a linked list.
createAndAddEdgeToConnection
makes adding that created node to a connection easier:
Example
const updater = { // Get the mutation payload from the store const addedPet = ; if addedPet ; };
Signature
: void type CreateAndAddEdgeToConnectionConfig = {| node: RecordProxy // From the Relay store connections: Array<ConnectionConfig> edgeName: string insertAt?: 'START' | 'END'|}; type ConnectionConfig = | parentID: string key: string filters?: Object|;
createAndAddEdgeToLinkedRecords
A close sibling of createAndAddEdgeToConnections
, createAndAddEdgeToLinkedRecords
does the same thing, but for connections that are not annotated with @connection
.
Example
Following the example of createAndAddEdgeToConnections
, you'd write an updater like this instead for createAndAddEdgeToLinkedRecords
:
const updater = { // Get the mutation payload from the store const addedPet = ; const petOwner = store; // Omitted since it's an example, but the node that owns the connection if addedPet ; };
Signature
: void type CreateAndAddEdgeToLinkedRecordConfig = {| node: RecordProxy linkedRecords: Array<LinkedRecordConfig> edgeName: string insertAt?: 'START' | 'END'|}; type LinkedRecordConfig = | parentID: string key: string filters?: Object|;
removeNodeFromStore
Whenever you delete something through your GraphQL API, and you can't/don't want to refetch everything that might've been affected by the delete,
you'll need to do some cleanup in the store to remove the deleted record. This involves deleting the record, as well as removing it + its edges from connections
or other linked records that might have the node in the store. removeNodeFromStore
helps with that.
Example
You delete something:
mutation DeletePet($input: DeletePetInput!) { deletePet(input: $input) { ok }}
Now, you want to clean up that node + things that use it:
const updater = { const deletedPetNode = store; // The data ID of the deleted pet node // This might exist in the viewers pets connection const viewer = store; // It might also exist in a second users pets list. This list however is not annotated with @connection, so we'll need to remove it as a linked record. const secondPossibleOwner = store; ;};
PLEASE NOTE that removeNodeFromStore
does no automatic deletion - it relies on you providing it with all connections/linked record owner that might have the node.
Signature
: void type RemoveNodeAndConnectionsConfig = {| node: RecordProxy connections?: Array<ConnectionConfig> linkedRecords?: Array<LinkedRecordConfig>|}; type ConnectionConfig = | parentID: string key: string filters?: Object|; type LinkedRecordConfig = | parentID: string key: string filters?: Object|;
createMissingFieldsHandler
Sets up resolvers for missing fields in the graph. Uses the built in missingFieldHandlers
on the Relay Environment
.
Example
A fairly common thing is to have two separate id's for your types in your schema - one that's a Relay data id for global uniqueness, and one that's your database id or similar, that you use when updating/deleting and otherwise referencing the object when interacting with the API/backend. In this example we'll say that you have a type that looks like this:
type Pet { id: ID! # The Relay data ID, globally unique and generated by the backend _id: ID! # The database ID of this object, used when updating/deleting etc}
It's easier to use the database ID when interacting with the backend. Imagine you have a query that looks like this:
query SinglePetQuery($id: ID!) { # id here is not the Relay data ID, but the actual database ID Pet(id: $id) { name }}
Lets say you want to use SinglePetQuery
to get the Pet
with id 123
. But, in your store, there's already
enough data for the Pet
with id 123
to be resolved without a round-trip to the database, since you've fetched that Pet
as part of another query.
Relay should just resolve this from the cache then, right? Well, Relay has no clue how to use your id
query argument (that's a database id) to look up a Pet
in the local store.
Lets use createMissingFieldsHandler
to teach it how to do that so we won't have to make a round-trip to the database:
// Missing field handlers are defined when creating the Environmentconst environment = network store missingFieldHandlers: ;
This teaches Relay how to use your query argument id
on your root-level field Pet
to resolve a Pet
already in the store.
Signature
: $ReadOnlyArray<MissingFieldHandler> type DataID = string; type ScalarMissingFieldReplacerFn = ( config: MissingFieldReplacerConfig) => ?mixed; // Return whatever you want as value for the scalar field here, or undefined if you want the field to remain missing. type LinkedFieldMissingFieldReplacerFn = ( config: MissingFieldReplacerConfig) => ?DataID; type PluralLinkedFieldMissingFieldReplacerFn = ( config: MissingFieldReplacerConfig) => ?Array<?DataID>; type MissingFieldReplacerConfig = {| name: string // The name of the original field, regardless of alias alias: ?string // If the field is aliased to something, this will be set args: ?Variables // Arguments for the operation/query the field is found in fieldArgs: ?$ReadOnlyArray<NormalizationArgument> // Arguments for this specific field. Check out NormalizationArgument in the Relay code base for more info ownerTypename: ?string // The typename of the record owning the field. This will be ROOT_TYPE imported from relay-runtime when the field is a root field owner: ?Record // The record that owns this field store: ReadOnlyRecordSourceProxy // The full Relay store|};
Notes
This is an opinionated abstraction on top of Relays own missingFieldHandlers
. You might not want to use createMissingFieldHandlers
,
but use Relays API directly. Check out Relays own type definition for missingFieldHandlers here. The missingFieldHandlers
is then attached when creating the Environment
, as illustrated in the example above.