Building Chatty — Part 4: GraphQL Mutations
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.
Here’s what we will accomplish in this tutorial:
- Design GraphQL Mutations and add them to the GraphQL Schemas on our server
- Modify the layout on our React Native client to let users send Messages
- Build GraphQL Mutations on our RN client and connect them to components using
react-apollo
- 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
- Add GraphQL Mutations on our server for creating, modifying, and deleting Groups
- Add new Screens to our React Native app for creating, modifying, and deleting Groups
- 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 like 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 GraphQL Playground to make sure everything works:
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 clientnpm i 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 } from 'react';
import PropTypes from 'prop-types';
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({... }; this.renderItem = this.renderItem.bind(this);
this.send = this.send.bind(this);
} componentWillReceiveProps(nextProps) {... }
} send(text) {
// TODO: send the message
console.log(`sending message: ${text}`);
} keyExtractor = item => item.id.toString(); renderItem = ({ item: message }) => (... renderItem={this.renderItem}
ListEmptyComponent={<View />}
/>
<MessageInput send={this.send} />
</View>
);
}
It should look like this:
But don’t be fooled by your simulator! This UI will break on a phone because of the keyboard:
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}... ListEmptyComponent={<View />}
/>
<MessageInput send={this.send} />
</KeyboardAvoidingView>
);
}
}
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 GraphQL Playground:
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) {... 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';const styles = StyleSheet.create({
container: {... } send(text) {
this.props.createMessage({
groupId: this.props.navigation.state.params.groupId,
userId: 1, // faking the user for now
text,
});
} keyExtractor = item => item.id.toString();...}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 send
with 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:
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
... createMessage: ({ text, userId, groupId }) =>
mutate({
variables: { text, userId, groupId },
update: (store, { data: { createMessage } }) => {
// Read the data from our cache for this query.
const groupData = store.readQuery({
query: GROUP_QUERY,
variables: {
groupId,
},
}); // Add our message from the mutation to the end.
groupData.group.messages.unshift(createMessage); // Write our data back to the cache.
store.writeQuery({
query: GROUP_QUERY,
variables: {
groupId,
},
data: groupData,
});
},
}), }),
});
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.
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 groupData = 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
... groupId: this.props.navigation.state.params.groupId,
userId: 1, // faking the user for now
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}
🔥🔥🔥!!!
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:
- Add GraphQL Mutations on our server for creating, modifying, and deleting
Groups
- Add new Screens to our React Native app for creating, modifying, and deleting
Groups
- Build GraphQL Queries and Mutations for our new Screens and connect them using
react-apollo
- Include
update
for these new mutations where necessary
If you want to see some UI or you want a hint or you don’t wanna write any code, that’s cool too! Below is some 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!