Computed local-only fields in Apollo Client
Tags: graphql, react, typescript, javascript
Published: 2022-06-30 12:46:18

_This article was also posted on [eftech.com](https://www.eftech.com)_

One valuable feature of Apollo Client is local-only fields.  These fields are redacted from an operation that is sent to the server by an application, and then computed and added to the server response to generate the final result.  The Apollo docs clearly explain how to leverage this feature for local state management, but it's less clear on how to derive pure local-only fields solely from other fields on the operation result.

## A (Contrived) Example

Suppose we have an operation that queries for the current user.

```typescript
const USER_QUERY = gql`
  query User {
    user {
      id
      firstName
      lastName
      department {
        id
        name
      }
    }
  }
`;
```

We use the result of this operation in some `UserProfile` component to render a display name in the format of `John Doe - Engineering team`. 

```react
const UserProfile = () => {
  const { data } = useQuery(USER_QUERY);
  const displayName = `${data.user.firstName} ${data.user.lastName} - ${data.user.department.name} team`;

  return (
    <div>
      <ProfilePicture />
      <p>{displayName}</p>
      <ContactInfo />
    </div>
  );    
}
```

As time goes on, we find ourselves using this same `displayName` format in numerous places throughout our application, duplicating the same logic each time.

```react
const BlogPost = () => {
  const { data } = useQuery(USER_QUERY);
  const displayName = `${data.user.firstName} ${data.user.lastName} - ${data.user.department.name} team`;

  return (
    <div>
      <BlogTitle />
      <p>Written by {displayName}</p>
      <BlogContent />
    </div>
  );
}
```

We consider how best to reuse this formatted name across our application.  Our first thought might be a server-side resolver, but this isn't always feasible.  We might want to make use of client-side data - local time, for example - or maybe our calculation will use fields from a variety of subgraphs that are difficult to federate between.  Our next thought is a React component, but this won't work very well either.  We want a consistent format for our displayName, but usage or styling might vary considerably depending on context.  What about a hook, then? Maybe a `useDisplayName` hook that encapsulates the query and display logic?  Better, but inelegant: we'll probably find ourselves invoking both the `useQuery` and `useDisplayName` hook in the same components, over and over.  What we'd really like is not logic derived from the query result, but rather logic incorporated in the query result.

## A Solution

The first requirement for a local-only field is a corresponding field policy with a `read` function in our cache. (Technically, a field policy could be omitted in favor of reading to and writing from the cache, but we'll save that for another post.)

```typescript
const cache = new InMemoryCache({
  typePolicies: {
    User: {
      fields: {
        displayName: {
          read(_) {
            return null;  // We'll implement this soon
          }
        }
      }
    }
  }
});
```

The first argument of the read function is the existing field value, which will be undefined for local-only fields since by definition they do not yet exist.

The other requirement for a local-only field is to add it to the operation with the `@client` directive.  This directs Apollo Client to strip the field from the server request and then restore it to the result, with the value computed by the `read` function.

```typescript
const USER_QUERY = gql`
  query User {
    user {
      id
      firstName
      lastName
      displayName @client
      department {
        id
        name
      }
    }
  }
`;
```

This field will now be included in the `data` field returned by our `useQuery` hook, but of course, it always returns null right now.  Our desired format requires three fields from the server response: the user `firstName` and `lastName`, and the department `name`.  The trick here is `readField`, a helper provided by the second "options" argument of the `read` function.  This helper will provide the requested value (if it exists) from the parent object of the field by default, or from another object if it's included as the second argument.  This helper will also resolve normalized references, allowing us to conveniently nest `readField` invocations.  Note that we can't really force the operation to include the fields on which the local-only field is dependent, and thus `readField` always has the potential to return a value of undefined (even if it's a non-nullable field).

```typescript
const cache = new InMemoryCache({
  typePolicies: {
    User: {
      fields: {
        displayName: {
          read(_, { readField }) {
            // References the parent User object by default
            const firstName = readField("firstName");
            const lastName = readField("lastName");
             // References the Department object of the parent User object
            const departmentName = readField("name", readField("department"));

            // We can't guarantee these fields were included in the operation
            if (!firstName || !lastName || !departmentName) {
              return "A Valued Team Member";
            }
            return `${data.user.firstName} ${data.user.lastName} - ${data.user.department.name} team`;
          }
        }
      }
    }
  }
});
```

Now, it's trivial to use this formatted display name anywhere in our application - it's just another field on our query data!

```react
const BlogPost = () => {
  const { data } = useQuery(USER_QUERY);

  return (
    <div>
      <BlogTitle />
      <p>Written by {data.displayName}</p>
      <BlogContent />
    </div>
  );
}
```

## Bonus: Local-only fields with GraphQL Code Generation

It's trivial to include local-only fields if you're using `graphql-codegen` (and if you're not using it, it's pretty easy to get started, too.). All you need to do is extend the type to which you're adding the local-only field in your client-side schema file.

```typescript
const typeDefs = gql`
  extend type User {
    # Don't forget to return a default value
    # in the event a field dependency is undefined
    # if the local-only field is non-nullable
    displayName: String!
  }
`;
```