How To Handle Data With GraphQL Relay Client Schema Extensions
Tags: webdev, react, tutorial, graphql
Published: 2024-01-16 11:00:35

[GraphQL Relay](https://relay.dev) 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`:

```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`.

```graphql
# 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](https://graphql.org/learn/pagination/#end-of-list-counts-and-connections).

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.

```ts
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:

```tsx
// 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:

```ts
// 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](https://relay.dev/docs/guides/client-schema-extensions) documentation, it gives you an idea of how to use it.

---

[Woovi](https://www.woovi.com/) 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](https://woovi.com/jobs/)!

---

Photo by <a href="https://unsplash.com/@pankajpatel?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Pankaj Patel</a> on <a href="https://unsplash.com/photos/nzNTw9e_1tg?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>