Managing Local State with Apollo Client: Queries and Virtual Fields
In almost every app we build in real, we display a combination of remote data from our API and local data such as network status, form state, and more. Apollo Client simplified this process as it allows us to store local data inside the Apollo cache and query it alongside our remote data with GraphQL.
In a graph application, it is recommended that Apollo cache be used in managing local state instead of bringing in another state management library like Redux. This is necessary as to ensure a single source of truth in your application.
Managing local data with Apollo Client entails you write a client schema and resolvers for your local data. then using the @client directive to query the data.
In the server, a schema is the first step toward defining our data model, similarly writing a local schema is the first step we take on the client.
To get started, navigate to src/resolvers.jsx and copy the following code to create your client schema and an empty resolvers which we will be writing in the later part of this tutorial.
//src/resolvers.jsx
import gql from "graphql-tag";
export const typeDefs = gql`
extend type Query {
isLoggedIn: Boolean!
cartItems: [ID!]!
}
extend type Launch {
isInCart: Boolean!
}
extend type Mutation {
addOrRemoveFromCart(id: ID!): [ID!]!
}
`;
export const resolvers = {};
From the above code snippet, to build the client schema, we extended the types of our server schema and wrapped it with the gql function. The "extend" keyword allows us to combine both schemas inside developer tooling like Apollo VSCode and Apollo DevTools.
Local fields can also be added to server data by extending types from our server. In the above example code snippet, we added the isInCart local field to the Launch type we received from our graph API.
Initialize the store
Having created our client schema, its time to learn how to initialize the store. Since queries execute as soon as the component mounts, it's important for us to warm the Apollo cache with some default state so those queries don't error out. We will need to write initial data to the cache for both isLoggedIn and cartItems:
Moving back to src/index.jsx , notice we had already added a cache.writeData call to prepare the cache in the last section. We also imported the typeDefs and resolvers that we have created to enable us use them:
//src/index.jsx
import { resolvers, typeDefs } from "./resolvers";
const client = new ApolloClient({
cache,
link: new HttpLink({
uri: "http://localhost:4000/graphql",
headers: {
authorization: localStorage.getItem("token")
}
}),
typeDefs, resolvers
});
cache.writeData({ data: { isLoggedIn: !!localStorage.getItem("token"), cartItems: [] }});
So far, we've added default state to the Apollo cache, let's proceed and learn how to query local data from within our React components.
Query local data
Querying local data from the Apollo cache is the same as querying remote data from a graph API. Just that when querying from local, we add the @client directive to tell Apollo Client to pull it from the cache.
An example of local query is the isLoggedIn field in the code snippet below:
src/index.jsx
import { ApolloProvider, useQuery } from "@apollo/react-hooks";
import gql from "graphql-tag";
import Pages from "./pages";
import Login from "./pages/login";
import injectStyles from "./styles";
const IS_LOGGED_IN = gql` query IsUserLoggedIn { isLoggedIn @client }`;
function IsLoggedIn() {
const { data } = useQuery(IS_LOGGED_IN); return data.isLoggedIn ? <Pages /> : <Login />;
}
injectStyles();
ReactDOM.render(
<ApolloProvider client={client}>
<IsLoggedIn />
</ApolloProvider>,
document.getElementById("root")
);
At first, we created our IsUserLoggedIn local query by adding the @client directive to the isLoggedIn field. Then, we rendered a component with useQuery, and passed our local query in. Based on the response, we rendered either a login screen or the homepage depending if the user is logged in. Due to the synchronous nature of Apollo cache, we don't have to account for any loading state.
Looking at another example of a component that queries local state. Proceed to src/pages/cart.jsx. Just like before, we created our query:
//src/pages/cart.jsx
import React, { Fragment } from "react";
import { useQuery } from "@apollo/react-hooks";
import gql from "graphql-tag";
import { Header, Loading } from "../components";
import { CartItem, BookTrips } from "../containers";
export const GET_CART_ITEMS = gql`
query GetCartItems {
cartItems @client
}
`;
Next, we called useQuery and bound it to our GetCartItems query:
//src/pages/cart.jsx
const Cart = () => {
const { data, loading, error } = useQuery(GET_CART_ITEMS);
if (loading) return <Loading />;
if (error) return <p>ERROR: {error.message}</p>;
return (
<Fragment>
<Header>My Cart</Header>
{!data || (!!data && data.cartItems.length === 0) ? (
<p data-testid="empty-message">No items in your cart</p>
) : (
<Fragment>
{!!data &&
data.cartItems.map(launchId => (
<CartItem key={launchId} launchId={launchId} />
))}
<BookTrips cartItems={!!data ? data.cartItems : []} />
</Fragment>
)}
</Fragment>
);
};
export default Cart;
It's important to note local queries with remote queries can be mixed together in a single GraphQL document.
Having leant how to query locally, lets proceed and look at how we can ass virtual fields to our server data.
Adding virtual fields to server data
One of the unique advantages of managing your local data with Apollo Client is that you can add virtual fields to data you receive back from your graph API. These fields only exist on the client and are useful for decorating server data with local state. In this example, we're going to add an isInCart virtual field to our Launch type.
To add a virtual field, first extend the type of the data you're adding the field to in your client schema. Here, we will be extending the Launch type:
//src/resolvers.jsx
import gql from "graphql-tag";
export const schema = gql`
extend type Launch {
isInCart: Boolean!
}
`;
Next, specify a client resolver on the Launch type to tell Apollo Client how to resolve your virtual field :
//src/resolvers.jsx
// previous imports
import { GET_CART_ITEMS } from "./pages/cart";
// type defs and other previous variable declarations
export const resolvers = {
Launch: {
isInCart: (launch, _, { cache }) => {
const queryResult = cache.readQuery({
query: GET_CART_ITEMS
});
if (queryResult) {
return queryResult.cartItems.includes(launch.id);
}
return false;
}
}
};
We're going to learn more about client resolvers in the section below. The important thing to note is that the resolver API on the client is the same as the resolver API on the server.
Now, you're ready to query your virtual field on the launch detail page! Similar to the previous examples, just add your virtual field to a query and specify the @client directive.
src/pages/launch.jsx
export const GET_LAUNCH_DETAILS = gql`
query LaunchDetails($launchId: ID!) {
launch(id: $launchId) {
isInCart @client site
rocket {
type
}
...LaunchTile
}
}
${LAUNCH_TILE_DATA}
`;
So far, we've focused on querying local data from the Apollo cache and we have looked at various examples. Apollo Client also allow you update local data in the cache with either direct cache writes or client resolvers. Direct writes are typically used to write simple booleans or strings to the cache whereas client resolvers are for more complicated writes such as adding or removing data from a list.
In the very next tutorial, we will learn more about these resolvers.
Previous:
Updating Local Data in Apollo Cache with Resolvers & Direct Writes.
Next:
The Apollo CLI
It will be nice if you may share this link in any developer community or anywhere else, from where other developers may find this content. Thanks.
https://www.w3resource.com/apollo-graphql/local-state-management.php
- Weekly Trends and Language Statistics
- Weekly Trends and Language Statistics