Building Chatty — Part 4 (GraphQL Mutations & Optimistic UI)

A WhatsApp clone with React Native and Apollo

This is the fourth blog in a multipart series where we will be building Chatty, a WhatsApp clone, using React Native and Apollo. You can view the code for this part of the series here.

In Part 1, we set up our dev environment and connected our Express server to a React Native client.

In Part 2, we designed GraphQL Schemas and Queries and connected them to real data on our server.

In Part 3, we designed the basic layout of our React Native app and queried for data on our server using react-apollo.

Here’s what we will accomplish in this tutorial:

  1. Design GraphQL Mutations and add them to the GraphQL Schemas on our server
  2. Modify the layout on our React Native client to let users send Messages
  3. Build GraphQL Mutations on our RN client and connect them to components using react-apollo
  4. Add Optimistic UI to our GraphQL Mutations so our RN client updates as soon as the Message is sent — even before the server sends a response!

YOUR CHALLENGE

  1. Add GraphQL Mutations on our server for creating, modifying, and deleting Groups
  2. Add new Screens to our React Native app for creating, modifying, and deleting Groups
  3. Build GraphQL Queries and Mutations for our new Screens and connect them using react-apollo

Adding GraphQL Mutations on the Server

While GraphQL Queries let us fetch data from our server, GraphQL Mutations allow us to modify our server held data.

To add a mutation to our GraphQL endpoint, we start by defining the mutation in our GraphQL Schema much like we did with queries. We’ll define a createMessage mutation that will enable users to send a new message to a Group:

type Mutation {
# create a new message
# text is the message text
# userId is the id of the user sending the message
# groupId is the id of the group receiving the message
createMessage(text: String!, userId: Int!, groupId: Int!): Message
}

GraphQL Mutations are written nearly identically to GraphQL Queries. For now, we will require a userId parameter to identify who is creating the Message, but we won’t need this field once we implement authentication in a future tutorial.

Let’s update our Schema in server/data/schema.js to include the mutation:

Step 4.1: Add Mutations to Schema

Changed server/data/schema.js

...
    group(id: Int!): Group
}
  type Mutation {
# send a message to a group
createMessage(
text: String!, userId: Int!, groupId: Int!
): Message
}


schema {
query: Query
mutation: Mutation
}
`];
...

We also need to modify our resolvers to handle our new mutation. We’ll modify server/data/resolvers.js as follows:

Step 4.2: Add Mutations to Resolvers

Changed server/data/resolvers.js

...
      return User.findOne({ where: args });
},
},
Mutation: {
createMessage(_, { text, userId, groupId }) {
return Message.create({
userId,
text,
groupId,
});
},
},

Group: {
users(group) {
return group.getUsers();
...

That’s it! When a client uses createMessage, the resolver will use the Message model passed by our connector and call Message.create with arguments from the mutation. The Message.create function returns a Promise that will resolve with the newly created Message.

We can easily test our newly minted createMessage mutation in GraphIQL to make sure everything works:

BaDumCh!

Designing the Input

Wow, that was way faster than when we added queries! All the heavy lifting we did in the first 3 parts of this series is starting to pay off….

Now that our server allows clients to create messages, we can build that functionality into our React Native client. First, we’ll start by creating a new component MessageInput where our users will be able to input their messages.

For this component, let’s use cool icons. react-native-vector-icons is the goto package for adding icons to React Native. Please follow the instructions in the react-native-vector-icons README before moving onto the next step.

# make sure you're adding this package in the client folder!!!
cd client
yarn add react-native-vector-icons
react-native link
# this is not enough to install icons!!! PLEASE FOLLOW THE INSTRUCTIONS IN THE README TO PROPERLY INSTALL ICONS!

After completing the steps in the README to install icons, we can start putting together the MessageInput component in a new file client/src/components/message-input.component.js:

Step 4.3: Create MessageInput

Added client/src/components/message-input.component.js

import React, { Component, PropTypes } from 'react';
import {
StyleSheet,
TextInput,
View,
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
const styles = StyleSheet.create({
container: {
alignSelf: 'flex-end',
backgroundColor: '#f5f1ee',
borderColor: '#dbdbdb',
borderTopWidth: 1,
flexDirection: 'row',
},
inputContainer: {
flex: 1,
paddingHorizontal: 12,
paddingVertical: 6,
},
input: {
backgroundColor: 'white',
borderColor: '#dbdbdb',
borderRadius: 15,
borderWidth: 1,
color: 'black',
height: 32,
paddingHorizontal: 8,
},
sendButtonContainer: {
paddingRight: 12,
paddingVertical: 6,
},
sendButton: {
height: 32,
width: 32,
},
iconStyle: {
marginRight: 0, // default is 12
},
});
const sendButton = send => (
<Icon.Button
backgroundColor={'blue'}
borderRadius={16}
color={'white'}
iconStyle={styles.iconStyle}
name="send"
onPress={send}
size={16}
style={styles.sendButton}
/>
);
class MessageInput extends Component {
constructor(props) {
super(props);
this.state = {};
this.send = this.send.bind(this);
}
  send() {
this.props.send(this.state.text);
this.textInput.clear();
this.textInput.blur();
}
  render() {
return (
<View style={styles.container}>
<View style={styles.inputContainer}>
<TextInput
ref={(ref) => { this.textInput = ref; }}
onChangeText={text => this.setState({ text })}
style={styles.input}
placeholder="Type your message here!"
/>
</View>
<View style={styles.sendButtonContainer}>
{sendButton(this.send)}
</View>
</View>
);
}
}
MessageInput.propTypes = {
send: PropTypes.func.isRequired,
};
export default MessageInput;

Our MessageInput component is a View that wraps a controlled TextInput and an Icon.Button. When the button is pressed, props.send will be called with the current state of the TextInput text and then the TextInput will clear. We’ve also added some styling to keep everything looking snazzy.

Let’s add MessageInput to the bottom of the Messages screen and create a placeholder send function:

Step 4.4: Add MessageInput to Messages

Changed client/src/screens/messages.screen.js

...
import { graphql, compose } from 'react-apollo';
import Message from '../components/message.component';
import MessageInput from '../components/message-input.component';
import GROUP_QUERY from '../graphql/group.query';
const styles = StyleSheet.create({
...
      usernameColors: {},
};
    this.send = this.send.bind(this);
}
...
  send(text) {
// TODO: send the message
console.log(`sending message: ${text}`);
}
  render() {
const { loading, group } = this.props;
...
/>
<MessageInput send={this.send} />
</View>
);
}
...

It should look like this:

Ooh Lala!

But don’t be fooled by your simulator! This UI will break on a phone because of the keyboard:

Where’d the input go?!?!

You are not the first person to groan over this issue. For you and the many groaners out there, the wonderful devs at Facebook have your back. KeyboardAvoidingView to the rescue!

Step 4.5: Add KeyboardAvoidingView

Changed client/src/screens/messages.screen.js

...
import {
ActivityIndicator,
FlatList,
KeyboardAvoidingView,
StyleSheet,
View,
} from 'react-native';
...
    // render list of messages for group
return (
<KeyboardAvoidingView
behavior={'position'}
contentContainerStyle={styles.container}
keyboardVerticalOffset={64}
style={styles.container}
>
<FlatList
data={group.messages.slice().reverse()}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
/>
<MessageInput send={this.send} />
</KeyboardAvoidingView>
);
}
}
...
Welcome back, old friend

Our layout looks ready. Now let’s make it work!

Adding GraphQL Mutations on the Client

Let’s start by defining our GraphQL Mutation like we would using GraphIQL:

mutation createMessage($text: String!, $userId: Int!, $groupId: Int!) {
createMessage(text: $text, userId: $userId, groupId: $groupId) {
id
from {
id
username
}
createdAt
text
}
}

That looks fine, but notice the Message fields we want to see returned look exactly like the Message fields we are using for GROUP_QUERY:

query group($groupId: Int!) {
group(id: $groupId) {
id
name
users {
id
username
}
messages {
id
from {
id
username
}
createdAt
text
}
}
}

GraphQL allows us to reuse pieces of queries and mutations with Fragments. We can factor out this common set of fields into a MessageFragment that looks like this:

Step 4.6: Create MessageFragment

Added client/src/graphql/message.fragment.js

import gql from 'graphql-tag';
const MESSAGE_FRAGMENT = gql`
fragment MessageFragment on Message {
id
to {
id
}
from {
id
username
}
createdAt
text
}
`;
export default MESSAGE_FRAGMENT;

Now we can apply MESSAGE_FRAGMENT to GROUP_QUERY by changing our code as follows:

Step 4.7: Add MessageFragment to Group Query

Changed client/src/graphql/group.query.js

import gql from 'graphql-tag';
import MESSAGE_FRAGMENT from './message.fragment';
const GROUP_QUERY = gql`
query group($groupId: Int!) {
group(id: $groupId) {
id
name
users {
id
username
}
messages {
... MessageFragment
}

}
}
${MESSAGE_FRAGMENT}
`;
export default GROUP_QUERY;

Let’s also write our createMessage mutation using messageFragment in a new file client/src/graphql/create-message.mutation.js:

Step 4.8: Create CREATE_MESSAGE_MUTATION

Added client/src/graphql/create-message.mutation.js

import gql from 'graphql-tag';
import MESSAGE_FRAGMENT from './message.fragment';
const CREATE_MESSAGE_MUTATION = gql`
mutation createMessage($text: String!, $userId: Int!, $groupId: Int!) {
createMessage(text: $text, userId: $userId, groupId: $groupId) {
... MessageFragment
}
}
${MESSAGE_FRAGMENT}
`;
export default CREATE_MESSAGE_MUTATION;

Now all we have to do is plug our mutation into our Messages component using the graphql module from react-apollo. Before we connect everything, let’s see what a mutation call with the graphql module looks like:

const createMessage = graphql(CREATE_MESSAGE_MUTATION, {
props: ({ ownProps, mutate }) => ({
createMessage: ({ text, userId, groupId }) =>
mutate({
variables: { text, userId, groupId },
}),
}),
});

Just like with a GraphQL Query, we first pass our mutation to graphql, followed by an Object with configuration params. The props param accepts a function with named arguments including ownProps (the components current props) and mutate. This function should return an Object with the name of the function that we plan to call inside our component, which executes mutate with the variables we wish to pass. If that sounds complicated, it’s because it is. Kudos to the Meteor team for putting it together though, because it’s actually some very clever code.

At the end of the day, once you write your first mutation, it’s really mostly a matter of copy/paste and changing the names of the variables.

Okay, so let’s put it all together in messages.screen.js:

Step 4.9: Add CREATE_MESSAGE_MUTATION to Messages

Changed client/src/screens/messages.screen.js

...
import Message from '../components/message.component';
import MessageInput from '../components/message-input.component';
import GROUP_QUERY from '../graphql/group.query';
import CREATE_MESSAGE_MUTATION from '../graphql/create-message.mutation';
...
  send(text) {
this.props.createMessage({
groupId: this.props.navigation.state.params.groupId,
userId: 1, // faking the user for now
text,
});

}
...
}
Messages.propTypes = {
createMessage: PropTypes.func,
navigation: PropTypes.shape({
state: PropTypes.shape({
params: PropTypes.shape({
groupId: PropTypes.number,
}),
}),
}),

group: PropTypes.shape({
messages: PropTypes.array,
users: PropTypes.array,

...
});
const createMessageMutation = graphql(CREATE_MESSAGE_MUTATION, {
props: ({ mutate }) => ({
createMessage: ({ text, userId, groupId }) =>
mutate({
variables: { text, userId, groupId },
}),
}),
});
export default compose(
groupQuery,
createMessageMutation,
)(Messages);

By attaching createMessage with compose, we attach a createMessage function to the component’s props. We call props.createMessage in sendwith the required variables (we’ll keep faking the user for now). When the user presses the send button, this method will get called and the mutation should execute.

Let’s run the app and see what happens:

Where’s my message?!

What went wrong? Well technically nothing went wrong. Our mutation successfully executed, but we’re not seeing our message pop up. Why? Running a mutation doesn’t automatically update our queries with new data! If we were to refresh the page, we’d actually see our message. This issue only arrises when we are adding or removing data with our mutation.

To overcome this challenge, react-apollo lets us declare a property update within the argument we pass to mutate. In update, we specify which queries should update after the mutation executes and how the data will transform.

Our modified createMessage should look like this:

Step 4.10: Add update to mutation

Changed client/src/screens/messages.screen.js

...
function isDuplicateMessage(newMessage, existingMessages) {
return newMessage.id !== null &&
existingMessages.some(message => newMessage.id === message.id);
}
class Messages extends Component {
static navigationOptions = ({ navigation }) => {
const { state } = navigation;
...
    createMessage: ({ text, userId, groupId }) =>
mutate({
variables: { text, userId, groupId },
update: (store, { data: { createMessage } }) => {
// Read the data from our cache for this query.
const data = store.readQuery({
query: GROUP_QUERY,
variables: {
groupId,
},
});
          if (isDuplicateMessage(createMessage, data.group.messages)) {
return data;
}
          // Add our message from the mutation to the end.
data.group.messages.unshift(createMessage);
          // Write our data back to the cache.
store.writeQuery({
query: GROUP_QUERY,
variables: {
groupId,
},
data,
});
},
}),
}),
});

In update, we first retrieve the existing data for the query we want to update (GROUP_QUERY) along with the specific variables we passed to that query. This data comes to us from our Redux store of Apollo data. We check to see if the new Message returned from createMessage already exists (in case of race conditions down the line), and then update the previous query result by sticking the new message in front. We then use this modified data object and rewrite the results to the Apollo store with store.writeQuery, being sure to pass all the variables associated with our query. This will force props to change reference and the component to rerender.

There it is!

Optimistic UI

But wait! There’s more!

update will currently only update the query after the mutation succeeds and a response is sent back on the server. But we don’t want to wait till the server returns data — we crave instant gratification! If a user with shoddy internet tried to send a message and it didn’t show up right away, they’d probably try and send the message again and again and end up sending the message multiple times… and then they’d yell at customer support!

Optimistic UI is our weapon for protecting customer support. We know the shape of the data we expect to receive from the server, so why not fake it until we get a response? react-apollo lets us accomplish this by adding an optimisticResponse parameter to mutate. In our case it looks like this:

Step 4.11: Add optimisticResponse to mutation

Changed client/src/screens/messages.screen.js

...
createMessage: ({ text, userId, groupId }) =>
mutate({
variables: { text, userId, groupId },
optimisticResponse: {
__typename: 'Mutation',
createMessage: {
__typename: 'Message',
id: -1, // don't know id yet, but it doesn't matter
text, // we know what the text will be
createdAt: new Date().toISOString(), // the time is now!
from: {
__typename: 'User',
id: 1, // still faking the user
username: 'Justyn.Kautzer', // still faking the user
},
to: {
__typename: 'Group',
id: groupId,
},
},
},

update: (store, { data: { createMessage } }) => {
// Read the data from our cache for this query.
const data = store.readQuery({
...

The Object returned from optimisticResponse is what the data should look like from our server when the mutation succeeds. We need to specify the __typename for all values in our optimistic response just like our server would. Even though we don’t know all values for all fields, we know enough to populate the ones that will show up in the UI, like the text, user, and message creation time. This will essentially be a placeholder until the server responds.

Let’s also modify our UI a bit so that our FlatList scrolls to the bottom when we send a message as soon as we receive new data:

Step 4.12: Add scrollToEnd to Messages after send

Changed client/src/screens/messages.screen.js

...
    this.props.createMessage({
groupId: this.props.navigation.state.params.groupId,
text,
}).then(() => {
this.flatList.scrollToEnd({ animated: true });
});
...
        style={styles.container}
>
<FlatList
ref={(ref) => { this.flatList = ref; }}
data={group.messages.slice().reverse()}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
...
You’re welcome, person in customer support

🔥🔥🔥!!!

YOUR CHALLENGE

First, let’s take a break. We’ve definitely earned it.

Now that we’re comfortable using GraphQL Queries and Mutations and some tricky stuff in React Native, we can do most of the things we need to do for most basic applications. In fact, there are a number of Chatty features that we can already implement without knowing much else. This post is already plenty long, but there are features left to be built. So with that said, I like to suggest that you try to complete the following features on your own before we move on:

  1. Add GraphQL Mutations on our server for creating, modifying, and deleting Groups
  2. Add new Screens to our React Native app for creating, modifying, and deleting Groups
  3. Build GraphQL Queries and Mutations for our new Screens and connect them using react-apollo
  4. Include update for these new mutations where necessary

Here is the code for this tutorial before these features were implemented.

Also, if you want to see my UI or you want a hint or you don’t wanna write any code, that’s cool too! Here’s my code with these features added.

That’s it for Part 4. In the next part of this series, we will add pagination to our app so we can progressively load data and improve the user experience.

As always, please share your thoughts, questions, struggles, and breakthroughs below!

You can view the code for this tutorial here

You can view the code for this tutorial including the challenge here

Continue to Building Chatty — Part 5 (GraphQL Pagination)