With your GraphQL server built, it’s now time to set up GraphQL on the client side. Very broadly, a client is just an application that communicates with a server. Because of the flexibility of GraphQL, there’s no prescription for how to build a client. You might be building apps for web browsers. You might be creating native applications for phones. You might be building a GraphQL service for a screen on your refrigerator. It also does not matter to the client in which language the service is written.
All you really need to send queries and mutations is the ability to send an HTTP request. When the service responds with some data, you can use it in your client no matter what that client is.
The easiest way to begin is just to make an HTTP request to your GraphQL endpoint. To test the server that we built in Chapter 5, make sure your service is running locally at http://localhost:4000/graphql. You can also find all of these samples running on CodeSandbox at the links found in the Chapter 6 repository.
As you saw in Chapter 3, you can send requests to a GraphQL service by using cURL. All you need is a few different values:
A query: {totalPhotos, totalUsers}
A GraphQL endpoint: http://localhost:4000/graphql
A content type: Content-Type: application/json
From there, you send the cURL request directly from the terminal/command prompt using the POST method:
curl -X POST -H "Content-Type: application/json" --data '{ "query": "{totalUsers, totalPhotos}" }' http://localhost:4000/graphql
If we send this request, we should see the correct results,
{"data":{"totalUsers":7,"totalPhotos":4}}
, as JSON data returned in
the terminal. Your numbers for totalUsers
and totalPhotos
will reflect your current data. If your client is a shell script, you can start
building that script with cURL.
Because we’re using cURL, we can use anything that sends an HTTP request.
We could build a tiny client by using fetch
, which will work in the
browser:
var
query
=
`{totalPhotos, totalUsers}`
var
url
=
'http://localhost:4000/graphql'
var
opts
=
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
},
body
:
JSON
.
stringify
({
query
})
}
fetch
(
url
,
opts
)
.
then
(
res
=>
res
.
json
())
.
then
(
console
.
log
)
.
catch
(
console
.
error
)
After we fetch the data, we’ll see the expected result logged in the console:
{
"data"
:
{
"totalPhotos"
:
4
,
"totalUsers"
:
7
}
}
We can use the resulting data on the client to build applications.
Let’s consider a basic example to see how we might list totalUsers
and
totalPhotos
directly in the DOM:
fetch
(
url
,
opts
)
.
then
(
res
=>
res
.
json
())
.
then
(({
data
})
=>
`
<p>photos:
${
data
.
totalPhotos
}
</p>
<p>users:
${
data
.
totalUsers
}
</p>
`
)
.
then
(
text
=>
document
.
body
.
innerHTML
=
text
)
.
catch
(
console
.
error
)
Instead of logging the results to the console, we use the data to build some HTML text. We can then take that text and write it directly to the document’s body. Be careful: it’s possible to overwrite anything that was in the body after the request is complete.
If you already know how to send HTTP requests using your favorite client, you already have the tools necessary to build a client application that communicates with any GraphQL API.
Though cURL and fetch work well, there are other frameworks that you can use to send GraphQL operations to an API. One of the most notable examples of this is graphql-request
. graphql-request
wraps fetch requests in a promise that can be used to make requests to the GraphQL server. It also handles the details of making the request and parsing the data for you.
To get started with graphql-request
, you first need to install it:
npm install graphql-request
From there, you import and use the module as request
. Be sure to
keep the photo service running on port 4000:
import
{
request
}
from
'graphql-request'
var
query
=
`
query listUsers {
allUsers {
name
avatar
}
}
`
request
(
'http://localhost:4000/graphql'
,
query
)
.
then
(
console
.
log
)
.
catch
(
console
.
error
)
The request function takes in url
and query
, makes the request to
the server, and returns the data in one line of code. The data returned
is, as expected, a JSON response of all of the users:
{
"allUsers"
:
[
{
"name"
:
"sharon adams"
,
"avatar"
:
"http://..."
},
{
"name"
:
"sarah ronau"
,
"avatar"
:
"http://..."
},
{
"name"
:
"paul young"
,
"avatar"
:
"http://..."
},
]
}
We can begin using this data in our client straight away.
You can also send mutations with graphql-request
:
import
{
request
}
from
'graphql-request'
var
url
=
'http://localhost:4000/graphql'
var
mutation
=
`
mutation populate($count: Int!) {
addFakeUsers(count:$count) {
id
name
}
}
`
var
variables
=
{
count
:
3
}
request
(
url
,
mutation
,
variables
)
.
then
(
console
.
log
)
.
catch
(
console
.
error
)
The request
function takes in the API URL, the mutation, and a third
argument for variables. This is just a JavaScript object that passes in
a field and value for the query variables. After we invoke request
, we
issue the addFakeUsers
mutation.
Though graphql-request
doesn’t offer any formal integration with UI
libraries and frameworks, we can incorporate a library fairly simply.
Let’s load some data into a React component using graphql-request
, as demonstrated in Example 6-1.
import
React
from
'react'
import
ReactDOM
from
'react-dom'
import
{
request
}
from
'graphql-request'
var
url
=
'http://localhost:4000/graphql'
var
query
=
`
query listUsers {
allUsers {
avatar
name
}
}
`
var
mutation
=
`
mutation populate($count: Int!) {
addFakeUsers(count:$count) {
githubLogin
}
}
`
const
App
=
({
users
=
[]
})
=>
<
div
>
{
users
.
map
(
user
=>
<
div
key
=
{
user
.
githubLogin
}
>
<
img
src
=
{
user
.
avatar
}
alt
=
""
/>
{
user
.
name
}
<
/div>
)}
<
button
onClick
=
{
addUser
}
>
Add
User
<
/button>
<
/div>
const
render
=
({
allUsers
=
[]
})
=>
ReactDOM
.
render
(
<
App
users
=
{
allUsers
}
/>
,
document
.
getElementById
(
'root'
)
)
const
addUser
=
()
=>
request
(
url
,
mutation
,
{
count
:
1
})
.
then
(
requestAndRender
)
.
catch
(
console
.
error
)
const
requestAndRender
=
()
=>
request
(
url
,
query
)
.
then
(
render
)
.
catch
(
console
.
error
)
requestAndRender
()
Our file starts with an import of both React and ReactDOM. We then
create an App
component. App
maps over the users
that are passed
as props and creates div
elements containing their avatar
and
username
. The render
function renders the App
to the #root
element
and passes in allUsers
as a property.
From there, requestAndRender
calls request
from graphql-request
. This issues the query, receives the data, and then calls render
, which provides the data to the App
component.
This little app also handles mutations. In the App
component, the
button has an onClick
event that calls the addUser
function. When
invoked, this sends the mutation and then calls requestAndRender
to
issue a new request for the services users and rerenders the <App />
with the new list of users.
So far, we’ve looked at a few different ways to begin building
client apps using GraphQL. You can write shell scripts with cURL. You
can build web pages with fetch. You can build apps a little faster with
graphql-request
. You could stop right there if you wanted to, but there
are even more powerful GraphQL clients available. Let’s go for it.
A huge benefit of using Representational State Transfer (REST) is the ease with which you can handle caching. With REST, you can save the response data from a request in a cache under the URL that was used to access that request. Caching done, no problem. Caching GraphQL is a little trickier. We don’t have a multitude of routes with a GraphQL API—everything is sent and received over a single endpoint, so we cannot simply save the data returned from a route under the URL that was used to request it.
To build a robust, performant application, we need a way to cache queries and their resulting objects. Having a localized caching solution is essential as we constantly strive to create fast, efficient apps. We could create something like this ourselves, or we could lean on one of the vetted clients that already exist.
The most prominent GraphQL client solutions available today are Relay and Apollo Client. Relay was open sourced by Facebook in 2015 at the same time as GraphQL. It brings together everything that Facebook learned about using GraphQL in production. Relay is compatible with React and React Native only, which means that there was an opportunity to create a GraphQL client to support developers who might not use React.
Enter Apollo Client. Brought to you by Meteor Development Group, Apollo Client is a community-driven project to build a flexible GraphQL client solution to handle tasks like caching, optimistic UI updates, and more. The team has created packages that supply bindings for React, Angular, Ember, Vue, iOS, and Android.
We’ve already been using several tools from the Apollo team on the server, but Apollo Client focuses specifically on sending and receiving requests from the client to the server. It handles the network requests with Apollo Link and handles all caching with Apollo Cache. Apollo Client then wraps the link and the cache and manages all interactions with the GraphQL service efficiently.
For the rest of the chapter, we take a closer look at Apollo Client. We’re going to be using React to build out our UI components, but we can apply many of the techniques described here to projects that use different libraries and frameworks.
Since working with React is what led us to GraphQL in the first place, we have chosen React as the user interface library. We haven’t offered much explanation about React itself. It is a library that was created at Facebook that uses a component-based architecture to compose UIs. If you are a user of a different library and you never want to look at React again after this, that’s ok. The ideas presented in this next section are applicable to other UI frameworks.
In this chapter, we show you how to build a React app that
interacts with a GraphQL service using Apollo Client. To begin, we need to scaffold the frontend of this project using
create-react-app
. create-react-app
allows you to generate an entire
React project without setting up any build configuration. If you haven’t
used create-react-app
before, you might need to install it:
npm install -g create-react-app
Once installed, you can create a React project anywhere on your computer with:
create-react-app photo-share-client
This command installs a new base React application in a folder named photo-share-client. It automatically adds and installs everything that you will need to get started building a React app. To start the application, navigate to the photo-share-client folder and run npm start
. You’ll see your browser open to http://localhost:3000
where your React client application is running. Remember, you can find all of the files for this chapter in the repository at http://github.com/moonhighway/learning-graphql.
You’ll need to install a few packages to build a GraphQL client with Apollo tools. First, you’ll need graphql
which includes the GraphQL language parser. Then you’ll need a package called apollo-boost
. Apollo Boost includes the Apollo packages necessary for creating an Apollo Client and sending operations to that client. Finally, we’ll need react-apollo
. React Apollo is an npm library that contains React components that we will use to construct a user interface with Apollo.
Let’s install these three packages at the same time:
npm install graphql apollo-boost react-apollo
Now we are ready to create our client. The ApolloClient
constructor found in apollo-boost
can be used to create our first client. Open the src/index.js file and replace the code in that file with the following:
import
ApolloClient
from
'apollo-boost'
const
client
=
new
ApolloClient
({
uri
:
'http://localhost:4000/graphql'
})
Using the ApolloClient
constructor, we’ve created a new client
instance. The client
is ready to handle all network communication with the GraphQL service hosted at http://localhost:4000/graphql
. For example, we can use the client to send a query to the PhotoShare Service:
import
ApolloClient
,
{
gql
}
from
'apollo-boost'
const
client
=
new
ApolloClient
({
uri
:
'http://localhost:4000/graphql'
})
const
query
=
gql
`
{
totalUsers
totalPhotos
}
`
client
.
query
({
query
})
.
then
(({
data
})
=>
console
.
log
(
'data'
,
data
))
.
catch
(
console
.
error
)
This code uses the client
to send a query for the total photo count and the total user count. To make this happen, we imported the gql
function from apollo-boost
. This function is a part of the graphql-tag
package that was automatically included with apollo-boost
. The gql
function is used to parse a query into an AST or abstract syntax tree.
We can send the AST to the client by invoking client.query({query})
. This method returns a promise. It sends the query as an HTTP request to our GraphQL service and resolves the data returned from that service. In the above example, we are logging the response to the console:
{ totalUsers: 4, totalPhotos: 7, Symbol(id): "ROOT_QUERY" }
Make sure that the GraphQL service is still running on http://localhost:4000
so that you can test the client connection to the server.
In addition to handling all network requests to our GraphQL service, the client also caches the responses locally in memory. At any point, we can take a look at the cache by invoking client.extract()
:
console
.
log
(
'cache'
,
client
.
extract
())
client
.
query
({
query
})
.
then
(()
=>
console
.
log
(
'cache'
,
client
.
extract
()))
.
catch
(
console
.
error
)
Here we have a look at the cache before the query is sent, and another look at it after the query has been resolved. We can see that we now have the results saved in a local object which is managed by the client:
{ ROOT_QUERY: { totalPhotos: 4, totalUsers: 7 } }
The next time we send the client a query for this data, it will read it from the cache as opposed to sending another network request to our service. Apollo Client provides us with options to specify when, and how often, we should send HTTP requests over the network. We’ll cover those options later on in this chapter. For now, it is important to understand that Apollo Client is used to handle all network requests to our GraphQL service. Additionally, by default, it automatically caches the results locally and defers to the local cache to improve our applications performance.
To get started with react-apollo
, all we need to do is create a client and add it to our user interface with a component called ApolloProvider
. Replace the code found in the index.js
file with the following:
import
React
from
'react'
import
{
render
}
from
'react-dom'
import
App
from
'./App'
import
{
ApolloProvider
}
from
'react-apollo'
import
ApolloClient
from
'apollo-boost'
const
client
=
new
ApolloClient
({
uri
:
'http://localhost:4000/graphql'
})
render
(
<
ApolloProvider
client
=
{
client
}
>
<
App
/>
<
/ApolloProvider>,
document
.
getElementById
(
'root'
)
)
This is all the code you will need to get started using Apollo with React. Here, we’ve created a client and then placed that client in React’s global scope with the help of a component called the ApolloProvider
. Any child component wrapped by the ApolloProvider
will have access to the client. That means that the <App />
component and any of its children are ready to receive data from our GraphQL service via Apollo Client.
Using Apollo Client, we need a way to handle queries to fetch data to
load into our React UI. The Query
component will take care of fetching data, handling loading state, and updating our UI. We can use the Query
component anywhere within the ApolloProvider
. The Query
component sends a query
using the client. Once resolved, the client will return the results that we’ll use to construct the user interface.
Open the src/App.js
file and replace the code that is currently inside of this file with the following:
import
React
from
'react'
import
Users
from
'./Users'
import
{
gql
}
from
'apollo-boost'
export
const
ROOT_QUERY
=
gql
`
query allUsers {
totalUsers
allUsers {
githubLogin
name
avatar
}
}
`
const
App
=
()
=>
<
Users
/>
export
default
App
In the App
component, we’ve created a query called ROOT_QUERY
. Remember, one of the benefits of using GraphQL is to request everything you’ll need to construct your UI and receive all of that data in a single response. That means we are going to request both the totalUsers
count and the allUsers
array in a query that we’ve created in the root of our application. Using the gql
function, we’ve converted our string query an AST object named ROOT_QUERY
, and we’ve exported this object so that other components can use it.
At this point, you should see an error. This is because we’ve told the App
to render a component that we have not created. Create a new file called src/Users.js
and place this code inside of that file:
import
React
from
'react'
import
{
Query
}
from
'react-apollo'
import
{
ROOT_QUERY
}
from
'./App'
const
Users
=
()
=>
<
Query
query
=
{
ROOT_QUERY
}
>
{
result
=>
<
p
>
Users
are
loading
:
{
result
.
loading
?
"yes"
:
"no"
}
<
/p>
}
<
/Query>
export
default
Users
Now you should see the error clear, and the message “Users are loading: no” should be displayed in the browser window. Under the hood, the Query
component is sending the ROOT_QUERY
to our GraphQL service and caching the result locally. We obtain the result using a React technique called render props. Render props allow us to pass properties as function arguments to child components. Notice that we are obtaining the result
from a function and returning a paragraph element.
The result contains more information than just the response data. It will tell us whether or not an operation is loading via the result.loading
property. In the preceding example, we can tell the user whether or not the current query is loading.
Your network might be too fast to see more than a quick flicker of the loading property in the browser. You can use the Network tab in Chrome’s developer tools to throttle the HTTP request. In the developer tools, you’ll find a dropdown that has the “Online” option selected. Selecting “Slow 3G” from the dropdown will simulate a slower response. This will allow you to see the loading happen in the browser.
Once the data has loaded, it will be passed along with the result.
Instead of displaying “yes” or “no” when the client is loading data, we can display UI components instead. Let’s adjust the Users.js
file:
const
Users
=
()
=>
<
Query
query
=
{
ROOT_QUERY
}
>
{({
data
,
loading
})
=>
loading
?
<
p
>
loading
users
...
<
/p> :
<
UserList
count
=
{
data
.
totalUsers
}
users
=
{
data
.
allUsers
}
/>
}
<
/Query>
const
UserList
=
({
count
,
users
})
=>
<
div
>
<
p
>
{
count
}
Users
<
/p>
<
ul
>
{
users
.
map
(
user
=>
<
UserListItem
key
=
{
user
.
githubLogin
}
name
=
{
user
.
name
}
avatar
=
{
user
.
avatar
}
/>
)}
<
/ul>
<
/div>
const
UserListItem
=
({
name
,
avatar
})
=>
<
li
>
<
img
src
=
{
avatar
}
width
=
{
48
}
height
=
{
48
}
alt
=
""
/>
{
name
}
<
/li>
If the client is loading
the current query, we will display a “loading users…” message. If the data has been loaded, we will pass the total user count along with an array containing the name
, githubLogin
, and avatar
of every user to the UserList
component: exactly the data we asked for in our query. The UserList
uses the result data to build the UI. It displays the count along with a list that displays the user’s avatar image alongside of their name.
The results
object also has several utility functions for pagination, refetching, and polling. Let’s use the refetch
function to refetch
the list of users when we click a button:
const
Users
=
()
=>
<
Query
query
=
{
ROOT_QUERY
}
>
{({
data
,
loading
,
refetch
})
=>
loading
?
<
p
>
loading
users
...
<
/p> :
<
UserList
count
=
{
data
.
totalUsers
}
users
=
{
data
.
allUsers
}
refetchUsers
=
{
refetch
}
/>
}
<
/Query>
Here we’ve obtained a function that can be used to refetch
the ROOT_QUERY
or request the data from the server again. The refetch
property is simply a function. We can pass it to the UserList
where it can be added to a button click:
const
UserList
=
({
count
,
users
,
refetch
})
=>
<
div
>
<
p
>
{
count
}
Users
<
/p>
<
button
onClick
=
{()
=>
refetch
()}
>
Refetch
<
/button>
<
ul
>
{
users
.
map
(
user
=>
<
UserListItem
key
=
{
user
.
githubLogin
}
name
=
{
user
.
name
}
avatar
=
{
user
.
avatar
}
/>
)}
<
/ul>
<
/div>
In the UserList
, we are using the refetch
function to request the same root data from our GraphQL service. Whenever you click the “Refetch Users” button, another query will be sent to the GraphQL endpoint to refetch any data changes. This is one way to keep your user interface in sync with the data on the server.
To test this, we can change the user data after the initial fetch. You can delete the users collection, delete user documents directly from MongoDB, or add fake users by sending a query with the server’s GraphQL Playground. When you change the data in the database, the “Refetch Users” button will need to be clicked in order to re-render the most up to date data in the browser.
Polling is another option that is available with the Query
component. When we add the pollInterval
prop to the Query
component, data is automatically fetched over and over again based on a given interval:
<
Query
query
=
{
ROOT_QUERY
}
pollInterval
=
{
1000
}
>
Setting a pollInterval
automatically refetches the data at a specified time. In this case, we will refetch the data from the server every second. Be careful when using polling as this code actually sends a new network request every second.
In addition to loading
, data
, and refetch
, the response object has some additional options:
stopPolling
A function that stops polling
startPolling
A function that will start polling
fetchMore
A function that can be used to fetch the next page of data
Before we continue, remove any pollInterval
properties from the Query
component. We do not want polling to take place as we continue to iterate on this example.
When we want to send mutations to the GraphQL service, we can use
the Mutation
component. In the next example, we use this component
to handle the addFakeUsers
mutation. When we send this mutation, we
write the new list of users directly to the cache.
To begin, let’s import the Mutation
component and add a mutation to the Users.js
file:
import
{
Query
,
Mutation
}
from
'react-apollo'
import
{
gql
}
from
'apollo-boost'
...
const
ADD_FAKE_USERS_MUTATION
=
gql
`
mutation addFakeUsers($count:Int!) {
addFakeUsers(count:$count) {
githubLogin
name
avatar
}
}
`
Once we have the mutation, we can use it in combination with the Mutation
component. This component will pass a function to its children via render props. This function can be used to send the mutation when we are ready:
const
UserList
=
({
count
,
users
,
refetchUsers
})
=>
<
div
>
<
p
>
{
count
}
Users
<
/p>
<
button
onClick
=
{()
=>
refetchUsers
()}
>
Refetch
Users
<
/button>
<
Mutation
mutation
=
{
ADD_FAKE_USERS_MUTATION
}
variables
=
{{
count
:
1
}}
>
{
addFakeUsers
=>
<
button
onClick
=
{
addFakeUsers
}
>
Add
Fake
Users
<
/button>
}
<
/Mutation>
<
ul
>
{
users
.
map
(
user
=>
<
UserListItem
key
=
{
user
.
githubLogin
}
name
=
{
user
.
name
}
avatar
=
{
user
.
avatar
}
/>
)}
<
/ul>
<
/div>
Just as we sent query
as a prop to the Query
component, we will send a mutation
prop to the Mutation
component. Notice also that we’re using the variables
property. This will send the necessary query variables with the mutation. In this case, it sets the count to 1
, which will cause the mutation to add one fake user at a time. The Mutation
component uses a function, addFakeUsers
, that will send the mutation once it has been invoked. When the user clicks the “Add Fake Users” button, the mutation will be sent to our API.
Currently, these users are being added to the database, but the only way to see the changes is to click the “Refetch Users” button. We can tell the Mutation
component to refetch specific queries once the mutation has completed instead of waiting for our users to click a button:
<
Mutation
mutation
=
{
ADD_FAKE_USERS_MUTATION
}
variables
=
{{
count
:
1
}}
refetchQueries
=
{[{
query
:
ROOT_QUERY
}]}
>
{
addFakeUsers
=>
<
button
onClick
=
{
addFakeUsers
}
>
Add
Fake
Users
<
/button>
}
<
/Mutation>
refetchQueries
is a property that lets you specify which queries to refetch after sending a mutation. Simply place a list of objects that contain queries. Each of the query operations found in this list will refetch data after the mutation has completed.
In Chapter 5, we built a mutation to authorize a user with GitHub. In the following section, we show you how to set up user authorization on the client side.
The process of authorizing a user involves several steps. The bold steps indicate the functionality we’ll add to the client:
Redirects the user to GitHub with the client_id
Allows access to account information on GitHub for the client application
Redirects back to the website with code: http://localhost:3000?code=XYZ
Sends GraphQL Mutation authUser(code)
with code
Requests a GitHub access_token
with client_id
, client_secret
, and client_code
Responds with access_token
that can be used with future info requests
Request user info with access_token
Responds with user info: name, github_login, avatar_url
Resolves authUser(code)
mutation with AuthPayload
, which contains a token and the user
Saves token to send with future GraphQL requests
It is now time to authorize the user. To facilitate this
example, we use React Router, which we install via npm: npm install react-router-dom
.
Let’s modify our main <App />
component. We’ll incorporate the BrowserRouter
, and we’ll add a new component, AuthorizedUser
, that we can use to authorize users with GitHub:
import
React
from
'react'
import
Users
from
'./Users'
import
{
BrowserRouter
}
from
'react-router-dom'
import
{
gql
}
from
'apollo-boost'
import
AuthorizedUser
from
'./AuthorizedUser'
export
const
ROOT_QUERY
=
gql
`
query allUsers {
totalUsers
allUsers { ...userInfo }
me { ...userInfo }
}
fragment userInfo on User {
githubLogin
name
avatar
}
`
const
App
=
()
=>
<
BrowserRouter
>
<
div
>
<
AuthorizedUser
/>
<
Users
/>
<
/div>
<
/BrowserRouter>
export
default
App
BrowserRouter
wraps all of the other components that we want to render. We also will add a new AuthorizedUser
component, which we will build in a new file. We should see an error until we add that component.
We’ve also modified the ROOT_QUERY
to get it ready for authorization. We are now additionally asking for the me
field, which returns information about the current user when someone is logged in. When a user is not logged in, this field will simply return null
. Notice that we’ve added a fragment called userInfo
to the query document. This allows us obtain the same information about a User
in two places: the me
field and the allUsers
field.
The AuthorizedUser
component should redirect the user to GitHub to request a code. That code should be passed back from GitHub to our app at http://localhost:3000
.
In a new file called AuthorizedUser.js
, let’s implement this process:
import
React
,
{
Component
}
from
'react'
import
{
withRouter
}
from
'react-router-dom'
class
AuthorizedUser
extends
Component
{
state
=
{
signingIn
:
false
}
componentDidMount
()
{
if
(
window
.
location
.
search
.
match
(
/code=/
))
{
this
.
setState
({
signingIn
:
true
})
const
code
=
window
.
location
.
search
.
replace
(
"?code="
,
""
)
alert
(
code
)
this
.
props
.
history
.
replace
(
'/'
)
}
}
requestCode
()
{
var
clientID
=
<
YOUR_GITHUB_CLIENT_ID
>
window
.
location
=
`https://github.com/login/oauth/authorize?client_id=
${
clientID
}
&scope=user`
}
render
()
{
return
(
<
button
onClick
=
{
this
.
requestCode
}
disabled
=
{
this
.
state
.
signingIn
}
>
Sign
In
with
GitHub
<
/button>
)
}
}
export
default
withRouter
(
AuthorizedUser
)
The AuthorizedUser
component renders a “Sign In with GitHub” button. Once clicked, this button will redirect the user to GitHub’s OAuth process. Once authorized, GitHub will pass a code back to the browser: http://localhost:3000?code=XYZGNARLYSENDABC
. If the code is found in the query string, the component parses it from the window’s location and displays it in an alert box to the user before removing it with the history
property that was sent to this component with React Router.
Instead of sending the user an alert with the GitHub code, we need to send it to the githubAuth
mutation:
import
{
Mutation
}
from
'react-apollo'
import
{
gql
}
from
'apollo-boost'
import
{
ROOT_QUERY
}
from
'./App'
const
GITHUB_AUTH_MUTATION
=
gql
`
mutation githubAuth($code:String!) {
githubAuth(code:$code) { token }
}
`
The above mutation will be used to authorize the current user. All we need is the code. Let’s add this mutation to the render
method of this component:
render
()
{
return
(
<
Mutation
mutation
=
{
GITHUB_AUTH_MUTATION
}
update
=
{
this
.
authorizationComplete
}
refetchQueries
=
{[{
query
:
ROOT_QUERY
}]}
>
{
mutation
=>
{
this
.
githubAuthMutation
=
mutation
return
(
<
button
onClick
=
{
this
.
requestCode
}
disabled
=
{
this
.
state
.
signingIn
}
>
Sign
In
with
GitHub
<
/button>
)
}}
<
/Mutation>
)
}
The Mutation
component is tied to the GITHUB_AUTH_MUTATION
. Once completed, it will invoke the component’s authorizationComplete
method and refetch the ROOT_QUERY
. The mutation function has been added to the scope of the AuthorizedUser
component by setting: this.githubAuthMutation = mutation
. We can now invoke this this.githubAuthMutation()
function when we are ready (when we have a code).
Instead of alerting the code, we will send it along with the mutation to authorize the current user. Once authorized, we will save the resulting token to localStorage
and use the router’s history
property to remove the code from the window’s location:
class
AuthorizedUser
extends
Component
{
state
=
{
signingIn
:
false
}
authorizationComplete
=
(
cache
,
{
data
})
=>
{
localStorage
.
setItem
(
'token'
,
data
.
githubAuth
.
token
)
this
.
props
.
history
.
replace
(
'/'
)
this
.
setState
({
signingIn
:
false
})
}
componentDidMount
()
{
if
(
window
.
location
.
search
.
match
(
/code=/
))
{
this
.
setState
({
signingIn
:
true
})
const
code
=
window
.
location
.
search
.
replace
(
"?code="
,
""
)
this
.
githubAuthMutation
({
variables
:
{
code
}
})
}
}
...
}
To start the authorization process, invoke this.githubAuthMutation()
and add the code
to the operation’s variables
. Once complete, the authorizationComplete
method will be called. The data
passed to this method is the data that we selected in the mutation. It has a token
. We’ll save the token
locally and use React Router’s history
to remove the code query string from the window’s location bar.
At this point, we will have signed in the current user with GitHub. The next step will be to make sure that we send this token along with every request in the HTTP headers.
Our next task is to add a token to the authorization header for each request. Remember, the photo-share-api
service that we created in the last chapter will identify users who pass an authorization token in the header. All we have to do is make sure any token saved to localStorage
is sent along with every request sent to our GraphQL service.
Let’s modify the src/index.js
file. We need to find the line where we create the Apollo Client and replace it with this code:
const
client
=
new
ApolloClient
({
uri
:
'http://localhost:4000/graphql'
,
request
:
operation
=>
{
operation
.
setContext
(
context
=>
({
headers
:
{
...
context
.
headers
,
authorization
:
localStorage
.
getItem
(
'token'
)
}
}))
}
})
We’ve now added a request method to our Apollo Client configuration. This method pass the details about every operation
just before it is sent to the GraphQL service. Here we are setting the context of every operation
to include an authorization
header that contains the token saved to local storage. Don’t worry, if we don’t have a token saved the value of this header will simply be null and our service will assume that there a user has not been authorized.
Now that we’ve added the authorization token to every header, our me
field should return data about the current user. Let’s display that data in our UI. Find the render
method in the AuthorizedUser
component and replace it with this code:
render
()
{
return
(
<
Mutation
mutation
=
{
GITHUB_AUTH_MUTATION
}
update
=
{
this
.
authorizationComplete
}
refetchQueries
=
{[{
query
:
ROOT_QUERY
}]}
>
{
mutation
=>
{
this
.
githubAuthMutation
=
mutation
return
(
<
Me
signingIn
=
{
this
.
state
.
signingIn
}
requestCode
=
{
this
.
requestCode
}
logout
=
{()
=>
localStorage
.
removeItem
(
'token'
)}
/>
)
}}
<
/Mutation>
)
}
Instead of rendering a button, this Mutation
component now renders a component called Me
. The Me
component will either display information about the current user who is logged in or the authorize button. It will need to know whether or not the user is currently in the process of signing in. It also needs to access the requestCode
methods of the AuthorizedUser
component. Finally, we need to provide a function that can log the current user out. For now, we’ll just remove the token
from localStorage
when the user logs out. All of these values have been passed down to the Me
component as properties.
It’s now time to create the Me
component. Add the following code above the declaration of the AuthorizedUser
component:
const
Me
=
({
logout
,
requestCode
,
signingIn
})
=>
<
Query
query
=
{
ROOT_QUERY
}
>
{({
loading
,
data
})
=>
data
.
me
?
<
CurrentUser
{...
data
.
me
}
logout
=
{
logout
}
/>
:
loading
?
<
p
>
loading
...
<
/p> :
<
button
onClick
=
{
requestCode
}
disabled
=
{
signingIn
}
>
Sign
In
with
GitHub
<
/button>
}
<
/Query>
const
CurrentUser
=
({
name
,
avatar
,
logout
})
=>
<
div
>
<
img
src
=
{
avatar
}
width
=
{
48
}
height
=
{
48
}
alt
=
""
/>
<
h1
>
{
name
}
<
/h1>
<
button
onClick
=
{
logout
}
>
logout
<
/button>
<
/div>
The Me
component renders a Query
component to obtain the data about the current user from the ROOT_QUERY
. If there is a token, the me
field in the ROOT_QUERY
will not be null. Within the query component, we check to see if data.me
is null. If there is data under this field, we will display the CurrentUser
component and pass the data about the current user to this component as properties. The code {...data.me}
uses the spread operator to pass all of the fields to the CurrentUser
component as individual properties. Additionally, the logout
function is passed to the CurrentUser
component. When the user clicks the logout button, this function will be invoked and their token removed.
As developers, we’re in the network request minimization business. We don’t want our users to have to make extraneous requests. In order to minimize the number of network requests that our apps send, we can dig deeper into how to customize the Apollo Cache.
By default, Apollo Client stores data in a local JavaScript variable. Every time we create a client, a cache is created for us. Every time we send an operation, the response is cached locally. The fetchPolicy
tells Apollo Client where to look for data to resolve an operation: either the local cache or a network request. The default fetchPolicy
is cache-first
. This means that the client will look locally in the cache for data to resolve the operation. If the client can resolve the operation without sending a network request, it will do so. However, if data to resolve the query is not in the cache then the client will send a network request to the GraphQL service.
Another type of fetchPolicy
is cache-only
. This policy tells the client to only look in the cache and never send a network request. If the data to fulfill the query does not exist in the cache, then an error will be thrown.
Take a look at src/Users.js
, and find the Query
inside the Users
component. We can change the fetch policy of individual queries simply by adding the fetchPolicy
property:
<
Query
query
=
{{
query
:
ROOT_QUERY
}}
fetchPolicy
=
"cache-only"
>
At present, if we set the policy for this Query
to cache-only
and refresh the browser, we should see an error because Apollo Client is only looking in the cache for the data to resolve our query and that data is not present when the app starts. To clear the error, change the fetch policy to cache-and-network
:
<
Query
query
=
{{
query
:
ROOT_QUERY
}}
fetchPolicy
=
"cache-and-network"
>
The application works again. The cache-and-network
policy always resolves the query immediately from the cache and additionally sends a network request to get the latest data. If the local cache does not exist, as is the case when the app starts, this policy will simply retrieve the data from the network. Other policies include:
network-only
Always sends a network request to resolve a query
no-cache
Always sends a network request to resolve the data and it doesn’t cache the resulting response
It is possible to save the cache locally on the client. This unlocks the power of the cache-first
policy, because the cache will already exist when the user returns to the application. In this case, the cache-first
policy will immediately resolve the data from the existing local cache and not send a request to the network at all.
To save cache data locally, we’ll need to install an npm package:
npm install apollo-cache-persist
The apollo-cache-persist
package contains a function that enhance the cache by saving it to a local store whenever it changes. To implement cache persistance, we’ll need to create our own cache
object and add it to the client
when we configure our application.
Add the following code to the src/index.js
file:
import
ApolloClient
,
{
InMemoryCache
}
from
'apollo-boost'
import
{
persistCache
}
from
'apollo-cache-persist'
const
cache
=
new
InMemoryCache
()
persistCache
({
cache
,
storage
:
localStorage
})
const
client
=
new
ApolloClient
({
cache
,
...
})
First, we’ve created our own cache instance using the InMemoryCache
constructor provided with apollo-boost
. Next, we imported the persistCache
method from apollo-cache-persist
. Using InMemoryCache
, we create a new cache
instance and send it to the persistCache
method along with a storage
location. We’ve chosen to save the cache in the browser window’s localStorage
store. This means that once we start our application, we should see the value of our cache saved to our store. You can check for it by adding the following syntax:
console
.
log
(
localStorage
[
'apollo-cache-persist'
])
The next step is to check localStorage
on startup to see if we already have a cache saved. If we do, then we’ll want to initialize our local cache
with that data before creating the client:
const
cache
=
new
InMemoryCache
()
persistCache
({
cache
,
storage
:
localStorage
})
if
(
localStorage
[
'apollo-cache-persist'
])
{
let
cacheData
=
JSON
.
parse
(
localStorage
[
'apollo-cache-persist'
])
cache
.
restore
(
cacheData
)
}
Now our application will load any cached data before it starts. If we do have data saved under the key apollo-cache-persist
, then we’ll use the cache.restore(cacheData)
method to add it to the cache
instance.
We’ve successfully minimized the number of network requests to our service simply by using Apollo Client’s cache effectively. In the next section, we will learn about how we can write data directly to the local cache.
The Query
component is capable of reading directly from the cache. That’s what makes a fetch policy like cache-only
possible. We are also able to interact directly with the Apollo Cache. We can read current data from the cache or write data directly to the cache. Every time we change data stored in the cache, react-apollo
detects that change and re-renders all of the effected components. All we have to do is change the cache and the UI will automatically update to match the change.
Data is read from the Apollo Cache using GraphQL. You read queries. Data is written to the Apollo Cache using GraphQL, you write data to queries. Consider the ROOT_QUERY
that is located in src/App.js
:
export
const
ROOT_QUERY
=
gql
`
query allUsers {
totalUsers
allUsers { ...userInfo }
me { ...userInfo }
}
fragment userInfo on User {
githubLogin
name
avatar
}
`
This query has three fields in its selection set: totalUsers
, allUsers
, and me
. We can read any data that we currently have stored in our cache using the cache.readQuery
method:
let
{
totalUsers
,
allUsers
,
me
}
=
cache
.
readQuery
({
query
:
ROOT_QUERY
})
In this line of code, we’ve obtained the values for totalUsers
, allUsers
, and me
that were stored in the cache.
We can also write data directly to the totalUsers
, allUsers
, and me
fields of the ROOT_QUERY
using the cache.writeQuery
method:
cache
.
writeQuery
({
query
:
ROOT_QUERY
,
data
:
{
me
:
null
,
allUsers
:
[],
totalUsers
:
0
}
})
In this example, we are clearing all of the data from our cache and resetting default values for all of the fields in the ROOT_QUERY
. Because we are using react-apollo
, this change would trigger a UI update and clear the entire list of users from the current DOM.
A good place to write data directly to the cache is inside of the logout
function in the AuthorizedUser
component. At present this function is removing the user’s token, but the UI does not update until the “Refetch” button has been clicked or the browser is refreshed. To improve this feature, we will clear out the current user from the cache directly when the user logs out.
First we need to make sure that this component has access to the client
in its props. One of the fastest ways to pass this property is to use the withApollo
higher order component. This will add the client
to the AuthorizedUser
component’s properties. Since this component already uses the withRouter
higher order component, we will use the compose
function to make sure that the AuthorizedUser
component is wrapped with both higher order components:
import
{
Query
,
Mutation
,
withApollo
,
compose
}
from
'react-apollo'
class
AuthorizedUser
extends
Component
{
...
}
export
default
compose
(
withApollo
,
withRouter
)(
AuthorizedUser
)
Using compose
, we assemble the withApollo
and withRouter
functions into a single function. withRouter
adds the Router’s history
to the properties, and withApollo
adds Apollo Client to the properties.
This means that we can access Apollo Client in our logout
method and use it to remove the details about the current user from the cache:
logout
=
()
=>
{
localStorage
.
removeItem
(
'token'
)
let
data
=
this
.
props
.
client
.
readQuery
({
query
:
ROOT_QUERY
})
data
.
me
=
null
this
.
props
.
client
.
writeQuery
({
query
:
ROOT_QUERY
,
data
})
}
The above code not only removes the current user’s token from localStorage
, it clears the me
field for the current user saved in the cache. Now when users log out, they will see the “Sign In with GitHub” button immediately without having to refresh the browser. This button is rendered only when the ROOT_QUERY
doesn’t have any values for me
.
Another place that we can improve our application thorough working directly with the cache is in the src/Users.js
file. Currently, when we click the “Add Fake User” button, a mutation is sent to the GraphQL service. The Mutation
component that renders the “Add Fake User” button contains the following property:
refetchQueries
=
{[{
query
:
ROOT_QUERY
}]}
This property tells the client to send an additional query to our service once the mutation has completed. However, we are already receiving a list of the new fake users in the response of the mutation itself:
mutation
addFakeUsers
(
$count
:
Int
!
)
{
addFakeUsers
(
count
:
$count
)
{
githubLogin
name
avatar
}
}
Since we already have a list of the new fake users, there is no need to go back to the server for the same information. What we need to do is obtain this new list of users in the mutation’s response and add it directly to the cache. Once the cache changes, the UI will follow.
Find the Mutation
component in the Users.js
file that handles the addFakeUsers
mutation and replace the refetchQueries
with an update
property:
<
Mutation
mutation
=
{
ADD_FAKE_USERS_MUTATION
}
variables
=
{{
count
:
1
}}
update
=
{
updateUserCache
}
>
{
addFakeUsers
=>
<
button
onClick
=
{
addFakeUsers
}
>
Add
Fake
User
<
/button>
}
<
/Mutation>
Now, when the mutation has completed, the response data will be sent to a function called updateUserCache
:
const
updateUserCache
=
(
cache
,
{
data
:
{
addFakeUsers
}
})
=>
{
let
data
=
cache
.
readQuery
({
query
:
ROOT_QUERY
})
data
.
totalUsers
+=
addFakeUsers
.
length
data
.
allUsers
=
[
...
data
.
allUsers
,
...
addFakeUsers
]
cache
.
writeQuery
({
query
:
ROOT_QUERY
,
data
})
}
When the Mutation
component invokes the updateUserCache
function, it sends the cache
and the data that has been returned in the mutation’s response.
We want to add the fake users to the current cache, so we’ll read the data that is already in the cache using cache.readQuery({ query: ROOT_QUERY })
and add to it. First, we’ll increment the total users, data.totalUsers += addFakeUsers.length
. Then, we’ll concatenate the current list of users with the fake users that we’ve received from the mutation. Now that the current data has been changed, it can be written back to the cache using cache.writeQuery({ query: ROOT_QUERY, data })
. Replacing the data in the cache
will cause the UI to update and display the new fake user.
At this point, we have completed the first version of the User portion of our app. We can list all users, add fake users, and sign in with GitHub. We have built a full stack GraphQL application using Apollo Server and Apollo Client. The Query
and Mutation
components are tools that we can use to quickly begin developing clients with Apollo Client and React.
In Chapter 7, we see how we can incorporate subscriptions and file uploading into the PhotoShare application. We also discuss emerging tools in the GraphQL ecosystem that you can incorporate into your projects.