How To Handle Data With GraphQL Relay Client Schema Extensions

noghartt

Guilherme Ananias

Posted on January 16, 2024

How To Handle Data With GraphQL Relay Client Schema Extensions

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/'],
}
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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);
  });
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
  );
});
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
noghartt
Guilherme Ananias

Posted on January 16, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related