Tutorial: GraphQL Input Types and Custom Resolvers

June 06, 2017 0 Comments

Tutorial: GraphQL Input Types and Custom Resolvers

 

 

In Part 4, we went over how to use store updates and Optimistic UI to deal with network latency in the channel list view of our sample app.

In this part, we’ll build a channel detail view that displays all the messages in a channel and allows you to post new messages. By the end, you’ll know how to:

  • Use field arguments in your queries
  • Make the most out of Apollo’s normalized cache
  • Use GraphQL input types

To get started, let’s clone the git repo and install the dependencies:

git clone https://github.com/apollographql/graphql-tutorial.git
cd graphql-tutorial
git checkout t5-start
cd server && npm install
cd ../client && npm install

To make sure it worked, let’s start the server and the client, each in a separate terminal:

cd server
npm start

In another terminal:

cd client
npm start

You can now navigate to localhost:3000 and explore the current state of the channel detail view. We’ve already done some work for you, so it should look something like this:

Initial state of the channel detail view

Since this tutorial focuses on GraphQL, we have already built the routing and template for the channel detail view for you. You don’t need to know how it works for this tutorial, but just in case you are curious, we use react-router(see the react-router tutorial and documentation).

It may look like we’ve done all the work already, but the new view is only a stub so far. To make it actually work, you will need to write a GraphQL query to get the channel name and its messages from the server, and you’ll need to create a mutation to add new messages.

The channel detail view should display the channel name, its messages, and a new message input. First, let’s modify the schema and write a query to display the messages currently in the channel.

In the schema (on the server), we need to create a Message type, add the messages field to the Channel type, and provide a way to fetch a single channel by adding a channel field to the root Query type. After making these changes, typeDefs in schema.js should look like this:

//server/src/schema.js
const typeDefs = <br>type Channel {<br>  id: ID!<br>  name: String<br>  <strong class="markup--strong markup--pre-strong">messages</strong>: [Message]!<br>}</pre><pre id="7e18" class="graf graf--pre graf-after--pre">type <strong class="markup--strong markup--pre-strong">Message</strong> {<br>  id: ID!<br>  text: String<br>}</pre><pre id="2734" class="graf graf--pre graf-after--pre"># This type specifies the entry points into our API<br>type Query {<br>  channels: [Channel]<br>  <strong class="markup--strong markup--pre-strong">channel</strong>(id: ID!): Channel<br>}</pre><pre id="909a" class="graf graf--pre graf-after--pre"># The mutation root type, used to define all mutations<br>type Mutation {<br>  addChannel(name: String!): Channel<br>}<br>;
const schema = makeExecutableSchema({ typeDefs, resolvers });
export { schema };

Notice that the way we fetch a single Channel is by adding id as a field argument. This is a very common pattern in GraphQL and you will probably use it a lot in your applications. Arguments can be of any scalar or input type, which we’ll learn more about later in this tutorial.

Next, the new query needs to be backed by a resolver, which returns the proper channel. For this, add the bolded query to resolvers.js:

//server/src/resolvers.js
const channels = [{
id: '1',
name: 'baseball',
messages: [{
id: '2',
text: 'baseball is life',
}]
}];
let nextId = 3;
export const resolvers = {
Query: {
channels: () => { ... },
channel: (root, { id }) => {
return channels.find(channel => channel.id = id);
},

},
};

Note: We created an array with pre-populated messages for channels . If you didn’t check out the t5-start branch, you’ll have to create that array yourself.

Now that the sever supports querying a specific channel, the client — specifically the ChannelDetails component — needs to perform the query. The best practice in GraphQL is to use query variables for arguments ($channelId for id in this case). The GraphQL spec requires that we define the variables we use after the query keyword. If we don’t do it, the server will complain that we used a variable without defining it. The definition has to match the type that the argument expects. In this case, it’s ID.

In channelDetails.js write the following query:

//client/src/components/channelDetails.js
export const channelDetailsQuery = gql<br>  query <strong class="markup--strong markup--pre-strong">ChannelDetailsQuery($channelId : ID!)</strong> {<br>    channel(id: $channelId) {<br>      id<br>      name<br>      messages {<br>        id<br>        text<br>      }<br>    }<br>  }<br>;

In the ChannelDetails React component, replace the stub with code that renders the actual data. First check if the query is loading (data.loading), then check to make sure that there is no error (data.error), and finally render the channel name and MessagesList.

If you do all of that, you should end up with a component that looks like this:

//client/src/components/ChannelDetails.js
const ChannelDetails = ({ data: {loading, error, channel }, match }) => {
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>{error.message}</p>;
}
if(channel = null){
return <NotFound />
}
  return (<div>
<div className="channelName">
{channel.name}
</div>
<MessageList messages={channel.messages}/>
</div>);
}
//export const channelDetailsQuery = gql...;
// ...

Now all you have to do is wrap the component with the query we wrote earlier, and export it.

//client/src/components/ChannelDetails.js (at the bottom)
export default (graphql(channelDetailsQuery, {
options: (props) => ({
variables: { channelId: props.match.params.channelId },
}),
})(ChannelDetails));

We’re well on our way to a functioning chat application, try it out for yourself! It should look like this:

Midpoint state of channel detail view

Now that we have a channel name and message stream, let’s add the message mutation to post new messages.

Creating a functional AddMessage is very similar to adding a channel in part 3, so first instinct suggests using a mutation with fields for the message text and a channel id. But in the future, we may want to associate a username, timestamp, text encoding, picture, mentioned users, or other meta-message information. Adding each of these to the Mutation’s signature quickly becomes unwieldy and inflexible. To keep things tidy, we’re going to use a GraphQL input type, which is an object that can only contain basic scalar types, list types, and other input types. Input types allow client mutation signatures to stay constant and provide better readability in the schema.

Starting on the server, let’s define the MessageInput input type and include the mutation in schema.js as follows:

//server/src/schema.js
input MessageInput{
channelId: ID!
text: String
}
type Mutation {
# A mutation to add a new channel to the list of channels
addChannel(name: String!): Channel
addMessage(message: MessageInput!): Message
}

The resolver for addMessage in resolvers.js should check that the input

//server/src/resolvers.js
Mutation: {
addChannel: {...},
addMessage: (root, { message }) => {
const channel = channels.find(channel => channel.id = message.channelId);
if(!channel)
throw new Error("Channel does not exist");
    const newMessage = { id: String(nextMessageId++), text: message.text };
channel.messages.push(newMessage);
return newMessage;
},
},

Next on the client side, we need to complete AddMessage.js, starting with the query:

//client/src/components/AddMessage.js
const addMessageMutation = gql<br>  mutation addMessage($message: MessageInput!) {<br>    addMessage(message: $message) {<br>      id<br>      text<br>    }<br>  }<br>;

The AddMessage component body adds variables to AddChannel’s base code, which includes the same optimistic UI functionality we used in the last tutorial. The only part that’s different are the variables. I have highlighted the changes in bold below:

//client/src/components/AddMessage.js
const AddMessage = ({ mutate, match }) => {
const handleKeyUp = (evt) => {
if (evt.keyCode = 13) {
mutate({
variables: {
message: {
channelId: match.params.channelId,
text: evt.target.value
}
},
optimisticResponse: {
addMessage: {
text: evt.target.value,
id: Math.round(Math.random() * -1000000),
_typename: 'Message',
},
},
update: (store, { data: { addMessage } }) => {
// Read the data from the cache for this query.
const data = store.readQuery({
query: channelDetailsQuery,
variables: {
channelId: match.params.channelId,
}
});
// Add our Message from the mutation to the end.
data.channel.messages.push(addMessage);
// Write the data back to the cache.
store.writeQuery({
query: channelDetailsQuery,
variables: {
channelId: match.params.channelId,
},
data
});
},
});
evt.target.value = '';
}
};
  return ( 
...
);
};
//const addMessageMutation = gql...
const AddMessageWithMutation = graphql(
addMessageMutation,
)(withRouter(AddMessage));
export default AddMessageWithMutation;

Note: match is react-router’s interface to url properties provided by withRouter

Now we have a fully-functioning messaging channel! However, there’s a small problem: if the network is slow, the user has to wait for both the channel name and messages to be loaded from the server. Until all of the data is loaded, the user won’t even know which channel they are in, which is bad UX. Ideally, we’d want the user to see a good channel preview while messages are being loaded. That’s what we’re going to do in the last section of this tutorial.

As you may have noticed, the client already knows the channel names because it loaded them with theChannelsListQuery on the homepage. If there was a way for us to keep the channel name around, we could display it without making another request to the server!

Lucky for you, Apollo Client automatically stores each query result in its normalized cache, which means we can just query for the data we want and let Apollo Client figure out whether it can be loaded from the cache or not. However, there is a small catch:

By default, Apollo Client uses the query path (for example /channel(id:5)/name) to determine if an object is cached.

Since the channels and channel queries result in different paths to the same object, Apollo Client doesn’t know that they are the same unless you explicitly tell it that the channel query might resolve to an object that was retrieved by the channels query. We can tell Apollo Client about this relationship by adding a custom resolver to the ApolloClient constructor in App.js. This custom resolver tells Apollo Client to check its cache for a Channel object with ID $channelId whenever we make a channel query. If it finds a channel with that ID in the cache, it will not make a request to the server.

The following custom resolver creates this mapping in App.js:

//client/src/App.js
//function dataIdFromObject (result) {...}
const client = new ApolloClient({
networkInterface,
customResolvers: {
Query: {
channel: (
, args) => {
return toIdValue(dataIdFromObject({ __typename: 'Channel', id: args['id'] }))
},
},
},
dataIdFromObject,
});

ApolloClient uses dataIdFromObject to tag GraphQL objects in the cache and toIdValue ensures an ID type is returned.

Now all you have to do is create the ChannelPreview component as you normally would:

//client/src/components/ChannelPreview.js
const ChannelPreview = ({ data: {loading, error, channel } }) => {  
return (
<div>
<div className="channelName">
{channel ? channel.name : 'Loading...'}
</div>
      <div>Loading Messages</div>
</div>
);
};
export const channelQuery = gql<br>  query ChannelQuery($channelId : ID!) {<br>    channel(id: $channelId) {<br>      id<br>      name<br>    }<br>  }<br>;
export default (graphql(channelQuery, {
options: (props) => ({
variables: { channelId: props.channelId },
}),
})(ChannelPreview));

Lastly, we need to replace the loading message of our ChannelDetails component with the ChannelPreview component:

//client/src/components/ChannelDetails.js
const ChannelDetails = (...) => {
if (loading) {
return <ChannelPreview channelId={match.params.channelId}/>;
}

By pulling data from the cache, we have created a channel detail view that displays the channel name immediately, while loading the messages in the background.

Final state of channel detail view

Congratulations, you now have an application that provides channel-labelled messaging streams! The service is almost ready for production, after a couple enhancements: First, we will want to paginate the messages, since loading all messages at one time could be slow. Second, we’ll want a way to show messages in real time using GraphQL Subscriptions. Finally, we’ll also want to add login and authentication to make sure we know who a message is from. Look out for future tutorials on pagination, subscriptions and authentication!

If you liked this tutorial and want to keep learning about Apollo and GraphQL, make sure to click the “Follow” button below, and follow us on Twitter at @apollographql and the author at @evanshauser.

A huge thank you to Jonas Helfer for his guidance!


Tag cloud