# Step 4: GraphQL Mutations [//]: # (head-end) This is the fourth blog in a multipart series where we will be building Chatty, a WhatsApp clone, using [React Native](https://facebook.github.io/react-native/) and [Apollo](http://dev.apollodata.com/). 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: ```graphql 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: [{]: (diffStep 4.1) #### [Step 4.1: Add Mutations to Schema](https://github.com/srtucker22/chatty/commit/9c0162b) ##### Changed server/data/schema.js ```diff @@ -44,8 +44,16 @@ ┊44┊44┊ group(id: Int!): Group ┊45┊45┊ } ┊46┊46┊ +┊ ┊47┊ type Mutation { +┊ ┊48┊ # send a message to a group +┊ ┊49┊ createMessage( +┊ ┊50┊ text: String!, userId: Int!, groupId: Int! +┊ ┊51┊ ): Message +┊ ┊52┊ } +┊ ┊53┊ ┊47┊54┊ schema { ┊48┊55┊ query: Query +┊ ┊56┊ mutation: Mutation ┊49┊57┊ } ┊50┊58┊`; ``` [}]: # We also need to modify our resolvers to handle our new mutation. We’ll modify `server/data/resolvers.js` as follows: [{]: (diffStep 4.2) #### [Step 4.2: Add Mutations to Resolvers](https://github.com/srtucker22/chatty/commit/42c4fd7) ##### Changed server/data/resolvers.js ```diff @@ -18,6 +18,15 @@ ┊18┊18┊ return User.findOne({ where: args }); ┊19┊19┊ }, ┊20┊20┊ }, +┊ ┊21┊ Mutation: { +┊ ┊22┊ createMessage(_, { text, userId, groupId }) { +┊ ┊23┊ return Message.create({ +┊ ┊24┊ userId, +┊ ┊25┊ text, +┊ ┊26┊ groupId, +┊ ┊27┊ }); +┊ ┊28┊ }, +┊ ┊29┊ }, ┊21┊30┊ Group: { ┊22┊31┊ users(group) { ┊23┊32┊ 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: ![Create Message Img](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step4-2.png) # 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`](https://github.com/oblador/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](https://github.com/oblador/react-native-vector-icons) before moving onto the next step. ```sh # make sure you're adding this package in the client folder!!! cd client npm 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`: [{]: (diffStep 4.3 files="client/src/components/message-input.component.js") #### [Step 4.3: Create MessageInput](https://github.com/srtucker22/chatty/commit/8697057) ##### Added client/src/components/message-input.component.js ```diff @@ -0,0 +1,95 @@ +┊ ┊ 1┊import React, { Component } from 'react'; +┊ ┊ 2┊import PropTypes from 'prop-types'; +┊ ┊ 3┊import { +┊ ┊ 4┊ StyleSheet, +┊ ┊ 5┊ TextInput, +┊ ┊ 6┊ View, +┊ ┊ 7┊} from 'react-native'; +┊ ┊ 8┊ +┊ ┊ 9┊import Icon from 'react-native-vector-icons/FontAwesome'; +┊ ┊10┊ +┊ ┊11┊const styles = StyleSheet.create({ +┊ ┊12┊ container: { +┊ ┊13┊ alignSelf: 'flex-end', +┊ ┊14┊ backgroundColor: '#f5f1ee', +┊ ┊15┊ borderColor: '#dbdbdb', +┊ ┊16┊ borderTopWidth: 1, +┊ ┊17┊ flexDirection: 'row', +┊ ┊18┊ }, +┊ ┊19┊ inputContainer: { +┊ ┊20┊ flex: 1, +┊ ┊21┊ paddingHorizontal: 12, +┊ ┊22┊ paddingVertical: 6, +┊ ┊23┊ }, +┊ ┊24┊ input: { +┊ ┊25┊ backgroundColor: 'white', +┊ ┊26┊ borderColor: '#dbdbdb', +┊ ┊27┊ borderRadius: 15, +┊ ┊28┊ borderWidth: 1, +┊ ┊29┊ color: 'black', +┊ ┊30┊ height: 32, +┊ ┊31┊ paddingHorizontal: 8, +┊ ┊32┊ }, +┊ ┊33┊ sendButtonContainer: { +┊ ┊34┊ paddingRight: 12, +┊ ┊35┊ paddingVertical: 6, +┊ ┊36┊ }, +┊ ┊37┊ sendButton: { +┊ ┊38┊ height: 32, +┊ ┊39┊ width: 32, +┊ ┊40┊ }, +┊ ┊41┊ iconStyle: { +┊ ┊42┊ marginRight: 0, // default is 12 +┊ ┊43┊ }, +┊ ┊44┊}); +┊ ┊45┊ +┊ ┊46┊const sendButton = send => ( +┊ ┊47┊ +┊ ┊57┊); +┊ ┊58┊ +┊ ┊59┊class MessageInput extends Component { +┊ ┊60┊ constructor(props) { +┊ ┊61┊ super(props); +┊ ┊62┊ this.state = {}; +┊ ┊63┊ this.send = this.send.bind(this); +┊ ┊64┊ } +┊ ┊65┊ +┊ ┊66┊ send() { +┊ ┊67┊ this.props.send(this.state.text); +┊ ┊68┊ this.textInput.clear(); +┊ ┊69┊ this.textInput.blur(); +┊ ┊70┊ } +┊ ┊71┊ +┊ ┊72┊ render() { +┊ ┊73┊ return ( +┊ ┊74┊ +┊ ┊75┊ +┊ ┊76┊ { this.textInput = ref; }} +┊ ┊78┊ onChangeText={text => this.setState({ text })} +┊ ┊79┊ style={styles.input} +┊ ┊80┊ placeholder="Type your message here!" +┊ ┊81┊ /> +┊ ┊82┊ +┊ ┊83┊ +┊ ┊84┊ {sendButton(this.send)} +┊ ┊85┊ +┊ ┊86┊ +┊ ┊87┊ ); +┊ ┊88┊ } +┊ ┊89┊} +┊ ┊90┊ +┊ ┊91┊MessageInput.propTypes = { +┊ ┊92┊ send: PropTypes.func.isRequired, +┊ ┊93┊}; +┊ ┊94┊ +┊ ┊95┊export default MessageInput; ``` [}]: # Our `MessageInput` component is a `View` that wraps a controlled `TextInput` and an [`Icon.Button`](https://github.com/oblador/react-native-vector-icons#iconbutton-component). 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: [{]: (diffStep 4.4) #### [Step 4.4: Add MessageInput to Messages](https://github.com/srtucker22/chatty/commit/72a31a8) ##### Changed client/src/screens/messages.screen.js ```diff @@ -10,6 +10,7 @@ ┊10┊10┊import { graphql, compose } from 'react-apollo'; ┊11┊11┊ ┊12┊12┊import Message from '../components/message.component'; +┊ ┊13┊import MessageInput from '../components/message-input.component'; ┊13┊14┊import GROUP_QUERY from '../graphql/group.query'; ┊14┊15┊ ┊15┊16┊const styles = StyleSheet.create({ ``` ```diff @@ -46,6 +47,7 @@ ┊46┊47┊ }; ┊47┊48┊ ┊48┊49┊ this.renderItem = this.renderItem.bind(this); +┊ ┊50┊ this.send = this.send.bind(this); ┊49┊51┊ } ┊50┊52┊ ┊51┊53┊ componentWillReceiveProps(nextProps) { ``` ```diff @@ -65,6 +67,11 @@ ┊65┊67┊ } ┊66┊68┊ } ┊67┊69┊ +┊ ┊70┊ send(text) { +┊ ┊71┊ // TODO: send the message +┊ ┊72┊ console.log(`sending message: ${text}`); +┊ ┊73┊ } +┊ ┊74┊ ┊68┊75┊ keyExtractor = item => item.id.toString(); ┊69┊76┊ ┊70┊77┊ renderItem = ({ item: message }) => ( ``` ```diff @@ -96,6 +103,7 @@ ┊ 96┊103┊ renderItem={this.renderItem} ┊ 97┊104┊ ListEmptyComponent={} ┊ 98┊105┊ /> +┊ ┊106┊ ┊ 99┊107┊ ┊100┊108┊ ); ┊101┊109┊ } ``` [}]: # It should look like this: ![Message Input Image](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step4-4.png) But **don’t be fooled by your simulator!** This UI will break on a phone because of the keyboard: ![Broken Input Image](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step4-4-2.png) 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`](https://facebook.github.io/react-native/docs/keyboardavoidingview.html) to the rescue! [{]: (diffStep 4.5) #### [Step 4.5: Add KeyboardAvoidingView](https://github.com/srtucker22/chatty/commit/8605eb3) ##### Changed client/src/screens/messages.screen.js ```diff @@ -1,6 +1,7 @@ ┊1┊1┊import { ┊2┊2┊ ActivityIndicator, ┊3┊3┊ FlatList, +┊ ┊4┊ KeyboardAvoidingView, ┊4┊5┊ StyleSheet, ┊5┊6┊ View, ┊6┊7┊} from 'react-native'; ``` ```diff @@ -96,7 +97,12 @@ ┊ 96┊ 97┊ ┊ 97┊ 98┊ // render list of messages for group ┊ 98┊ 99┊ return ( -┊ 99┊ ┊ +┊ ┊100┊ ┊100┊106┊ } ┊105┊111┊ /> ┊106┊112┊ -┊107┊ ┊ +┊ ┊113┊ ┊108┊114┊ ); ┊109┊115┊ } ┊110┊116┊} ``` [}]: # ![Fixed Input Image](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step4-5.png) 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: ```graphql 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`: ```graphql 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**](http://graphql.org/learn/queries/#fragments). We can factor out this common set of fields into a `MessageFragment` that looks like this: [{]: (diffStep 4.6) #### [Step 4.6: Create MessageFragment](https://github.com/srtucker22/chatty/commit/1ff1359) ##### Added client/src/graphql/message.fragment.js ```diff @@ -0,0 +1,18 @@ +┊ ┊ 1┊import gql from 'graphql-tag'; +┊ ┊ 2┊ +┊ ┊ 3┊const MESSAGE_FRAGMENT = gql` +┊ ┊ 4┊ fragment MessageFragment on Message { +┊ ┊ 5┊ id +┊ ┊ 6┊ to { +┊ ┊ 7┊ id +┊ ┊ 8┊ } +┊ ┊ 9┊ from { +┊ ┊10┊ id +┊ ┊11┊ username +┊ ┊12┊ } +┊ ┊13┊ createdAt +┊ ┊14┊ text +┊ ┊15┊ } +┊ ┊16┊`; +┊ ┊17┊ +┊ ┊18┊export default MESSAGE_FRAGMENT; ``` [}]: # Now we can apply `MESSAGE_FRAGMENT` to `GROUP_QUERY` by changing our code as follows: [{]: (diffStep 4.7) #### [Step 4.7: Add MessageFragment to Group Query](https://github.com/srtucker22/chatty/commit/8cfe14f) ##### Changed client/src/graphql/group.query.js ```diff @@ -1,5 +1,7 @@ ┊1┊1┊import gql from 'graphql-tag'; ┊2┊2┊ +┊ ┊3┊import MESSAGE_FRAGMENT from './message.fragment'; +┊ ┊4┊ ┊3┊5┊const GROUP_QUERY = gql` ┊4┊6┊ query group($groupId: Int!) { ┊5┊7┊ group(id: $groupId) { ``` ```diff @@ -10,16 +12,11 @@ ┊10┊12┊ username ┊11┊13┊ } ┊12┊14┊ messages { -┊13┊ ┊ id -┊14┊ ┊ from { -┊15┊ ┊ id -┊16┊ ┊ username -┊17┊ ┊ } -┊18┊ ┊ createdAt -┊19┊ ┊ text +┊ ┊15┊ ... MessageFragment ┊20┊16┊ } ┊21┊17┊ } ┊22┊18┊ } +┊ ┊19┊ ${MESSAGE_FRAGMENT} ┊23┊20┊`; ┊24┊21┊ ┊25┊22┊export default GROUP_QUERY; ``` [}]: # Let’s also write our `createMessage` mutation using `messageFragment` in a new file `client/src/graphql/create-message.mutation.js`: [{]: (diffStep 4.8) #### [Step 4.8: Create CREATE_MESSAGE_MUTATION](https://github.com/srtucker22/chatty/commit/5318b34) ##### Added client/src/graphql/create-message.mutation.js ```diff @@ -0,0 +1,14 @@ +┊ ┊ 1┊import gql from 'graphql-tag'; +┊ ┊ 2┊ +┊ ┊ 3┊import MESSAGE_FRAGMENT from './message.fragment'; +┊ ┊ 4┊ +┊ ┊ 5┊const CREATE_MESSAGE_MUTATION = gql` +┊ ┊ 6┊ mutation createMessage($text: String!, $userId: Int!, $groupId: Int!) { +┊ ┊ 7┊ createMessage(text: $text, userId: $userId, groupId: $groupId) { +┊ ┊ 8┊ ... MessageFragment +┊ ┊ 9┊ } +┊ ┊10┊ } +┊ ┊11┊ ${MESSAGE_FRAGMENT} +┊ ┊12┊`; +┊ ┊13┊ +┊ ┊14┊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: ```js 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`: [{]: (diffStep 4.9) #### [Step 4.9: Add CREATE_MESSAGE_MUTATION to Messages](https://github.com/srtucker22/chatty/commit/e002433) ##### Changed client/src/screens/messages.screen.js ```diff @@ -13,6 +13,7 @@ ┊13┊13┊import Message from '../components/message.component'; ┊14┊14┊import MessageInput from '../components/message-input.component'; ┊15┊15┊import GROUP_QUERY from '../graphql/group.query'; +┊ ┊16┊import CREATE_MESSAGE_MUTATION from '../graphql/create-message.mutation'; ┊16┊17┊ ┊17┊18┊const styles = StyleSheet.create({ ┊18┊19┊ container: { ``` ```diff @@ -69,8 +70,11 @@ ┊69┊70┊ } ┊70┊71┊ ┊71┊72┊ send(text) { -┊72┊ ┊ // TODO: send the message -┊73┊ ┊ console.log(`sending message: ${text}`); +┊ ┊73┊ this.props.createMessage({ +┊ ┊74┊ groupId: this.props.navigation.state.params.groupId, +┊ ┊75┊ userId: 1, // faking the user for now +┊ ┊76┊ text, +┊ ┊77┊ }); ┊74┊78┊ } ┊75┊79┊ ┊76┊80┊ keyExtractor = item => item.id.toString(); ``` ```diff @@ -116,6 +120,14 @@ ┊116┊120┊} ┊117┊121┊ ┊118┊122┊Messages.propTypes = { +┊ ┊123┊ createMessage: PropTypes.func, +┊ ┊124┊ navigation: PropTypes.shape({ +┊ ┊125┊ state: PropTypes.shape({ +┊ ┊126┊ params: PropTypes.shape({ +┊ ┊127┊ groupId: PropTypes.number, +┊ ┊128┊ }), +┊ ┊129┊ }), +┊ ┊130┊ }), ┊119┊131┊ group: PropTypes.shape({ ┊120┊132┊ messages: PropTypes.array, ┊121┊133┊ users: PropTypes.array, ``` ```diff @@ -134,6 +146,16 @@ ┊134┊146┊ }), ┊135┊147┊}); ┊136┊148┊ +┊ ┊149┊const createMessageMutation = graphql(CREATE_MESSAGE_MUTATION, { +┊ ┊150┊ props: ({ mutate }) => ({ +┊ ┊151┊ createMessage: ({ text, userId, groupId }) => +┊ ┊152┊ mutate({ +┊ ┊153┊ variables: { text, userId, groupId }, +┊ ┊154┊ }), +┊ ┊155┊ }), +┊ ┊156┊}); +┊ ┊157┊ ┊137┊158┊export default compose( ┊138┊159┊ groupQuery, +┊ ┊160┊ createMessageMutation, ┊139┊161┊)(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: ![Send Fail Gif](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step4-9.gif) 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: [{]: (diffStep "4.10") #### [Step 4.10: Add update to mutation](https://github.com/srtucker22/chatty/commit/49b732b) ##### Changed client/src/screens/messages.screen.js ```diff @@ -151,7 +151,29 @@ ┊151┊151┊ createMessage: ({ text, userId, groupId }) => ┊152┊152┊ mutate({ ┊153┊153┊ variables: { text, userId, groupId }, +┊ ┊154┊ update: (store, { data: { createMessage } }) => { +┊ ┊155┊ // Read the data from our cache for this query. +┊ ┊156┊ const groupData = store.readQuery({ +┊ ┊157┊ query: GROUP_QUERY, +┊ ┊158┊ variables: { +┊ ┊159┊ groupId, +┊ ┊160┊ }, +┊ ┊161┊ }); +┊ ┊162┊ +┊ ┊163┊ // Add our message from the mutation to the end. +┊ ┊164┊ groupData.group.messages.unshift(createMessage); +┊ ┊165┊ +┊ ┊166┊ // Write our data back to the cache. +┊ ┊167┊ store.writeQuery({ +┊ ┊168┊ query: GROUP_QUERY, +┊ ┊169┊ variables: { +┊ ┊170┊ groupId, +┊ ┊171┊ }, +┊ ┊172┊ data: groupData, +┊ ┊173┊ }); +┊ ┊174┊ }, ┊154┊175┊ }), +┊ ┊176┊ ┊155┊177┊ }), ┊156┊178┊}); ``` [}]: # 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. ![Fixed Send Gif](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step4-10.gif) # 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: [{]: (diffStep 4.11) #### [Step 4.11: Add optimisticResponse to mutation](https://github.com/srtucker22/chatty/commit/462d8c4) ##### Changed client/src/screens/messages.screen.js ```diff @@ -151,6 +151,24 @@ ┊151┊151┊ createMessage: ({ text, userId, groupId }) => ┊152┊152┊ mutate({ ┊153┊153┊ variables: { text, userId, groupId }, +┊ ┊154┊ optimisticResponse: { +┊ ┊155┊ __typename: 'Mutation', +┊ ┊156┊ createMessage: { +┊ ┊157┊ __typename: 'Message', +┊ ┊158┊ id: -1, // don't know id yet, but it doesn't matter +┊ ┊159┊ text, // we know what the text will be +┊ ┊160┊ createdAt: new Date().toISOString(), // the time is now! +┊ ┊161┊ from: { +┊ ┊162┊ __typename: 'User', +┊ ┊163┊ id: 1, // still faking the user +┊ ┊164┊ username: 'Justyn.Kautzer', // still faking the user +┊ ┊165┊ }, +┊ ┊166┊ to: { +┊ ┊167┊ __typename: 'Group', +┊ ┊168┊ id: groupId, +┊ ┊169┊ }, +┊ ┊170┊ }, +┊ ┊171┊ }, ┊154┊172┊ update: (store, { data: { createMessage } }) => { ┊155┊173┊ // Read the data from our cache for this query. ┊156┊174┊ 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: [{]: (diffStep 4.12) #### [Step 4.12: Add scrollToEnd to Messages after send](https://github.com/srtucker22/chatty/commit/c4517e3) ##### Changed client/src/screens/messages.screen.js ```diff @@ -74,6 +74,8 @@ ┊74┊74┊ groupId: this.props.navigation.state.params.groupId, ┊75┊75┊ userId: 1, // faking the user for now ┊76┊76┊ text, +┊ ┊77┊ }).then(() => { +┊ ┊78┊ this.flatList.scrollToEnd({ animated: true }); ┊77┊79┊ }); ┊78┊80┊ } ┊79┊81┊ ``` ```diff @@ -108,6 +110,7 @@ ┊108┊110┊ style={styles.container} ┊109┊111┊ > ┊110┊112┊ { this.flatList = ref; }} ┊111┊114┊ data={group.messages.slice().reverse()} ┊112┊115┊ keyExtractor={this.keyExtractor} ┊113┊116┊ renderItem={this.renderItem} ``` [}]: # ![Scroll to Bottom Gif](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step4-12.gif) ### 🔥🔥🔥!!! # **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 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. ![Groups Gif](https://github.com/srtucker22/chatty/blob/master/.tortilla/media/step4-13.gif) [{]: (diffStep 4.13) #### [Step 4.13: Add Group Mutations and Screens](https://github.com/srtucker22/chatty/commit/730b756) ##### Changed client/package.json ```diff @@ -15,6 +15,7 @@ ┊15┊15┊ "apollo-link-redux": "^0.2.1", ┊16┊16┊ "graphql": "^0.12.3", ┊17┊17┊ "graphql-tag": "^2.4.2", +┊ ┊18┊ "immutability-helper": "^2.6.4", ┊18┊19┊ "lodash": "^4.17.5", ┊19┊20┊ "moment": "^2.20.1", ┊20┊21┊ "prop-types": "^15.6.0", ``` ```diff @@ -22,6 +23,7 @@ ┊22┊23┊ "react": "16.4.1", ┊23┊24┊ "react-apollo": "^2.0.4", ┊24┊25┊ "react-native": "0.56.0", +┊ ┊26┊ "react-native-alpha-listview": "^0.2.1", ┊25┊27┊ "react-native-vector-icons": "^4.6.0", ┊26┊28┊ "react-navigation": "^1.0.3", ┊27┊29┊ "react-navigation-redux-helpers": "^1.1.2", ``` ##### Added client/src/components/selected-user-list.component.js ```diff @@ -0,0 +1,118 @@ +┊ ┊ 1┊import React, { Component } from 'react'; +┊ ┊ 2┊import PropTypes from 'prop-types'; +┊ ┊ 3┊import { +┊ ┊ 4┊ FlatList, +┊ ┊ 5┊ Image, +┊ ┊ 6┊ StyleSheet, +┊ ┊ 7┊ Text, +┊ ┊ 8┊ TouchableOpacity, +┊ ┊ 9┊ View, +┊ ┊ 10┊} from 'react-native'; +┊ ┊ 11┊import Icon from 'react-native-vector-icons/FontAwesome'; +┊ ┊ 12┊ +┊ ┊ 13┊const styles = StyleSheet.create({ +┊ ┊ 14┊ list: { +┊ ┊ 15┊ paddingVertical: 8, +┊ ┊ 16┊ }, +┊ ┊ 17┊ itemContainer: { +┊ ┊ 18┊ alignItems: 'center', +┊ ┊ 19┊ paddingHorizontal: 12, +┊ ┊ 20┊ }, +┊ ┊ 21┊ itemIcon: { +┊ ┊ 22┊ alignItems: 'center', +┊ ┊ 23┊ backgroundColor: '#dbdbdb', +┊ ┊ 24┊ borderColor: 'white', +┊ ┊ 25┊ borderRadius: 10, +┊ ┊ 26┊ borderWidth: 2, +┊ ┊ 27┊ flexDirection: 'row', +┊ ┊ 28┊ height: 20, +┊ ┊ 29┊ justifyContent: 'center', +┊ ┊ 30┊ position: 'absolute', +┊ ┊ 31┊ right: -3, +┊ ┊ 32┊ top: -3, +┊ ┊ 33┊ width: 20, +┊ ┊ 34┊ }, +┊ ┊ 35┊ itemImage: { +┊ ┊ 36┊ borderRadius: 27, +┊ ┊ 37┊ height: 54, +┊ ┊ 38┊ width: 54, +┊ ┊ 39┊ }, +┊ ┊ 40┊}); +┊ ┊ 41┊ +┊ ┊ 42┊export class SelectedUserListItem extends Component { +┊ ┊ 43┊ constructor(props) { +┊ ┊ 44┊ super(props); +┊ ┊ 45┊ +┊ ┊ 46┊ this.remove = this.remove.bind(this); +┊ ┊ 47┊ } +┊ ┊ 48┊ +┊ ┊ 49┊ remove() { +┊ ┊ 50┊ this.props.remove(this.props.user); +┊ ┊ 51┊ } +┊ ┊ 52┊ +┊ ┊ 53┊ render() { +┊ ┊ 54┊ const { username } = this.props.user; +┊ ┊ 55┊ +┊ ┊ 56┊ return ( +┊ ┊ 57┊ +┊ ┊ 60┊ +┊ ┊ 61┊ +┊ ┊ 65┊ +┊ ┊ 66┊ +┊ ┊ 71┊ +┊ ┊ 72┊ +┊ ┊ 73┊ {username} +┊ ┊ 74┊ +┊ ┊ 75┊ ); +┊ ┊ 76┊ } +┊ ┊ 77┊} +┊ ┊ 78┊SelectedUserListItem.propTypes = { +┊ ┊ 79┊ user: PropTypes.shape({ +┊ ┊ 80┊ id: PropTypes.number, +┊ ┊ 81┊ username: PropTypes.string, +┊ ┊ 82┊ }), +┊ ┊ 83┊ remove: PropTypes.func, +┊ ┊ 84┊}; +┊ ┊ 85┊ +┊ ┊ 86┊class SelectedUserList extends Component { +┊ ┊ 87┊ constructor(props) { +┊ ┊ 88┊ super(props); +┊ ┊ 89┊ +┊ ┊ 90┊ this.renderItem = this.renderItem.bind(this); +┊ ┊ 91┊ } +┊ ┊ 92┊ +┊ ┊ 93┊ keyExtractor = item => item.id.toString(); +┊ ┊ 94┊ +┊ ┊ 95┊ renderItem({ item: user }) { +┊ ┊ 96┊ return ( +┊ ┊ 97┊ +┊ ┊ 98┊ ); +┊ ┊ 99┊ } +┊ ┊100┊ +┊ ┊101┊ render() { +┊ ┊102┊ return ( +┊ ┊103┊ +┊ ┊110┊ ); +┊ ┊111┊ } +┊ ┊112┊} +┊ ┊113┊SelectedUserList.propTypes = { +┊ ┊114┊ data: PropTypes.arrayOf(PropTypes.object), +┊ ┊115┊ remove: PropTypes.func, +┊ ┊116┊}; +┊ ┊117┊ +┊ ┊118┊export default SelectedUserList; ``` ##### Added client/src/graphql/create-group.mutation.js ```diff @@ -0,0 +1,15 @@ +┊ ┊ 1┊import gql from 'graphql-tag'; +┊ ┊ 2┊ +┊ ┊ 3┊const CREATE_GROUP_MUTATION = gql` +┊ ┊ 4┊ mutation createGroup($name: String!, $userIds: [Int!], $userId: Int!) { +┊ ┊ 5┊ createGroup(name: $name, userIds: $userIds, userId: $userId) { +┊ ┊ 6┊ id +┊ ┊ 7┊ name +┊ ┊ 8┊ users { +┊ ┊ 9┊ id +┊ ┊10┊ } +┊ ┊11┊ } +┊ ┊12┊ } +┊ ┊13┊`; +┊ ┊14┊ +┊ ┊15┊export default CREATE_GROUP_MUTATION; ``` ##### Added client/src/graphql/delete-group.mutation.js ```diff @@ -0,0 +1,11 @@ +┊ ┊ 1┊import gql from 'graphql-tag'; +┊ ┊ 2┊ +┊ ┊ 3┊const DELETE_GROUP_MUTATION = gql` +┊ ┊ 4┊ mutation deleteGroup($id: Int!) { +┊ ┊ 5┊ deleteGroup(id: $id) { +┊ ┊ 6┊ id +┊ ┊ 7┊ } +┊ ┊ 8┊ } +┊ ┊ 9┊`; +┊ ┊10┊ +┊ ┊11┊export default DELETE_GROUP_MUTATION; ``` ##### Added client/src/graphql/leave-group.mutation.js ```diff @@ -0,0 +1,11 @@ +┊ ┊ 1┊import gql from 'graphql-tag'; +┊ ┊ 2┊ +┊ ┊ 3┊const LEAVE_GROUP_MUTATION = gql` +┊ ┊ 4┊ mutation leaveGroup($id: Int!, $userId: Int!) { +┊ ┊ 5┊ leaveGroup(id: $id, userId: $userId) { +┊ ┊ 6┊ id +┊ ┊ 7┊ } +┊ ┊ 8┊ } +┊ ┊ 9┊`; +┊ ┊10┊ +┊ ┊11┊export default LEAVE_GROUP_MUTATION; ``` ##### Changed client/src/graphql/user.query.js ```diff @@ -11,6 +11,10 @@ ┊11┊11┊ id ┊12┊12┊ name ┊13┊13┊ } +┊ ┊14┊ friends { +┊ ┊15┊ id +┊ ┊16┊ username +┊ ┊17┊ } ┊14┊18┊ } ┊15┊19┊ } ┊16┊20┊`; ``` ##### Changed client/src/navigation.js ```diff @@ -10,6 +10,9 @@ ┊10┊10┊ ┊11┊11┊import Groups from './screens/groups.screen'; ┊12┊12┊import Messages from './screens/messages.screen'; +┊ ┊13┊import FinalizeGroup from './screens/finalize-group.screen'; +┊ ┊14┊import GroupDetails from './screens/group-details.screen'; +┊ ┊15┊import NewGroup from './screens/new-group.screen'; ┊13┊16┊ ┊14┊17┊const styles = StyleSheet.create({ ┊15┊18┊ container: { ``` ```diff @@ -47,6 +50,9 @@ ┊47┊50┊const AppNavigator = StackNavigator({ ┊48┊51┊ Main: { screen: MainScreenNavigator }, ┊49┊52┊ Messages: { screen: Messages }, +┊ ┊53┊ GroupDetails: { screen: GroupDetails }, +┊ ┊54┊ NewGroup: { screen: NewGroup }, +┊ ┊55┊ FinalizeGroup: { screen: FinalizeGroup }, ┊50┊56┊}, { ┊51┊57┊ mode: 'modal', ┊52┊58┊}); ``` ##### Added client/src/screens/finalize-group.screen.js ```diff @@ -0,0 +1,261 @@ +┊ ┊ 1┊import { _ } from 'lodash'; +┊ ┊ 2┊import React, { Component } from 'react'; +┊ ┊ 3┊import PropTypes from 'prop-types'; +┊ ┊ 4┊import { +┊ ┊ 5┊ Alert, +┊ ┊ 6┊ Button, +┊ ┊ 7┊ Image, +┊ ┊ 8┊ StyleSheet, +┊ ┊ 9┊ Text, +┊ ┊ 10┊ TextInput, +┊ ┊ 11┊ TouchableOpacity, +┊ ┊ 12┊ View, +┊ ┊ 13┊} from 'react-native'; +┊ ┊ 14┊import { graphql, compose } from 'react-apollo'; +┊ ┊ 15┊import { NavigationActions } from 'react-navigation'; +┊ ┊ 16┊import update from 'immutability-helper'; +┊ ┊ 17┊ +┊ ┊ 18┊import { USER_QUERY } from '../graphql/user.query'; +┊ ┊ 19┊import CREATE_GROUP_MUTATION from '../graphql/create-group.mutation'; +┊ ┊ 20┊import SelectedUserList from '../components/selected-user-list.component'; +┊ ┊ 21┊ +┊ ┊ 22┊const goToNewGroup = group => NavigationActions.reset({ +┊ ┊ 23┊ index: 1, +┊ ┊ 24┊ actions: [ +┊ ┊ 25┊ NavigationActions.navigate({ routeName: 'Main' }), +┊ ┊ 26┊ NavigationActions.navigate({ routeName: 'Messages', params: { groupId: group.id, title: group.name } }), +┊ ┊ 27┊ ], +┊ ┊ 28┊}); +┊ ┊ 29┊ +┊ ┊ 30┊const styles = StyleSheet.create({ +┊ ┊ 31┊ container: { +┊ ┊ 32┊ flex: 1, +┊ ┊ 33┊ backgroundColor: 'white', +┊ ┊ 34┊ }, +┊ ┊ 35┊ detailsContainer: { +┊ ┊ 36┊ padding: 20, +┊ ┊ 37┊ flexDirection: 'row', +┊ ┊ 38┊ }, +┊ ┊ 39┊ imageContainer: { +┊ ┊ 40┊ paddingRight: 20, +┊ ┊ 41┊ alignItems: 'center', +┊ ┊ 42┊ }, +┊ ┊ 43┊ inputContainer: { +┊ ┊ 44┊ flexDirection: 'column', +┊ ┊ 45┊ flex: 1, +┊ ┊ 46┊ }, +┊ ┊ 47┊ input: { +┊ ┊ 48┊ color: 'black', +┊ ┊ 49┊ height: 32, +┊ ┊ 50┊ }, +┊ ┊ 51┊ inputBorder: { +┊ ┊ 52┊ borderColor: '#dbdbdb', +┊ ┊ 53┊ borderBottomWidth: 1, +┊ ┊ 54┊ borderTopWidth: 1, +┊ ┊ 55┊ paddingVertical: 8, +┊ ┊ 56┊ }, +┊ ┊ 57┊ inputInstructions: { +┊ ┊ 58┊ paddingTop: 6, +┊ ┊ 59┊ color: '#777', +┊ ┊ 60┊ fontSize: 12, +┊ ┊ 61┊ }, +┊ ┊ 62┊ groupImage: { +┊ ┊ 63┊ width: 54, +┊ ┊ 64┊ height: 54, +┊ ┊ 65┊ borderRadius: 27, +┊ ┊ 66┊ }, +┊ ┊ 67┊ selected: { +┊ ┊ 68┊ flexDirection: 'row', +┊ ┊ 69┊ }, +┊ ┊ 70┊ loading: { +┊ ┊ 71┊ justifyContent: 'center', +┊ ┊ 72┊ flex: 1, +┊ ┊ 73┊ }, +┊ ┊ 74┊ navIcon: { +┊ ┊ 75┊ color: 'blue', +┊ ┊ 76┊ fontSize: 18, +┊ ┊ 77┊ paddingTop: 2, +┊ ┊ 78┊ }, +┊ ┊ 79┊ participants: { +┊ ┊ 80┊ paddingHorizontal: 20, +┊ ┊ 81┊ paddingVertical: 6, +┊ ┊ 82┊ backgroundColor: '#dbdbdb', +┊ ┊ 83┊ color: '#777', +┊ ┊ 84┊ }, +┊ ┊ 85┊}); +┊ ┊ 86┊ +┊ ┊ 87┊class FinalizeGroup extends Component { +┊ ┊ 88┊ static navigationOptions = ({ navigation }) => { +┊ ┊ 89┊ const { state } = navigation; +┊ ┊ 90┊ const isReady = state.params && state.params.mode === 'ready'; +┊ ┊ 91┊ return { +┊ ┊ 92┊ title: 'New Group', +┊ ┊ 93┊ headerRight: ( +┊ ┊ 94┊ isReady ?