Modeling an Instagram Clone: Integrating Apollo Client - Dgraph Blog

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 follwers, 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 the Components 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 new ApolloClient instance using the ApolloClient 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.

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 to ApolloProvider.
  • Remember that we imported the root App component inside the ApolloWrapper.js file. This sits at the highest level of the component tree; wrapping this will expose the client to lower levels. So we wrap App inside ApolloProvider 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 the id_token.
  • Next we use the useEffect hook to obtain the id_token asynchronously, by calling the getIdTokenClaims function. We only call this function if isAuthenticated is true, i.e. the user has logged in. Then we set the state with the value of that token that now resides in the xAuthToken variable. The hook only runs when either getIdTokenClaims or isAuthenticated 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 the createHttpLink function, supplying your backend URL as the uri 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 the id_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 an X-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 user felix.

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 your src directory.
  • Create a new file called Queries.js inside the new directory src/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 the src/Components directory. This file will hold a Feed 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 the useAuth0 hook. This object contains information about the authenticated user, like name, email, and so on.
  • We extract only the email parameter from the user 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 a loading 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.
  • 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 of felix’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 called withAuthenticationRequired.
    • 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.

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/
1 Like

@diggy - I think I may have been wrong on that data model.

I believe it is impossible to get the follower feed the way we expect it to work until nested filters is implemented.

I believe this will return an array of followers with their posts, when what we want is an array of posts by their followers.

So this will not sort the posts, as they will be separated by each follower – hence the difference.

This was my misunderstanding, but thank you for listening to my post!

Perhaps @amaster507 can confirm this.

Thanks,

J

confirm this I will.

That is why I had to build my own nested filtering logic in my UI.