Guilherme Ananias
Posted on January 16, 2024
GraphQL Relay is one of the most powerful GraphQL clients that you can found on the web environment. It provides to you a lot of features that lets your development flow in a scalable way.
One of these great features are the client schema extensions, it's a way to extends your data coming from server with a kind of data managed on the client-side in the way you want.
In this scenario, we found the client schema extensions as an easier way to fits all our needs around the consumption of the data: needs to be easier consumed through all the application and should be managed by the Relay infrastructure, most focusing in the Relay Store.
This approach will let you sync an external store like Redux, Zustand or any other third-party global state management library into a Relay friendly way.
Extending My GraphQL Schema
The first step to handling client schema extensions in your codebase is to set up the schema extensions. For it, you will need to update your relay.config.js
:
// relay.config.js
module.exports = {
// ...
schemaExtensions: ['./src/'],
}
In this case, the schemaExtensions
will target a set of paths targeting directories that contains a *.graphql
file to extend.
After that, you just need to write a new *.graphql
file that will add some new type and compile it using the relay-compiler
.
# client-schema.gql
type User {
name: String!
age: Int
}
type UserEdge {
node: User
cursor: String!
}
type UserConnection {
count: Int
totalCount: Int
startCursorOffset: Int!
endCursorOffset: Int!
pageInfo: PageInfoExtended!
edges: [UserEdge]!
}
extend type Query {
users(
first: Int
after: String
last: Int
before: String
): UserConnection!
}
In this case, what we're doing is extending the type Query
of our schema with a new query called users
, it will let us query all the users
in our Relay Store and give an entire paginated data based on Connection Pagination Pattern.
You'll be able to run the relay-compiler
command with the schema extension now.
Managing The Data in The Store
Now that you have your extended schema, you can manage all client data handling it via Relay Store. For it, you'll need to use a helper function called commitLocalUpdate
, it'll give you all the tools to let you commit and update data in your Relay Store.
import { commitLocalUpdate, ConnectionHandler } from 'react-relay';
const appendNewUser = (environment, data) => {
commitLocalUpdate((store) => {
const root = store.getRoot(); // get the root node on the relay store
const connection = ConnectionHandler.getConnection(
root,
'UserListClientQuery_users', // this is the key of the @connection that will consume it
);
const edgeNumber = connection.getLinkedRecords('edges').length;
const dataId = createRelayDataId(row.id, 'User'); // this is just a util function to generate a global ID for this new entry on store
const node = store.create(dataId, row.__typename);
node.setValue(dataId, 'id');
const edgeId = `client:root:users:${node.getDataID().match(/[^:]+$/)[0]}:edges:${edgeNumber}`;
// assign all values for this node
Object.keys(data).forEach((key) => {
const value = data[key];
recordProxy.setValue(value, key);
});
// this will create the UserEdge node
const edge = store.create(
edgeId,
'UserEdge',
);
// will assign the User node in the edge as a __ref
edge.setLinkedRecord(node, 'node');
const newEndCursorOffset = connection.getValue('endCursorOffset');
connection.setValue(newEndCursorOffset + 1, 'endCursorOffset');
const newCount = connection.getValue('count');
connection.setValue(newCount + 1, 'count');
// insert new connection as the last item on the connection array, like the push() method
ConnectionHandler.insertEdgeAfter(connection, edge);
});
}
The function above is just a helper to abstract how we'll add new users to the Relay Store. After writing it, you can use it like this:
// UserList.tsx
import { useRelayEnvironment, useClientQuery, graphql } from 'relay-react';
const UserList = () => {
const environment = useRelayEnvironment();
const query = useClientQuery<UserListClientQuery>(
graphql`
query UserListClientQuery {
users(first: 10)
@connection(key: "UserListClientQuery_users", filters: []) {
edges {
node {
id
name
age
}
}
}
}
`,
{}
);
const handleAddNewUser = () => {
appendNewUser(environment, {
id: randomUserId(), // just a function to randomize an id
name: randomUserName(), // just a function to randomize name
age: randomUserAge(), // just a function to randomize age
});
}
return (
<>
<button onClick={handleAddNewUser}>Add User</<button>
<div>
{query.users.edges.map(node => (
<p key={node.id}>
{node.name} - {node.age}
</p>
))}
</div>
</>
);
}
The useClientQuery
is a helpful hook that lets you query ONLY client data. In this component, we're doing two things: listing all the users from the Relay Store and committing new users to the Relay Store.
Now, all your data related to the UserConnection
that has been persisted in your store is easily managed by your component.
Initializing The Local Data
By default, every data in the store will start with an undefined
value. For scenarios where this does not match the expectation that we want, like our example handling a connection array, we can easily initialize the local data with another value.
For this, you'll need to use the commitLocalUpdate
before querying any local data, in this case, you can use it when you setup your Relay Environment
. See the example below:
// relay/Environment.tsx
import { Environment, commitLocalUpdate } from 'relay-runtime';
const env = new Environment({
// ...
});
commitLocalUpdate(env, (store) => {
const connection = store.create('client:root:users', 'UserConnection');
connection.setLinkedRecords([], 'edges'); // add an edge field with empty __refs
connection.setValue(0, 'count');
const root = store.getRoot();
root.setLinkedRecord(
connection,
'__UserListClientQuery_users_connection', // this is the key of queried connection when persisted in the relay store
);
});
With this approach, you'll be able to access the client query in your first render with an empty connection. You can replicate this idea for other similar data too.
Other Resources
If you're curious about how powerful is client schema extensions, I suggest you read more about them on the Client Schema Extensions documentation, it gives you an idea of how to use it.
Woovi is a Startup that enables shoppers to pay as they like. To make this possible, Woovi provides instant payment solutions for merchants to accept orders.
If you want to work with us, we are hiring!
Photo by Pankaj Patel on Unsplash
Posted on January 16, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.