What is GraphQL ?

When creating applications, REST APIs are commonly used for communication between the client and the server. With REST, each endpoint returns predefined data, and because of this, it happens that more data than needed is received, or that more requests to the server must be made in order to get all the necessary data. The main idea of GraphQL is based on these shortcomings.

GraphQL represents a modern language for queries and data manipulation that allows clients to precisely define what information they want to receive from the server. This means that it is not necessary to send different requests in order to collect data from different resources, but everything that is needed can be obtained within one query, where all requests are sent via the POST method.

An important characteristic of GraphQL is its efficiency. Since the client receives only the requested data, the amount of transferred data is reduced, resulting in faster responses and lower network load. This is especially important for applications with limited resources. The GraphQL API is easier to maintain and expand because changes on the server do not have to imply a change in the way the client receives data.

One of the most popular libraries for working with GraphQL on the client side is Apollo Client. It enables efficient communication between the frontend and the GraphQL server by automatically managing queries, mutations, caching, and updating the user interface. In React applications, it is used through simple hooks such as useQuery, useMutation, and useSubscription. A development tool called Apollo DevTools is also offered, making it easier to monitor all GraphQL queries, responses, as well as the state of the cache in real time, which facilitates debugging and optimization.

Due to the fact that GraphQL uses the same method (POST) and the same URL endpoint for all queries, the browser is not able to recognize and differentiate individual requests. Since it cannot distinguish between them, it cannot cache them properly either, because the browser cache depends on the unique URL and request method. To overcome this issue, Apollo Client provides a caching system at the data level, recognizes different graphQL queries, and efficiently manages their results.

Caching with Apollo Client

One of the biggest advantages of the Apollo Client is its caching system, which speeds up the application and reduces server load. When data is downloaded from the GraphQL server, Apollo stores it in memory using InMemoryCache, so the query does not have to be sent to the server again. 

Data consistency is a very important advantage of caching with Apollo Client. Any change to the data is automatically applied to all parts of the application that use the same entities, ensuring the user always sees updated, accurate information. This reduces the risk of showing outdated data and makes it easier to maintain the application logic, especially when the same data is displayed on different parts of the interface. Programmers are enabled to precisely define caching rules, choose strategies for how the data will be retrieved, and, if necessary, manually update the data, which facilitates the management of complex data structures and the adaptation of the application’s behavior to different scenarios. In this way, greater flexibility and control are achieved. 

By default, when the user sends a request, Apollo Client first checks if the data is already in the cache. If the data is found in the cache, it is used immediately, so the server does not need to be called. If the data is unavailable, a request is sent to the server, and the data is then stored in the cache. This ensures that the next time the data is needed, it will already be in the cache. It is important to note that this behavior applies to the default cache-first fetch policy. If a different fetch policy is configured, Apollo Client may handle requests differently, as described in the following sections.

Apollo itself tries to recognize the types and IDs of the objects in order to properly organize them in the cache, but the rules of behavior can also be defined through type policies.

To illustrate how to configure the cache, an example scenario involving Employees and Projects is used. The relation between them is such that multiple Employees work on the same Project, but each Employee works on only one project. To ensure data consistency and integrity, Apollo Client is configured with InMemoryCache using typePolicies:

import { ApolloClient, InMemoryCache } from `@apollo/client`;
const client = new ApolloClient ({
uri: 'http://localhost:4000/graphql',


import { ApolloClient, InMemoryCache } from `@apollo/client`;
const client = new ApolloClient ({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache({
typePolicies: {
Employee: {
keyFields: ['id'], // ID is a unique key for every employee
},
Project: {
keyFields: ['id'], // ID is a unique key for every project
fields: {
employees: {
// If employees for the project are returned, combine them with existing ones
merge (existing = [], incoming) {
return [...existing, ...incoming]
}
}
}
}
}
})
});

In the configuration above, two key properties are used: keyFields and fields. keyFields tells Apollo how to identify each entity in the cache. In the example above, both Employee and Project use keyFields: [‘id’], which means Apollo uses the id field to recognize whether two objects represent the same entity. Even if the same Project is fetched multiple times across different queries, Apollo treats them as the same entity as long as they share the same ID. However, this can lead to a problem. Imagine the same Project is fetched twice, but each response contains a different subset of Employees,  for example, because different filters were applied. Since Apollo sees both responses as the same Project, the second response would overwrite the first, and some Employees might be lost.

To overcome this problem, fields is used to define rules for individual entity fields. In the example above, a merge function is defined for the employees field of the Project entity. This function takes the already cached employees (existing) and the newly arrived employees (incoming), and combines them. This way, instead of replacing the previous list, all Employees are preserved regardless of which query returned them.

It is important to note that keyFields doesn’t have to be ID; any unique combination of fields that identifies an entity can be used. For example, if employees did not have a numeric ID but were uniquely identified by their email address, keyFields: [‘email’] could be defined to ensure Apollo correctly identifies each employee in the cache.

With the above, Apollo knows how to identify each employee and project, how to combine or replace old and new data in the cache, and how to automatically refresh the parts of the UI that use this data.

Challenges and Limitations of Using Apollo Client for Caching

Although using the Apollo Client brings numerous advantages, it is important to emphasize that there are also certain disadvantages that may appear when working with caching. One of the main challenges is that the cache is not always fully aligned with the server state, so the application may display outdated data. Additionally, in certain situations, the developer must manually manage the cache, especially after mutations, so that the user interface displays the most recent information. There are also problems that can occur with complex data hierarchies, where inadequately defined typePolicies can lead to cache inconsistencies. Finally, using real-time data via a GraphQL subscription can create challenges in maintaining cache accuracy, as new data is constantly updated. Below, several of these problems are explained in more detail, and how they can be solved in practice is shown.

Stale Data

The data on the server is updated, but the old version stored in the cache is still displayed. One of the main reasons for stale data is that Apollo Client uses the “cache-first” fetch policy by default. This means that when a query is sent, Apollo will first check if the result exists in the cache, and if so, it will use that cached result without sending a new request to the server.

In order to solve this problem, the fetch policy can be changed, which controls the behavior of Apollo Client with each query. If the “cache-and-network” fetch policy is used, the server is told to return the data from the cache to display them on the UI immediately, but in parallel to send a request to the server to check if there is new data. This can be done in the following way:

const { data, loading } = useQuery(GET_EMPLOYEES, { 
fetchPolicy: "cache-and-network"
});

Apart from cache-and-network, Apollo Client offers several other fetch policies:

  • cache-first (default) – Returns data from the cache if available. A network request is only sent if no cached data exists.
  • cache-only – Returns data exclusively from the cache. No network request is ever made, and an error is thrown if the data is not cached.
  • network-only – Always sends a request to the server, ignoring the cache. However, the response is still stored in the cache for future use.
  • no-cache – Always sends a request to the server and does not store the response in the cache at all.
  • standby – Similar to cache-first, returns data from the cache if available and only sends a network request if no cached data exists. However, unlike cache-first, it does not automatically send a new request to the server when related queries update the cache. It must be manually triggered to refetch.

The choice of fetch policy depends on the specific requirements of the application. For data that changes frequently, cache-and-network or network-only may be more appropriate, while for static data that rarely changes, cache-first provides the best performance.

Cache Not Updated After Mutation

If an attempt is made to add a new employee, even though the new employee is successfully added, the employee list is not refreshed because Apollo uses the old cache. In such cases, there are two possible solutions:

  • Tell Apollo to refetch the query that was originally used to cache the data.
  • Manually update the cache with data from the mutation response, to avoid a new request to the server.

To tell Apollo to automatically refetch data, refetchQueries is used:

useMutation(ADD_EMPLOYEE, 
{ refetchQueries: [{ query: GET_EMPLOYEES }]
});


To manually update the cache, cache.modify() or cache.writeQuery() can be used:

const [addEmployee] = useMutation(ADD_EMPLOYEE, {   
update(cache, { data: {addEmployee} }) {
const data = cache.readQuery({ query: GET_EMPLOYEES });
cache.writeQuery({
query: GET_EMPLOYEES,
data: { employees: [...data.employees, addEmployee] }
});
}
});

It is important to note that in certain situations, the Apollo Client can independently update the cache after a mutation, without the need for additional programmer intervention. The automatic update occurs first when an entity is modified, and the mutation in the response returns an object of the same type with the same unique identifier that already exists in the cache. In such a situation, Apollo clearly recognizes that it is the same entity and replaces its old values with new ones.

Another case of automatic updating occurs when a mutation updates multiple entities at the same time, but the same types and identifiers(keyFields) that Apollo already uses to identify objects are again present in the response. Apollo can then merge new data with existing cache entries without additional configuration and ensure that the user interface displays the latest state of the data.

Real-Time Data – Subscriptions

If subscriptions are used so that new employees constantly arrive from the server in real time, it is necessary to update the cache every time a new event arrives. In this case, employeeAdded is specified. Within the subscription resolver, new data can be manually added to the cache in the following way:

const { data } = useSubscription(EMPLOYEE_ADDED, {  
onData: ({ client, data }) => {
const newEmployee = data.data.employeeAdded;
const existing = client.readQuery({ query: GET_EMPLOYEES });
client.writeQuery({
query: GET_EMPLOYEES,
data: { employees: [...existing.employees, newEmployee] }
});
}
});

Conclusion

GraphQL represents a big step forward when looking at communication in modern applications. Unlike REST, where multiple endpoints exist and more data is returned, GraphQL enables full control over data. This precise control not only improves efficiency but also reduces unnecessary data transfer and simplifies client-side processing.

On the client side, tools like Apollo Client make working with GraphQL significantly easier. Its powerful caching system, based on InMemoryCache and customizable typePolicies, allows applications to retrieve and manage data efficiently. By caching previously fetched data, Apollo minimizes redundant queries, reduces server load, and ensures users experience fast, responsive interfaces. Additionally, Apollo provides mechanisms to maintain data consistency, control caching behavior, and handle updates after mutations, which are crucial for keeping the user interface synchronized with server state.

However, while Apollo offers many advantages, it also introduces certain challenges. Issues such as stale data, manual cache updates after mutations, complexity with nested relationships, and handling real-time data updates require careful planning and proper configuration. By understanding these limitations and leveraging Apollo’s tools effectively, developers can harness the full potential of GraphQL while avoiding common pitfalls.

In conclusion, combining GraphQL with Apollo Client provides a flexible, efficient, and powerful solution for modern applications. Proper use of caching strategies enhances performance, reduces server load, and maintains a consistent and predictable state for users. At the same time, awareness of potential challenges ensures that applications remain robust and reliable even as complexity grows.

Leave a comment

Your email address will not be published. Required fields are marked *