This short tutorial will show you how to use GraphQL cache for scenarios where you need a fast response and good performance. But first, what is a cache? A cache is an interface that can help us store Request/Response object pairs. It preserves resources by saving or storing data from requests usually in memory, and with that, we can make new requests directly to the cache and not the server or API. This makes subsequent API requests faster.
Besides improving the speed of API requests, the cache will also help increase the application’s performance by reducing the overall network traffic, so the data that is not being cached will be returned faster by the server. Also, when the server is not available due to an issue or crash, the data cached will still be available to the application—giving more time for the team to solve the issues in the server.
For the backend in this tutorial, we will use GraphQL, a query language for APIs, and NodeJs, a server-side runtime that executes the queries GraphQL. It’s not the typical library tied to a specific language or database, as you can use it in any programming language. You can read more about GraphQL here, Switching from REST to GraphQL. For the front-end part, we will use React Native to build an autocomplete application reading data directly from the cache.
First, we will work on the backend part, where we need to set up our project using npm or yarn. In this case, we are going to use npm:
npm init
After we set up our project, we need to create the folder structure this way:
server/ graphql/ resolvers/ schema/ package.json app.js modus-autocomplete/ ===> mobile app code goes here
Then we need to install dependencies for the project. Apollo works as a spec-compliant GraphQL server and it’s compatible with any GraphQL client. So, we are going to use Apollo to handle all the configuration for the server. Let’s install the dependencies on the backend side:
First, let’s move to the server folder:
cd server
Now, let’s install the dependencies:
npm install apollo-server graphql node-fetch
Node-fetch will be used to make requests to JSONPlaceholder – Fake online REST API for testing and prototyping since we will use their API for the data of our app. You can also use Axios if you feel more comfortable.
Now let’s get our hands in the code. First, we are going to set our GQL Schema:
schema/index.js
const { gql } = require("apollo-server"); module.exports = gql` type Todo { id: ID userId: Int title: String completed: Boolean } input TodoInput { userId: Int title: String completed: Boolean } type Query { getTodos: [Todo] getTodo(id: ID): Todo } type Mutation { addTodo(input: TodoInput): Todo } `;
First, we need to import gql from the apollo-server library. After that, we define our schema using gql. We define Todo Type, which holds all the necessary information for the todos that we will receive from the API. We also need to define an input (TodoInput), which defines the data stored in the database for a specific type. The input definition will be used when we create a new todo for the app.
We also need to define our Queries and Mutations. For queries, let’s define getTodos that will retrieve a list of all todos, and getTodo that returns a specific todo that we want to find by the ID.
Then we are going to create our resolvers for the Queries and Mutations in the resolvers folder. Let’s create three files – mutation.js, query.js, and index.js.
mutation.js
const fetch = require("node-fetch"); const Mutation = { addTodo: async (parent, args, context, info) => { try { const { input: { title, completed, userId }, } = args; const response = await fetch( "https://jsonplaceholder.typicode.com/todo", { method: "POST", body: JSON.stringify({ title, completed, userId, }), headers: { "Content-type": "application/json; charset=UTF-8", }, } ); const todo = await response.json(); return todo; } catch (error) { throw new Error(error); } }, }; module.exports = Mutation;
Here we are creating the addTodo resolver for the mutation defined in the schema file. Remember that for the data passed to the resolver, we will use the input defined in our schema, TodoInput.
query.js
const fetch = require("node-fetch"); const Query = { getTodos: (parent, args, context, info) => fetch("https://jsonplaceholder.typicode.com/todos") .then((response) => response.json()) .then((json) => json) .catch((err) => console.error(err)), getTodo: (parent, args, context, info) => { const { id } = args; return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`) .then((response) => response.json()) .then((json) => json) .catch((err) => console.error(err)); }, }; module.exports = Query;
Here we are creating the resolvers for the queries defined in the schema file, getTodos and getTodo.
Now in the index.js file, we are going to import the queries and mutations that were created recently.
index.js
const todoMutation = require("./mutation"); const todoQueries = require("./query"); const Query = { ...todoQueries, }; const Mutation = { ...todoMutation, }; module.exports = { Query, Mutation, };
Now that we have all the resolvers defined, we need to configure our entry point. This will be the app.js that we defined in the root path of the project. Let’s see how it should look:
app.js
const { ApolloServer } = require("apollo-server"); const typeDefs = require("./graphql/schema/index"); const resolvers = require("./graphql/resolvers/index"); const server = new ApolloServer({ typeDefs, resolvers, tracing: true, cacheControl: { defaultMaxAge: 300, // default cache timeout calculateHttpHeaders: false, stripFormattedExtensions: true, }, }); server.listen().then(({ url }) => { console.log(`Server running at ${url}`);
As we can see here, the resolvers and also the schema were imported to the app.js to configure our Graphql server. Since this tutorial focuses on working with cache, we need to set the cacheControl property and define a value for the defaultMaxAge, which will be the time in seconds that the data will be cached. There are other ways to set the cache hints, for example:
Set hints for specific types, queries or mutations:
type Todo @cacheControl(maxAge: 30) { id: ID userId: Int title: String completed: Boolean }
In this case, the maxAge is set to 30 seconds and this will be used for this specific type instead of the maxAge defined on the server configuration. We can also define maxAge for specific properties defined in our types.
Now, let’s run npm start to see our GraphQL server running
https://www.screencast.com/t/zT8lbNtTqtz
Now after we review and test our GraphQL server, we are going to start with the frontend part, first we need to create a new folder, let’s call it, modus-autocomplete:
cd .. && cd modus-autocomplete
Now, let’s start setting up our project using npx:
npx react-native init ModusAutocomplete
Once the setup is done, we will need to add a couple of files to create our autocomplete app. Let’s go to the root folder of our recently created project and add the following dependencies:
cd ModusAutocomplete && yarn add @apollo/client graphql-tag
Now we need to add our Apollo client config like this. We will add this file in our root folder of the React Native project that we just created:
ModusAutocomplete/ApolloClient.js
import { ApolloClient, InMemoryCache } from "@apollo/client"; const client = new ApolloClient({ uri: "http://localhost:4000/", cache: new InMemoryCache(), }); export default client;
InMemory cache helps us with the performance and speeds up the execution of the queries that don’t rely on real-time data. We can add more properties to the InMemory cache constructor to add extra configurations. But for now, we are going to work with no extra config.
After that, we need to import our client recently created to the main file. For that, let’s go to the App.js file:
ModusAutocomplete/App.js
import React from "react"; import { ApolloProvider } from "@apollo/client"; import Home from "./src/components/Home"; import client from "./ApolloClient"; // Client that we recently created const App: () => React$Node = () => { return ( ); }; export default App;
Here we can see the apollo client that we just created. That client holds the config for the server connections and cache configuration. We also import ApolloProvider, that component from the @apollo/client will share the client object to the entire React component tree.
After that, we can add the queries that we are going to use for our application:
ModusAutocomplete/src/queries/Queries.js
import gql from "graphql-tag"; const GET_TODOS = gql` query getTodos { getTodos { id userId title completed } } `; export default GET_TODOS;
Now, let’s create our components. It will be a simple small component. I split the component into small pieces that will help with the code maintainability.
First, let’s add our Home component:
ModusAutocomplete/components/Home.js
import React, { useEffect } from "react"; import { SafeAreaView, StyleSheet, ScrollView, View, StatusBar, } from "react-native"; import { Colors } from "react-native/Libraries/NewAppScreen"; import { useQuery } from "@apollo/client"; import Header from "./Header"; import Search from "./Search"; import Todo from "./Todo"; import GET_TODOS from "../queries/Queries"; const Home: () => React$Node = () => { const [todo, setTodo] = React.useState(null); const { data } = useQuery(GET_TODOS); return ( <>
{todo && } </> ); }; const styles = StyleSheet.create({ scrollView: { backgroundColor: Colors.lighter, }, body: { backgroundColor: Colors.white, display: "flex", flexDirection: "column", alignContent: "center", alignItems: "center", flex: 1, }, }); export default Home;
Here we can see that we use one query GET_TODOS in our home component for the first load of our data. This will fill the cache with the result from that query. And we are using the apollo hook useQuery, which is used to fetch the data from the server. It has two possible results: error and data and also a third value is returned. Loading this value is available during the execution of the query and once the query is done it will return either data or error.
Now let’s add three more objects, Search that will hold the main feature of the application, also the Header component that is a simple header, and last the Todo component that will hold the information related to the todo object selected.
ModusAutocomplete/components/Search.js
import React, { useState } from "react"; import { StyleSheet, View, Text, TextInput, FlatList, TouchableOpacity, } from "react-native"; import { ApolloConsumer } from "@apollo/client"; import GET_TODOS from "../queries/Queries"; const Search: (props) => React$Node = (props) => { const [searchKeyword, setSearchKeyword] = useState(""); const [showResults, setShowResults] = useState(false); const [searchResults, setResults] = useState([]); const [tempResults, setTempResults] = useState([]); const search = async (value, client) => { setSearchKeyword(value); setShowResults(true); if (value.length > 0) { const { data } = await client.query({ query: GET_TODOS, }); if (data) { const results = data.getTodos.filter((element) => { return element.title.includes(value.toLowerCase()); }); const newItems = results.length > 0 ? results : searchResults; setResults(newItems); } } }; const handleSelect = (item) => { setSearchKeyword(item.title); setShowResults(false); props.handler(item); }; return ( {(client) => ( Search todos search(text, client)} value={searchKeyword} /> {showResults && ( { return ( handleSelect(item)} > {item.title} ); }} keyExtractor={(item) => item.id} style={styles.searchResultsContainer} /> )} )} ); }; export default Search;
Here in the Search component, we are going to use the ApolloConsumer component. This will give us direct access to the apollo client instances created before and provide a render prop function as a child.
We also define a search function, we are going to use the client object provided by the ApolloConsumer component, and we will use the query GET_TODOS to fetch the data. But this time, it will be from the cache and not from the server. This is where we will use the cache to fetch the data, which will improve the application’s performance. Why? Because the execution time will be shorter than the first load that we did in the Home component, thanks to the cache control that we set up in our application.
Now let’s add the other two missing components – Header and Todo:
ModusAutocomplete/components/Header.js
import React from 'react'; import { StyleSheet, ImageBackground } from 'react-native'; import { Colors } from 'react-native/Libraries/NewAppScreen'; const Header = (): Node => ( ); export default Header;
ModusAutocomplete/components/Todo.js
import React, { useEffect } from "react"; import { SafeAreaView, StyleSheet, ScrollView, View, Text } from "react-native"; import { Colors } from "react-native/Libraries/NewAppScreen"; const Todo: (props) => React$Node = (props) => { const { todo } = props; return ( <> {todo.title} Completed:{" "} {todo.completed ? "Yes, good work" : "No, you have work todo"} </> ); }; export default Todo;
Now, let’s start our server and application to see the results in action:
I added a console log on the server to see the first time the query was executed. Then, we can notice that it’s executed once, so when we start typing, the query goes directly to the cache instead of the server.
In conclusion, the cache can be a useful tool if we want to improve performance in scenarios similar to the one we just worked on. It can help a lot when handling a large amount of data. Since we just need to fetch the data from the server and then reach out to the cache. We can also use Apollo Server cache with other tools, like Redis and other cool technologies to create amazing and powerful applications.
Full repo: https://github.com/ModusCreateOrg/react-native-autocomplete-with-graphql-cache
Braulio Barrera
Related Posts
-
Switching from REST to GraphQL
Experiencing poor API performance with REST? Migrating from REST to GraphQL can dramatically improve API…
-
React Navigation and Redux in React Native Applications
In React Native, the question of “how am I going to navigate from one screen…
-
React Navigation and Redux in React Native Applications
In React Native, the question of “how am I going to navigate from one screen…
-
React Native ListView with Section Headers
Introduction React Native is an exciting framework that enables the developer to easily build native…