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
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;
Let's also edit our App.js
file to show this component:
[...]
function App() {
return (
<div className="container">
<IPInfo />
</div>
);
}
[...]
...and edit our App.css
file slightly to clean it up:
body {
margin: 2rem;
}
.container {
max-width: 800px;
margin: auto;
}
If we run npm start
, we should be greeted with something like this:
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(),
});
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>
);
}
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;
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;
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);
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:
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,
});
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:
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)