Let's learn how to consume a GraphQL API!
We will be using Apollo with React for this workshop.
In this workshop you will touch on:
- (Setting up for development towards local backend)
- Loading data
- Mutating data
- Updating the apollo-cache manually
- Using optimistic responses for better user experience
- Configuring the apollo-client with middleware
If you are stuck at any point, feel free to peek into the full react example.
In the 1_starter/react
folder, run yarn
to install dependencies and run yarn start
to start the create-react-app development build.
Verify that everything looks fine, and that the example query loads the data as expected.
Developing against the deployed example won't be the best experience as other people might add and remove data at any time. We would like to use a local node example as a development backend instead.
The 1_starter/react/src/apollo/client.js
file is where the URL the Apollo client uses is configured.
Change the uri
property to /
.
const client = new ApolloClient({
- uri: "https://node.gql.systek.dev/"
+ uri: "/"
});
Now run yarn
and yarn start
in the 3_examples/node
folder and you should have a backend running.
Together with the proxy
configuration already present in 1_starter/react/package.json
this will proxy any request from the React application to the backend.
Before continuing you might want to familiarise your self with the folder structure and example components.
In GraphQL, fetching data (without changing it) is called queries.
With Apollo, fetching data is done with the useQuery
hook. The hook takes two arguments: A GraphQL query, and some optional options
Optional: Familiarize your self with GraphQL queries!
Visit the node backends GraphQL Playground, (http://localhost:4000/) it is a small built-in web app to explore a GraphQL schema. It has auto completion and a query formatter.
Try pasting this query and hitting play:
{
ingredients {
id
name
}
}
Now head over to the 1_starter/react/src/example/Example.js
component and look at how a query is used together with the useQuery
hook.
The query parsed by gql
can be passed directly as an argument to the hook.
The value returned form the useQuery
hook contains everything we need, from loading state to errors, even the data from the server:
{
client: Object;
loading: false;
data: {
ingredients: Array(6);
}
error: undefined;
fetchMore: ƒ();
networkStatus: 7;
refetch: ƒ();
variables: {
}
}
🚧 Task: Play around with the exampleQuery
in /1_starter/react/src/components/example/Example.js
to fetch some more data. Expand the component to display these values.
Now create a completely new component that lists out all Orders
in the backend. Important for later!
(The query will currently only return an empty array, but will return our data once we start to populate our database)
Here is a complete query you can use:
query AllOrders {
orders {
orderId
delivery
delivered
items {
id
name
price
}
}
}
Remember to use it with the { gql }
function from apollo-boost
.
Queries are only for getting data (think HTTP GET request), to change any data we will use use a Mutation
.
Mutations work very similar to queries, are structured the same way and can ever return objects the same as queries.
In fact, from a syntax perspective the only difference between a query and a mutation is the word mutation before the query:
mutation {
order(dishes: { dishId: 100, quantity: 3 }) {
id
delivery
}
}
Mutations often accepts parameters, the example above (that you can paste straight into GraphQL Playground (http://localhost:4000/)) have some complex parameters. In fact, it takes in a list of objects, which means you can send in just one, like above, or several.
mutation {
order(dishes: [{ dishId: 100, quantity: 3 }, { dishId: 101, quantity: 2 }]) {
id
delivery
}
}
Parameters can be a lot simpler, they can be normal string, ints, floats or booleans as well.
mutation {
markDelivered(orderId: "0.7592086608808599")
}
Here the return value is a boolean, so the return body ({}
) can be omitted.
🚧 Task: Create a new component, and add a useMutation
hook at the top of the component.
useMutation
expects a graphql query as an argument.
Example:
import React from "react";
import { useMutation } from "@apollo/react-hooks";
import { gql } from "apollo-boost";
const orderDishMutation = gql`
mutation {
order(dishes: { dishId: 100, quantity: 3 }) {
id
delivery
}
}
`;
const OrderDishButton = () => {
const [submit] = useMutation(orderDishMutation);
return <button onClick={submit}>Store order!</button>;
};
The example is pretty plain, but it becomes interesting as soon as we start passing variables from JS to GraphQL. This is not done through manual string-concatenation but through GraphQLs own variables which tie in nicely with Apollo.
import React from "react";
import { useMutation } from "@apollo/react-hooks";
import { gql } from "apollo-boost";
const orderDishMutationWithVariables = gql`
mutation OrderDish($dishId: Int!, $count: Int!) {
order(dishes: { dishId: $dishId, quantity: $count }) {
id
delivery
}
}
`;
const OrderDishButton = () => {
const [submit] = useMutation(orderDishMutationWithVariables, {
variables: {
dishId: 100,
count: 10
}
});
return <button onClick={submit}>Store order!</button>;
};
You can even use complex objects if the mutation asks for it:
import React from "react";
import { useMutation } from "@apollo/react-hooks";
import { gql } from "apollo-boost";
const orderDishMutationWithVariables = gql`
mutation OrderDish($dishList: [Order!]!) {
order(dishes: $dishList) {
id
delivery
}
}
`;
const OrderDishButton = () => {
const [submit] = useMutation(orderDishMutationWithVariables, {
variables: {
dishList: [
{ dishId: 100, quantity: 8 },
{ dishId: 101, quantity: 2 }
]
}
});
return <button onClick={submit}>Store order!</button>;
};
🚧 Task: Expand useMutation
to take some input from the user (input, buttons, whatever you see fit).
The syntax for parameters in mutations can also be used in queries. Often used for filtering and sorting.
Everything fetched and updated through the apollo client is stored in an apollo cache.
So far we have created a component that lists out all orders in the system, as well as a button that can add new orders. If you add a few orders, you can see that they show up in your list component if you refresh the page.
To make them show up on the list automatically, there are a couple of things we can do. First, the easiest one is to simply tell apollo to fetch the Orders
query once more after the mutation has completed, this can very easily be achieved with refetchQueries
.
import React from "react";
import { useMutation } from "@apollo/react-hooks";
import { gql } from "apollo-boost";
import { ORDERS } from "./queries";
const orderDishMutationWithVariables = gql`
mutation OrderDish($dishList: [Order!]!) {
order(dishes: $dishList) {
id
delivery
}
}
`;
const OrderDishButton = () => {
const [submit] = useMutation(orderDishMutationWithVariables, {
variables: { ... },
refetchQueries: [{ query: ORDERS }]
});
return <button onClick={submit}>Store order!</button>;
};
Several queries can be refetched. It's important to note that the combination of a query and it's variables is used as the key in the cache. Meaning that a refetch query with the wrong variables might not do what you think.
🚧 Task: Add a refetch query to your mutation, observe that the list is automatically (maybe a bit slowly?) updated with your new order each time.
In our case our mutation returns a complete object, the same object that is returned from the Orders
query. In this scenario it makes sense to simply update the state manually.
import React from "react";
import { useMutation } from "@apollo/react-hooks";
import { gql } from "apollo-boost";
import { ORDERS } from "./queries";
const orderDishMutationWithVariables = gql`
mutation OrderDish($dishList: [Order!]!) {
order(dishes: $dishList) {
delivery
delivered
items {
id
name
price
}
}
}
`;
const OrderDishButton = () => {
const [submit] = useMutation(orderDishMutationWithVariables, {
variables: { ... },
update: (proxy, { data }) => {
const ordersCache = proxy.readQuery({ query: ORDERS });
proxy.writeQuery({
query: ORDERS,
data: { orders: [...ordersCache.orders, data.order] }
});
}
});
return <button onClick={submit}>Store order!</button>;
};
It's important that the object is 100% in accordance with what the query that we are updating is expecting.
🚧 Task: Implement the update function for your mutation, to make sure it works disable refetchQueries
, it will overwrite your manual cache updating.
update
becomes really powerful when you combine it with optimisticResponse
. Optimistic response is an object that emulates what you believe the server will respond with, if you can guess this correctly (in the happy path), you can make it seems like the mutation is instant for the user.
When you have a optimisticResponse
and a update
, the update function will be called twice. First with the shape of your optimisticResponse
as the { data }
object. This cache update will then be reverted, and invoked again with the actual response of the server.
import React from "react";
import { useMutation } from "@apollo/react-hooks";
import { gql } from "apollo-boost";
import { ORDERS } from "./queries";
const orderDishMutationWithVariables = gql`
mutation OrderDish($dishList: [Order!]!) {
order(dishes: $dishList) {
delivery
delivered
items {
id
name
price
}
}
}
`;
const OrderDishButton = () => {
const [submit] = useMutation(orderDishMutationWithVariables, {
variables: { ... },
optimisticResponse: {
order: {
__typename: "Receipt",
id: `temporary-id-${Object.keys(items).join("-")}`,
delivery: new Date().toISOString(),
delivered: null,
items: items.map(i => ({
id: i.dishId,
name: "DefaultName",
price: -1,
__typename: "Dish"
}))
}
},
// !!!! Invoked twice, first with the data defined above
// !!!! That first update is then reverted, then invoked
// !!!! with the data from the response
update: (proxy, { data }) => {
const ordersCache = proxy.readQuery({ query: ORDERS });
proxy.writeQuery({
query: ORDERS,
data: { orders: [...ordersCache.orders, data.order] }
});
}
});
return <button onClick={submit}>Store order!</button>;
};
(Here we have put in dummy values for name
and price
in the optimistic response. Ideally you would
know these values in the ui of a real application)
If both these updates look identical to the user, the user would be none the wiser that the request actually took a long time.
Should the request fail, the cache update will be reverted and your cache will look like the mutation never was ran. This is a good point to show your user an error as to why the UI just jumped around a bit strangely.
Discussion Is building the UI around the happy path a good UX? When is it good to use optimistic responses and manual cache updates?
🚧 Task: Implement optimistic response and cache update in your new order mutation. When done, your new orders should feel instant, even if your set your chrome network throttling to "Slow 3G".
Apollo links are a very powerful middleware concepts to change how a request is handled, common use cases are
- adding headers on outbound requests
- globally (as opposed to each component) handling errors on responses
- retrying requests
- batching queries
🚧 Task: To be able to use other links, you need to use a complete setup of your apollo client, instead of the preconfigured one from apollo-boost
. Set up a complete apollo client using this guide.
If you managed to get this far, good job!
Here are some useful concepts that have not been covered in this workshop: