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.

Source

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:

format_list_bulleted
help_outline