This article was also posted on 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.
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
.
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.
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.)
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.
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).
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!
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.
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!
}
`;
Top comments (1)
How to access nested fields that are Arrays?
This method doesn't play well with
interfaces
It seems like a lot of work and there is no type safety, building these client fields.
Whats the performance improvement/dev experience gains by using these client fields?