Chapter 6: Mutations
Full Stack React Native
link Mutations
By the end of this lesson, developers will be able to:
- Query a paginated GraphQL response
- Pass parameters to GraphQL mutations
- Create a mutation to update bitcoins
- Refetch a list of favorite bitcoins
link The Story So Far
So far, we set up a React Native application and an Express server. The app is designed to fetch bitcoins from the server.
App Setup
git clone https://github.com/Maelstroms38/bitcoins-starter.git
cd bitcoins-starter
yarn
yarn start
Server Setup
git clone https://github.com/Maelstroms38/bitcoins-api
cd bitcoins-api
yarn
yarn db:reset
yarn start
Setting Up
Let's start by opening and running both projects in two separate windows.
cd <app-name>
yarn start
cd <server-name>
yarn start
Once you have confirmed both projects running, we will create our first context provider.
Heads Up! This lesson's code edits will only affect your React Native project.
link Mutations
Mutations will grant us the ability to alter data on our GraphQL backend. We will reference a new hook, called useMutation
to create bitcoin updates.
link Apollo Provider
Install the Apollo Client and it's dependencies:
yarn add @apollo/react-hooks apollo-client apollo-cache-inmemory apollo-link-http graphql graphql-tag
Create a new file called ApolloClient.js
:
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
const BASE_URL = 'http://localhost:4000';
const httpLink = new HttpLink({
uri: BASE_URL
});
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache()
});
export default client;
Make the following changes inside your App.js
:
/* ... */
+import { ApolloProvider } from '@apollo/react-hooks';
export default function App(props) {
return (
+ <ApolloProvider client={client}>
<View style={styles.container}>
{/* ... */}
</View>
+ </ApolloProvider>
);
}
}
/* ... */
With all those changes in place, reload the application. If everything worked you should see the same app, but with the apollo client!
We can expect to see no coins listed on the HomeScreen
yet.
Now let's add the necessary queries to request coins for this screen.
link Pagination
Using fetchMore
In Apollo, the easiest way to do pagination is with a function called fetchMore
, which is included in the result object returned by the useQuery Hook. This basically allows you to do a new GraphQL query and merge the result into the original result.
You can specify what query and variables to use for the new query, and how to merge the new query result with the existing data on the client. How exactly you do that will determine what kind of pagination you are implementing.
Offset-based Pagination
Offset-based pagination — also called numbered pages — is a very common pattern, found on many websites, because it is usually the easiest to implement on the backend. In SQL for example, numbered pages can easily be generated by using OFFSET and LIMIT.
link Infinite Scroll
The onEndReached
method will add more bitcoins to the list, each time the user scrolls to the bottom.
Let's make the following changes to HomeScreen.js
:
+import React, { useEffect } from 'react';
import {
Platform,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
Image,
+ FlatList,
+ ActivityIndicator
} from 'react-native';
+import { useQuery } from '@apollo/react-hooks';
import { MonoText } from '../components/StyledText';
import Coin from '@components/Coin';
+import gql from 'graphql-tag';
- const coins = [{
- "id": 1,
- "name": "Bitcoin",
- "symbol": "BTC",
- "price": "$ 1,012.49",
- "imageUrl":"https://www.cryptocompare.com/media/19633/btc.png"
- },
- {
- "id": 2,
- "name": "Ethereum",
- "symbol": "ETH",
- "price": "$ 186.49",
- "imageUrl":"https://www.cryptocompare.com/media/20646/eth_logo.png",
- },
- {
- "id": 3,
- "name": "Litecoin",
- "symbol": "LTC",
- "price": "$ 72.52",
- "imageUrl": "https://www.cryptocompare.com/media/35309662/ltc.png"
- }];
+// Queries
+const BITCOINS_QUERY = gql`
+ query Bitcoins($offset: Int, $limit: Int) {
+ bitcoins(offset: $offset, limit: $limit) {
+ name
+ symbol
+ imageUrl
+ price
+ }
+ }
+`;
+
export default function HomeScreen(props) {
+ const { navigation } = props;
+ const { data, fetchMore } = useQuery(
+ BITCOINS_QUERY,
+ {
+ variables: {
+ offset: 0,
+ limit: 10
+ },
+ fetchPolicy: "cache-and-network"
+ }
+ );
+ if (!data || !data.bitcoins) {
+ return <ActivityIndicator color="#FFF" style={{...StyleSheet.absoluteFillObject}} />
+ }
return (
<View style={styles.container}>
<FlatList
style={styles.container}
contentContainerStyle={styles.contentContainer}
- data={coins}
+ data={data.bitcoins}
+ onEndReached={() => {
+ fetchMore({
+ variables: {
+ offset: data.bitcoins.length
+ },
+ updateQuery: (prev, { fetchMoreResult }) => {
+ if (!fetchMoreResult) return prev;
+ return Object.assign({}, prev, {
+ bitcoins: [...prev.bitcoins, ...fetchMoreResult.bitcoins]
+ });
+ }
+ })
+ }}
+ onEndReachedThreshold={0.9}
keyExtractor={(item, index) => {
return `${index}`;
}}
renderItem={({item, index}) => {
return <Coin coin={item} onPress={() => navigation.navigate('Detail', {coin: item})} />
}
}
/>
</View>
);
}
HomeScreen.navigationOptions = {
title: "Home",
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#161616',
},
contentContainer: {
paddingTop: 10,
paddingBottom: 85
},
});
As you can see, fetchMore
is accessible through the useQuery Hook result object. By default, fetchMore
will use the original query, so we just pass in new variables. Once the new data is returned from the server, the updateQuery
function is used to merge it with the existing data, which will cause a re-render of your UI component with an expanded list.
Nicely done, savor this moment!
Fetch Policy
The fetch policy is an option which allows you to specify how you want your component to interact with the Apollo data cache.
link Favorite Coins
Adding favorites will allow us to track the prices of specific coins.
This feature requires two new mutations, addCoin
and removeCoin
.
Prior to adding these two new mutations, let's bring over the RoundedButton
component from the previous unit.
Create a new file inside components
and paste in the RoundedButton
code.
Here's the RoundedButton source code, in case you need it.
import React from 'react';
import { Text, View, TouchableOpacity, StyleSheet } from 'react-native';
export default function RoundedButton(props) {
const { text, icon, textColor, backgroundColor, onPress } = props;
const color = textColor || 'white';
return (
<TouchableOpacity
onPress={() => onPress && onPress()}
style={[
styles.wrapper,
{ backgroundColor: backgroundColor || 'transparent' },
]}
>
<View style={styles.ButtonTextWrapper}>
{icon}
<Text style={[{ color }, styles.buttonText]}>{text}</Text>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
wrapper: {
padding: 15,
display: 'flex',
borderRadius: 40,
borderWidth: 1,
borderColor: 'white',
marginBottom: 15,
alignItems: 'center'
},
buttonText: {
fontSize: 16,
width: '100%',
textAlign: 'center'
},
ButtonTextWrapper: {
flexDirection: 'row',
justifyContent: 'flex-end'
}
});
With RoundedButton
back, we can make the final changes to CoinDetail.js
:
Click here for the full `CoinDetail` source code.
import React, { useContext } from 'react';
import {
StyleSheet,
Text,
Image,
TouchableOpacity,
View,
ScrollView,
FlatList,
} from 'react-native';
import RoundedButton from '@components/RoundedButton';
import Coin from '@components/Coin';
import { Ionicons } from '@expo/vector-icons';
import { useQuery, useMutation } from '@apollo/react-hooks';
import gql from 'graphql-tag';
// Queries
const FETCH_FAVORITES = gql`
query {
favorites {
name
symbol
price
imageUrl
favorite
}
}
`;
// Mutations
const ADD_COIN = gql`
mutation AddCoin($symbol:String!) {
addCoin(symbol: $symbol) {
name
symbol
price
imageUrl
favorite
}
}
`;
const REMOVE_COIN = gql`
mutation RemoveCoin($symbol:String!) {
removeCoin(symbol: $symbol) {
name
symbol
price
favorite
}
}
`;
function CoinDetail({ route, navigation }) {
const { data, refetch } = useQuery(FETCH_FAVORITES);
const [addCoin] = useMutation(ADD_COIN);
const [removeCoin] = useMutation(REMOVE_COIN);
const { params } = route;
const { coin } = params;
const {
id,
symbol,
name,
price,
imageUrl,
} = coin;
const isFavorite = data && data.favorites && data.favorites.find(coin => coin.symbol == symbol);
const primaryColor = isFavorite ? "rgba(75, 148, 214, 1)" : "#fff";
const secondaryColor = isFavorite ? "#fff" : "rgba(75, 148, 214, 1)";
const saveString = isFavorite ? `Remove ${symbol}` : `Save ${symbol}`;
return (
<View style={styles.container}>
<View style={styles.header}>
<Image style={styles.image} source={{uri: imageUrl}} />
<Text numberOfLines={1} style={styles.text}>{name}</Text>
<RoundedButton
text={saveString}
textColor={primaryColor}
backgroundColor={secondaryColor}
onPress={() => {
if (isFavorite) {
removeCoin({ variables: { symbol } })
.then(() => refetch())
.catch(err => console.log(err))
} else {
addCoin({ variables: { symbol } })
.then(() => refetch())
.catch(err => console.log(err))
}
}}
icon={<Ionicons name="md-checkmark-circle" size={20} color={primaryColor} style={styles.saveIcon} />}
/>
<View style={styles.statRow}>
<Text style={styles.stat} numberOfLines={1}>Price</Text>
<Text style={styles.stat} numberOfLines={1}>{price}</Text>
</View>
</View>
<View style={styles.statsContainer}>
{!!data && !!data.favorites && (
<FlatList
style={styles.container}
contentContainerStyle={styles.contentContainer}
data={data.favorites}
keyExtractor={(item, index) => {
return `${index}`;
}}
renderItem={(coin, index) => {
const {item} = coin;
return <Coin coin={item} onPress={() => navigation.navigate('Detail', {coin: item})} />
}
}
/>
)}
</View>
</View>
);
}
CoinDetail.navigationOptions = screenProps => ({
title: screenProps.navigation.getParam("coin").symbol
});
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flex: 38,
justifyContent: 'center',
alignItems: 'center',
padding: 10,
},
text: {
fontSize: 32,
color: '#161616',
paddingBottom: 15,
},
image: {
width: 60,
height: 60,
resizeMode: 'cover',
},
statsContainer: {
flex: 62,
backgroundColor: '#161616',
},
statRow: {
width: "100%",
padding: 10,
flexDirection: 'row',
justifyContent: 'space-between',
},
stat: {
color: '#161616',
fontSize: 16,
fontWeight: '500',
},
saveIcon: {
position: 'relative',
left: 20,
zIndex: 8
},
contentContainer: {
paddingTop: 10
},
});
export default CoinDetail;
By now, your favorites should appear listed below the CoinDetail
header. Each time a new coin is added or removed, we should see this list updated.
The useMutation
hook does not automatically execute the mutation you pass it when the component renders. Instead, it returns a tuple with a mutate function in its first position (which we assign to addCoin
in the example above). You then call the mutate function at any time to instruct Apollo Client to execute the mutation. In the example above, we call addCoin
when a user presses the rounded button.
WELL DONE!
link Bonus Challenge
Favorites Tab
The app currently displays a single tab with a list of all bitcoins' prices.
Try adding a second tab called "Favorites" to display the favorite bitcoins.
Wrapping Up
Today we utilized Queries and Mutations to create a bitcoins tracker. Here's the full list of resources for this lecture: