DEV Community

Chris Held for Headway

Posted on • Updated on

React: Prototyping with Apollo Client Schemas

Building a prototype is a great way to validate an idea, or to gather feedback from users without taking on the risk of having to build out an entire application. In this tutorial we'll take a look at Apollo Client and how we can leverage a client side schema to set us up for success when we're ready to build out an API to talk to our front end.

Apollo Client is a tool used to manage your client side data. It's typically paired with Apollo Server but it will work with any GraphQL server implementation, which makes it great for prototyping. Even if we choose a different GraphQL implementation for our server like Absinthe later, we can still keep our front end queries as long as the schema is defined the way we're expecting.

For our demo we're going to create an app that will return some information about our user's location based on their IP Address. Let's get started!

First we'll spin up a react app and install apollo:

npx create-react-app apollo-client-schema-demo
cd apollo-client-schema-demo
npm i
npm install @apollo/client graphql
Enter fullscreen mode Exit fullscreen mode

First let's create a component to display our user's information. We don't really need to worry about where the data is coming from right now so we'll use static data. Create an IPInfo.js file that looks like this:

import React from "react";

const IPInfo = () => {
  const data = {
    ipAddress: "1.1.1.1",
    city: {
      name: "Sheboygan",
      population: 123456,
    },
    country: {
      name: "USA",
      population: 123456,
    },
  };

  return (
    <main className="App">
      <h1>Howdy!</h1>
      <p>Your IP Address is {data.ipAddress}</p>
      <p>
        {`Your city, ${data.city.name}, has a current population of
         ${data.city.population}`}
      </p>
      <p>
        {`Your Country, ${data.country.name}, has a current population of
         ${data.country.population}`}
      </p>
      <p>Cool, huh?</p>
    </main>
  );
};

export default IPInfo;

Enter fullscreen mode Exit fullscreen mode

Let's also edit our App.js file to show this component:

[...]
function App() {
  return (
      <div className="container">
        <IPInfo />
      </div>
  );
}
[...]
Enter fullscreen mode Exit fullscreen mode

...and edit our App.css file slightly to clean it up:

body {
  margin: 2rem;
}

.container {
  max-width: 800px;
  margin: auto;
}
Enter fullscreen mode Exit fullscreen mode

If we run npm start, we should be greeted with something like this:

IP Info Page

Now we need to set up an apollo client. Add the following to App.js:

import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";

const client = new ApolloClient({
  uri: "https://48p1r2roz4.sse.codesandbox.io",
  cache: new InMemoryCache(),
});

Enter fullscreen mode Exit fullscreen mode

This sets up an instance of ApolloClient. The uri we chose is from the Apollo Documentation and can be used as a placeholder until we have a real server to point to. The contents of the server don't really matter since we'll only be pointing at our client schema, but it is a required field when instantiating a client.

In order to tie our app to apollo, we need to wrap it in an instance of ApolloProvider. To do that we'll need to edit our App component:

function App() {
  return (
    <ApolloProvider client={client}>
      <div className="container">
        <IPInfo />
      </div>
    </ApolloProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

If we refresh we shouldn't see any difference since we aren't actually querying for anything. To do that without having an actual server to call, we can define typeDefs in our app and pass them in to our client instantiation. Let's make some modifications to App.js:

import React from "react";
import "./App.css";

import {
  ApolloClient,
  ApolloProvider,
  InMemoryCache,
  gql,
} from "@apollo/client";
import IPInfo from "./IPInfo";

const typeDefs = gql`
  extend type Query {
    client: Client!
  }

  extend type Client {
    ipAddress: IPAddress!
  }

  extend type IPAddress {
    address: String!
    city: City
    country: Country
  }

  extend type City {
    name: String!
    population: Int
  }

  extend type Country {
    name: String!
    population: Int!
  }
`;

const client = new ApolloClient({
  uri: "https://48p1r2roz4.sse.codesandbox.io",
  cache: new InMemoryCache(),
  typeDefs,
});

function App() {
  return (
    <ApolloProvider client={client}>
      <div className="container">
        <IPInfo />
      </div>
    </ApolloProvider>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Here, we're defining typeDefs and creating a client query and some types to support it, then passing that in to our client constructor. Now we can use apollo's useQuery hook to fetch the results of that query, even though we still haven't written anything to resolve it and we haven't build out a server. Let's do that in IPInfo.js:

import React from "react";
import { useQuery, gql } from "@apollo/client";

const CLIENT_QUERY = gql`
  {
    client @client {
      ipAddress {
        address
        city {
          name
          population
        }
        country {
          name
          population
        }
      }
    }
  }
`;

const IPInfo = () => {
  const {
    data: {
      client: { ipAddress: { address, city = {}, country = {} } = {} } = {},
    } = {},
    loading,
    error,
  } = useQuery(CLIENT_QUERY);

  if (loading) {
    return (
      <p>
        Hmm...{" "}
        <span role="img" aria-label="thinking emoji">
          🤔
        </span>
      </p>
    );
  }

  if (error) {
    return (
      <p>
        Ruh Roh{" "}
        <span role="img" aria-label="sad emoji">
          😫
        </span>
      </p>
    );
  }

  return (
    <main className="App">
      <h1>Howdy!</h1>
      <p>Your IP Address is {address}</p>
      <p>
        {`Your city, ${city.name}, has a current population of
         ${city.population}`}
      </p>
      <p>
        {`Your Country, ${country.name}, has a current population of
         ${country.population}`}
      </p>
      <p>Cool, huh?</p>
    </main>
  );
};

export default IPInfo;
Enter fullscreen mode Exit fullscreen mode

We've changed a lot here, so let's step through.

First we define our graphql query. Nothing very special there if you're familiar with graphql, but note the @client directive. That tells apollo that this doesn't exist on the server, so there's no need to ask the server for this.

In the actual component code we take advantage of apollo's useQuery hook to make our query:

  const {
    data: {
      client: { ipAddress: { address, city = {}, country = {} } = {} } = {},
    } = {},
    loading,
    error,
  } = useQuery(CLIENT_QUERY);
Enter fullscreen mode Exit fullscreen mode

This gives us all the data we need to power our form, plus a few variables to manage different query states. Our markup has remained largely the same, though we did add a bit to handle loading and error states.

If we refresh our page, we'll see a whole lot of nothing:

IP Info Screen with no data

Why is that? Well, in our client schema we only defined the shape of our data, but not it's contents. To do that we need to create a resolver. Let's add one right underneath our schema in App.js:

const resolvers = {
  Query: {
    client: () => ({
      ipAddress: {
        address: "172.220.20.36",
        city: {
          name: "Sheboygan",
          population: 48895,
        },
        country: {
          name: "United States of America",
          population: 325145963,
        },
      },
    }),
  },
};

const client = new ApolloClient({
  uri: "https://48p1r2roz4.sse.codesandbox.io",
  cache: new InMemoryCache(),
  typeDefs,
  resolvers,
});

Enter fullscreen mode Exit fullscreen mode

Don't forget to add your resolvers object to your client.

In our resolver, we defined what should be returned when something calls the client query. We could make this more random if we wanted, but this will suit our prototype just fine. Now if we refresh we see the data from our resolver:

IP Info screen with mocked data

Let's say in parallel we did some research and found out there was a site, everbase.co, that had a schema that perfectly matched our client query. What a coincidence! All we have to do now is update our client url and remove the @client directive from our query and voila, we have an app connected to real data.

By doing the work of setting up our client and mocking our queries out up front, we wind up laying a lot of infrastructure necessary to complete our application when the time comes. If you'd like to see our demo in action it can be found here, or you can check out the source here. If you'd like to do some further research the Apollo docs are a great resource. Thanks for reading!

Top comments (0)