Across our last three articles in this series, we explored many concepts of GraphQL and Dgraph. As we continue to do so, today we’re going to integrate Apollo into the app and build more of the UI. We’ll also modify the GraphQL schema to improve our data model, resembling more of a real-world social media app. This will also allow us to generate the application feed containing posts from users one is following.
Let’s look at the list of topics:
If you want, you can take a look at the previous articles in this series that follow up to this one:
You can find all the code changes for today’s article here.
A better data model
We can improve our data model by changing the follower
and following
fields of our User
type. Technically, these should be actual User
entities rather than mere integers.
- Go to your Dgraph Cloud dashboard.
- Open the Schema window from the left sidebar and make the following changes:
type User {
...
following: [User] @hasInverse(field: follower)
follower: [User] @hasInverse(field: following)
}
The accounts each user is following
, and their follwer
s, are under the hood User
types. Here we’ve established that relationship by using the hasInverse
directive. Now it would be possible to get details on the accounts we’re following by querying for the following
fields. You’ll soon see that in action when rendering the feed.
After making the edits, click Deploy to re-deploy your backend.
Integrating Apollo
Apollo is a GraphQL client for JavaScript. You can use it with frameworks/libraries like React, Angular, etc. This is what your React app will use under the hood to make GraphQL requests.
Generally, all of the resources of a GraphQL service are exposed over HTTP via a single endpoint. A client like Apollo comes packed with many features that make a GraphQL service scalable over HTTP. Some of these functionalities include caching, UI integration, etc.
So let’s install Apollo:
npm install @apollo/client graphql
This will install the following packages:
-
@apollo/client
: This is the core of Apollo client. It has tools for caching, error handling, etc. -
graphql
: You need this package for parsing GraphQL queries.
Creating a Client instance
In your .env
file, create another variable holding your Dgraph Cloud endpoint.
REACT_APP_BACKEND_ENDPOINT="<<your-Dgraph-Cloud-endpoint>>"
You can access it from your Dgraph Cloud dashboard.
We’ll create a new component holding everything related to Apollo.
- So create a new file called
ApolloWrapper.js
in theComponents
directory. Then import the following definitions there:
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
import App from '../App';
- Inside a new functional component called
ApolloWrapper
, create a newApolloClient
instance using theApolloClient
constructor:
const ApolloWrapper = () => {
const client = new ApolloClient({
uri: process.env.REACT_APP_BACKEND_ENDPOINT,
cache: new InMemoryCache(),
});
};
What’s happening here?
- We pass the constructor an object specifying our client configuration.
- The
uri
parameter is the address of the backend endpoint, that we pull in from the.env
file. - We also initialize caching mechanism by starting an instance of
InMemoryCache
. This caches query results so that the app can access data faster.
- The
Connecting React with ApolloClient
After setting up the client, you need to connect it with your application. That’s where the ApolloProvider
component comes in. It will wrap the root of your React app, exposing the client throughout the component tree. This way, any component down the tree can use GraphQL if necessary.
You can do that in the ApolloWrapper
component:
const ApolloWrapper = () => {
...
return (
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
};
export default ApolloWrapper;
What’s happening here?
- We pass the
client
as a prop toApolloProvider
. - Remember that we imported the root
App
component inside theApolloWrapper.js
file. This sits at the highest level of the component tree; wrapping this will expose the client to lower levels. So we wrapApp
insideApolloProvider
and return the whole thing. - Then we’re export the component.
In index.js
file, we just need to place the ApolloWrapper
component inside the ReactDOM.render
function. Below is how your index.js
file should look like now:
import React from "react";
import ReactDOM from "react-dom";
import { Auth0Provider } from "@auth0/auth0-react";
import ApolloWrapper from './Components/ApolloWrapper';
const domain = process.env.REACT_APP_AUTH0_DOMAIN;
const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID;
ReactDOM.render(
<Auth0Provider
domain={domain}
clientId={clientId}
redirectUri={window.location.origin}
>
<React.StrictMode>
<ApolloWrapper />
</React.StrictMode>
</Auth0Provider>,
document.getElementById("root")
);
Run npm start
from your terminal and everything should work like before.
That’s it. This is how simple it can be to integrate a client with a React application. But we need to make some changes for it to work completely with our requirements.
Follow me to the next section.
Obtaining the JWT token using Auth0 React SDK
Remember that the HTTP requests need to have the X-Auth-Token
header containing the id_token
so that Dgraph can verify the requests. We’ve been doing that manually from the Dgraph Cloud console. You need to configure Apollo so that it automatically sends this header with the requests.
- In
ApolloWrapper.js
, make the following imports:
import { setContext } from "@apollo/client/link/context";
import { useAuth0 } from "@auth0/auth0-react";
import { useState, useEffect } from "react";
- Inside the
ApolloWrapper
component, put the following bit of code:
const { isAuthenticated, getIdTokenClaims } = useAuth0();
const [xAuthToken, setXAuthToken] = useState("");
useEffect(() => {
const getToken = async () => {
const token = isAuthenticated ? await getIdTokenClaims() : "";
setXAuthToken(token);
};
getToken();
}, [getIdTokenClaims, isAuthenticated]);
What’s happening here?
- From
useAuth0
hook, we extract two things:- A boolean value
isAuthenticated
, indicating the authentication status of a user. - A function called
getIdTokenClaims
that yields theid_token
.
- A boolean value
- Next we use the
useEffect
hook to obtain theid_token
asynchronously, by calling thegetIdTokenClaims
function. We only call this function ifisAuthenticated
is true, i.e. the user has logged in. Then we set the state with the value of that token that now resides in thexAuthToken
variable. The hook only runs when eithergetIdTokenClaims
orisAuthenticated
changes.
At this point, I should point out that what we’re doing here is changing how data flows between our backend and Apollo client. The client now needs to send some extra information to the API, so it’s not just plain HTTP requests anymore without any authentication flow.
Apollo has a library called Apollo Link that lets us modify the usual data flow. We can write multiple “links”, that each does a separate thing, and then chain them together.
So let’s write a link whose only purpose is to take care of HTTP headers. Specifically, it’ll set the X-Auth-Token
header, and return any other header that might exist. We’re also going to write a link that just handles the HTTP request. Later we’ll chain these two together inside the ApolloClient
instance.
- Import the following definitions first:
import {
...
createHttpLink,
} from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
- Now create a
HttpLink
using thecreateHttpLink
function, supplying your backend URL as theuri
parameter like before:
const httpLink = createHttpLink({
uri: process.env.REACT_APP_BACKEND_ENDPOINT,
});
And now the link for setting authentication headers:
const authLink = setContext((_, { headers, ...rest }) => {
if (!xAuthToken) return { headers, ...rest };
return {
...rest,
headers: {
...headers,
"X-Auth-Token": xAuthToken.__raw,
},
};
});
What’s happening here?
- We make use of the
setContext
function from Apollo to take care of the headers.- This function takes another function as the only argument. The inner function takes two arguments, as you can see above. It returns an object that sets the current context.
- The first argument is usually a request, which doesn’t matter in this case. So we ignore it.
- The second argument is the context. Context in React is another way to pass data like props. In our case, the data are the headers, and optionally any other data that might exist (which we capture via
rest
). They’re both inside an object.
- If there’s no token, then we return what contextual data we have.
- Otherwise, we set the
X-Auth-Token
header, setting its value equal to theid_token
field of the state variable.
Note: If you’re curious, you can examine the object getIdTokenClaims
returns. Just do a console.log(xAuthToken)
inside the getToken
function of the useEffect
hook, right after you set the state.
Now you can create the ApolloClient
instance like this:
const client = new ApolloClient({
cache: new InMemoryCache(),
link: authLink.concat(httpLink),
});
With that, we’ve finished integrating Apollo with our application! We can now start making GraphQL queries from within the app. For starters, let’s generate the application feed containing posts from users someone is following.
Generating the application feed
We’ll start fresh with a little bit of data that we can use to render the feed.
- On the modal that appears, select Drop All Data.
- You’ll see a modal again where you need to confirm it. After confirming, you’ll see a Success message stating a successful removal of all the data in your Dgraph backend.
Adding some mock data
Now we’ll start adding some test data for trying out Apollo and sending GraphQL requests.
As a first step, let’s simulate opening an account for ourselves.
First, we need to log in from the UI and obtain the ID token from Auth0. Then as usual we’ll use it as a request header. With how we’ve set up the authentication-authorization cycle, Dgraph will always expect a verified JWT in the X-Auth-Token
HTTP header.
We’ve gone through the following steps before. You can access them at the Getting the JWT token section of the authentication article, but here’s a recap:
- Go to the root of your application source code and run
npm start
from the terminal. - Open up your browser’s development console and go to the Network tab.
- When the UI renders, click on the Log in button if you’re logged out. Otherwise, you can refresh your browser window to receive the token again.
- After a successful login, you should see a networking event with a file type named token in the Network tab.
- Clicking on it will open its details. From there go to the Response tab.
- Copy the
id_token
field. - Now go to the GraphQL window from the left sidebar of your dashboard. Click on Request Headers above Explorer.
- Paste your
id_token
as the value of anX-Auth-Token
header.
Now we can dispatch an addUser
mutation.
- From the GraphQL window, paste the following in the text area:
mutation AddAUser($userInput: [AddUserInput!]!) {
addUser(input:$userInput) {
user {
name
username
}
}
}
The variable will hold the necessary details. For example, here’s mine:
{
"userInput": [
{
"username": "sakib",
"name": "Abu Sakib",
"email": "sakib@dgraph.io",
"about": "Programming, writing and literature.",
"avatarImageURL": "https://robohash.org/sakib"
}
]
}
- Click on the Play icon to execute the mutation.
Next, we’ll add another account using a different email address. It’d be good to use an email address that you can log into yourself. There are corner cases that by design have come into play so that you can follow along with the article by yourself easily:
- This account will be the “user” that the previous account will “follow” in our Instagram clone app.
- We’ll add a couple of posts for this account. Because of the authorization rules in the schema, you can’t add a post unless you’re logged in. That’s why you need to be able to log into this second account.
- After adding some posts, the application feed of the user
sakib
will contain posts from this second userfelix
.
So run the same mutation with the following data:
{
"userInput": [
{
"username": "felix",
"name": "Felix Biedermann",
"email": "mabusakib@gmail.com",
"about": "I tell stories.",
"avatarImageURL": "https://robohash.org/felix"
}
]
}
As the next step, let’s simulate the event of user sakib
following the user felix
. We can make use of the following updateUser
mutation for doing so:
mutation FollowAnAccount($updateInput: UpdateUserInput!) {
updateUser(input:$updateInput) {
user {
username
email
following {
username
email
about
}
}
}
}
The variable data will be:
{
"updateInput": {
"filter": {
"email": { "eq": "sakib@dgraph.io" }
},
"set": {
"following": [
{
"username": "felix"
}
]
}
}
}
Great! You’re now following a user on the app.
Next, let’s simulate the event of felix
posting some images on his InstaClone account.
- Log in using the second email address (of
felix
) from the UI of your app. - Obtain the
id_token
and paste it as a Request Header like before. - Then run the following mutation:
mutation AddPosts($postData: [AddPostInput!]!) {
addPost(input:$postData) {
post {
description
id
likes
description
}
}
}
With the variable data as:
{
"postData": [
{
"imageURL": "https://picsum.photos/id/156/2177/3264",
"description": "We went to the beach!",
"postedBy": {
"username": "felix"
},
"likes": 0
},
{
"imageURL": "https://picsum.photos/id/154/3264/2176",
"description": "I don't feel like going back from this trip...",
"postedBy": {
"username": "felix"
},
"likes": 0
}
]
}
With this last step, we’re done adding the necessary mock data to start building out the application feed.
Using useQuery React hook to make GraphQL queries
Time to write some GraphQL!
- Create a new folder
GraphQL
inside yoursrc
directory. - Create a new file called
Queries.js
inside the new directorysrc/GraphQL
. - This file will hold all GraphQL queries. For now, we only need one that fetches enough details to generate the feed:
import { gql } from "@apollo/client";
export const QUERY_LOGGED_IN_USER = gql`
query getUserInfo($userFilter: UserFilter!) {
queryUser(filter: $userFilter) {
email
following {
username
avatarImageURL
posts {
id
imageURL
description
likes
postedBy {
username
avatarImageURL
}
}
}
}
}
`;
The gql
is a template literal. With this, you can write GraphQL queries inside your JavaScript code. Later, it parses the query into the GraphQL AST, which both Apollo and the server can understand.
The query should be very familiar to you. And don’t worry, we’ll now discuss how to actually “execute” this query, and supply the query variable!
- Create a new file
Feed.js
in thesrc/Components
directory. This file will hold aFeed
component, in charge of rendering the application feed. - Then make the following imports there. You’ll need them throughout this component:
import { useQuery } from "@apollo/client";
import { QUERY_LOGGED_IN_USER } from "../GraphQL/Queries/Queries";
import { useAuth0, withAuthenticationRequired } from "@auth0/auth0-react";
import {
Spinner,
Text,
Card,
CardBody,
Heading,
Avatar,
CardHeader,
Box,
Grid,
Image,
CardFooter,
} from "grommet";
- Now let’s start writing the
Feed
component.
const Feed = () => {
const { user } = useAuth0();
const { email } = user;
const { data, loading, error } = useQuery(QUERY_LOGGED_IN_USER, {
variables: {
userFilter: {
email: {
eq: email,
},
},
},
});
if (loading) {
return <Spinner />;
}
if (error) {
return (
<div>
<Text size="large" color="red">
{error.message}
</Text>
</div>
);
}
return (
<Box pad="large" direction="row" alignSelf="center">
<Grid
gap="large"
rows="medium"
columns={{ count: "fit", size: ["small", "medium"] }}
>
{data.queryUser.map((userInfo) =>
userInfo.following.map((followedUser) =>
followedUser.posts.map((post) => (
<Card width="medium" key={post.id}>
<CardHeader
pad={{ horizontal: "small", vertical: "small" }}
background="light-1"
width="medium"
justify="stretch"
gap="small"
>
{/* Avatar of the user */}
<Avatar
size="small"
src={post.postedBy.avatarImageURL}
a11yTitle="Generated avatar for the user from robohash.org"
/>
<Heading level="4" margin="none">
{post.postedBy.username}
</Heading>
</CardHeader>
{/* The image of the post*/}
<CardBody height="medium">
<Image fit="cover" src={post.imageURL} />
</CardBody>
<CardFooter
pad={{ horizontal: "small" }}
height="xxsmall"
background="light-3"
justify="between"
gap="xxsmall"
>
<Text size="small">{post.description}</Text>
<Heading level="5" margin="none">
{post.likes == 1
? `${post.likes} like`
: `${post.likes} likes`}
</Heading>
</CardFooter>
</Card>
))
)
)}
</Grid>
</Box>
);
}
export default withAuthenticationRequired(Feed, {
onRedirecting: () => <Spinner />,
});
What’s happening here?
- We extract the
user
object from theuseAuth0
hook. This object contains information about the authenticated user, like name, email, and so on. - We extract only the
email
parameter from theuser
object. This is what we need for now in our GraphQL query. - The
useQuery
hook is what Apollo offers for making GraphQL queries. The hook takes many options, but we’re only making use of two of them.- The first option is the query we prepared earlier.
- The second option is for GraphQL variables. We’re passing the variable value containing the logged-in user’s email address that we just pulled in using
useAuth0
hook.
- After executing the query,
useQuery
returns an object containing the result.- The
loading
property indicates aloading
state when the data hasn’t come through yet. -
error
contains any error that might have occurred in the process. - The
data
property contains the actual data after a successful response.
- The
- While data is still loading, we return a
Spinner
component to visually represent a “loading” state in the app. - In case of an error, the component returns the error message.
- Otherwise, we
map
through the JSON response tree. Using Grommet’s UI components, we render the details offelix
’s posts. The details we expose are:- The avatar
- The username
- The image
- Description of the image
- Number of likes
- We don’t want to show the
Feed
component to someone who’s not logged in. For this purpose, we use the higher-order component calledwithAuthenticationRequired
.- A higher-order component wraps another component and returns a new one. This allows us to write custom logic for the UI. For example, here we’re “protecting” a component from unauthenticated users. If someone like that visits the app, Auth0 will instead render the
Spinner
component, as it redirects the user to the login page.
- A higher-order component wraps another component and returns a new one. This allows us to write custom logic for the UI. For example, here we’re “protecting” a component from unauthenticated users. If someone like that visits the app, Auth0 will instead render the
You should see the new Feed
component in action when you visit the app:
Yay! Our toy Instagram clone can now make authenticated requests to the backend, fetch data, and use that to render a feed.
Conclusion
Today we learned how to set up Apollo Client in a React application and use it to perform GraphQL queries. We also learned how to configure the client for authentication over HTTP. We improved upon the schema of the app and built more of the UI.
With the Dgraph platform taking care of the backend and the API, we can focus completely on learning and developing the app. There are no distractions, and we’re able to iterate faster to meet our goals. This becomes an even more powerful incentive in business-level, large-scale products.
Stay tuned for the upcoming deep-dives into more concepts and exciting stuff.
Below are some more resources that might help you regarding today’s discussion:
ReferencesCover photo is “communication” by urbanfeel, licensed under CC BY-ND 2.0 .This is a companion discussion topic for the original entry at https://dgraph.io/blog/post/insta-apollo/