Skip to content

State Management with Apollo Client (Reactive Variables)

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Introduction

State management is one of the most common problems for frontend developers and mobile developers. Most of the new frameworks come with a built-in state management system like Vuex and Pinia for Vue, and Svelte Store for Svelte. But because of the lack of state management in the ecosystem of React (Also because React is a library, not a framework), developers have to use variance state management systems like Redux and XState. But the problem is that the state management system is not easy to use, and they usually add more code to the final build bundle. Most of projects nowadays use GraphQL, and the most popular tool for GraphQL is Apollo. But did you know that you can use Apollo itself as a state management library for your project instead of adding more code to the final build bundle? That's what we are going to do in this tutorial.

What is GraphQL?

GraphQL is a query language for APIs, and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

Apollo Client

Apollo Client is a client for the Apollo GraphQL server. It is a library that allows you to easily integrate GraphQL queries and mutations into your React application. It is a state management system for your React application, and it has a lot of state management features behind the senses that you can use to manage your application state as an alternative to Redux and XState.

Reactive Variables

Reactive variables are new features that came with Apollo Client 3. From the documentation:

"Reactive variables are a useful mechanism for representing local state outside of the Apollo Client cache."

And their power comes from the fact that they are separated from the cache and you can use them to store any kind of data type or structure.

Create your first Reactive variable in Apollo

Create a reactive variable with the makeVar method, like so:

import { makeVar } from '@apollo/client';

const isDarkModeVar = makeVar(false);

and then you can use isDarkModeVar() like so:

console.log(isDarkModeVar());
// Output: false

isDarkModeVar(true);

console.log(isDarkModeVar());
// Output: true

Also you can use useReactiveVar hook to get the value of the variable inside React components:

import { useReactiveVar } from '@apollo/client';
import { isDarkModeVar } from './variables/isDarkMode.js';

export function HomePage() {
  const isDarkMode = useReactiveVar(isDarkModeVar);

  return (
    <div>
      <h1>
        {isDarkMode ? 'Dark Mode' : 'Light Mode'}
      </h1>
    </div>
  );
}

Reactive components in the Queries

For more advanced use, you can use the reactive variables in the Apollo client in the GraphQL queries as well, and update it automatically, for example:

Let’s say you want to fetch a list of products, and want to save them.

export const GET_PRODUCT_ITEMS = gql`
  query GetProductItems {
    productItems @client
  }
`;

And now we should initialize a reactive variable productItemsVar for example:

import { makeVar } from '@apollo/client';

export const productItemsVar = makeVar([]);

In cache.js let’s do this:

import { InMemoryCache } from '@apollo/client';
import { productItemsVar } from './variables/productItemsVar.js';

export const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        productItems: {
          read() {
            return productItemsVar();
          }
        }
      }
    }
  }
});

And now we can use productItemsVar instead of querying it from the APIs each time.

Wrapping Up

Apollo Reactive variables are great features in Apollo 3+, and you can save a lot of work by using them instead of Redux and XState, especially if you are already using Apollo client and GraphQL in your project. Also, its power comes from its simplicity. Just create a global state variable by one line of code.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Improving INP in React and Next.js cover image

Improving INP in React and Next.js

Improving INP in React and Next.js In one of my previous articles, I've explained what INP is, how it works, and how it may affect your website. I also promised you to follow up with more concrete advice on how to improve your INP in your favorite framework. This is the follow-up article, where I'll focus on how to improve your INP score in React and Next.js. How to prepare for INP in React and Next.js? The first thing to do is to ensure you're using the latest version of React. The React team has been working on making React more INP-friendly and has already made some improvements in the latest versions. To enhance your INP score, consider fully taking advantage of new features introduced in React 18, such as Concurrent Rendering, Automatic Batching, and Selective Hydration. However, there are also some general areas to focus on, such as SSR and SSG in Next.js, Web Workers, or optimizing your hooks and state management. Concurrent Rendering The Concurrent Mode in React uses an algorithm that breaks rendering down into so-called "fiber nodes" and schedules the renders based on their expiration and priority. This effectively allows the user to interact with the page while the rendering is still in progress. In previous React versions, all updates, such as setState calls were treated as "urgent" and once the re-render had started, there was no way to interrupt it. Concurrent Mode changes this by being able to prioritize the updates and interrupt a non-blocking state update started with startTransition. For a simple explanation of concurrency in React, you can check out Dan Abramov's explanation. As part of the Concurrent Mode, React introduced several lifecycle methods that allow you to prioritize the rendering of certain parts of your UI, such as: - useTransition hook that allows you to update the state without blocking the UI, - useDeferredValue hook that allows you to defer the rendering of certain parts of your UI, - startTransition API that, similarly to the useTransition hook lets you mark a state update as non-blocking. It lacks, however, an indication of whether it's still pending. Automatic Batching Introduced in React 18, Automatic Batching reduces the number of re-renders that happen on state changes even when they happen outside of event handlers, e.g. in a setTimeout or Promise callback. This feature comes out of the box and you don't have to do anything to enable it, and it makes a great argument for upgrading to React 18. Selective Hydration Selective Hydration allows you to take hydration off the main thread by wrapping your components in a Suspense boundary. This way, components can become interactive faster as the browser can do other work on the main thread while the hydration is happening. To fully take advantage of selective hydration, consider the following: - Prioritizing Above-the-Fold Content: Use Suspense boundaries strategically around any parts of your application that may take the server longer to deliver to ensure they don’t block critical content from becoming interactive as soon as possible. - Hydration on Interaction: Implementing hydration upon user interaction for non-critical components can drastically reduce the main thread's workload, enhancing INP. Vercel even has a small case study showing how using selective hydration improved the performance of a Next.js site. Server-Side Rendering (SSR) and Static Site Generation (SSG) in Next.js Not everything has to run client-side. Next.js excels in SSR and SSG capabilities, which can significantly impact INP by delivering content to users faster. Optimizing SSR with techniques like incremental static regeneration (ISR) or leveraging SSG for static pages ensures that users can interact with content faster, improving the perceived performance. Workers Offloading heavy computations to Web Workers can free up the main thread, enhancing the responsiveness of React and Next.js applications. This strategy is especially useful when dealing with third-party scripts. Offloading such scripts in Next.js can be easily done by specifying the "worker" strategy on your Script component. Be aware that this feature is not yet stable and does not work with the app directory, though. If you want to take things one step further, you could use Partytown, which helps you offload any resource-intensive scripts to Web Workers. It comes with a React component that you can use to wrap your third-party scripts and offload them to a Web Worker, and it's compatible with Next.js as well. Hooks and State Management State management in React applications can easily get out of hand, leading to unnecessary re-renders and effectively an increased INP. Sometimes, using a state management library like Redux or MobX can help you consolidate your state and reduce the number of re-renders. However, they are not silver bullets and can also introduce performance issues if not used properly. If you are dealing with a lot of re-renders due to prop changes, make sure you are leveraging memoization. As of now, you may need to work with useMemo and useCallback hooks to memoize your values and functions, respectively. The upcoming React 19’s Forget Compiler, however, will apparently memoize everything under the hood, making these hooks obsolete. Using memoization properly can help you reduce the number of re-renders and improve your INP. To investigate your hook dependencies and re-renders, you can leverage React Developer Tools or use this handy helper hook I found on the internet to trace your re-renders: ` Conclusion Improving INP in React and Next.js is not easy and can require much investigation and fine-tuning. Still, it's worth doing to avoid being penalized by Google in its search results and provide a better experience for your users. Adopting React 18's new features, leveraging SSR and SSG in Next.js, utilizing Web Workers, and optimizing hooks and state management can significantly boost your INP score and deliver a faster application to your users. Remember, INP is just one among many performance metrics emphasizing the need for a comprehensive approach to performance optimization...

How to test React custom hooks and components with Vitest cover image

How to test React custom hooks and components with Vitest

Introduction In this guide, we'll navigate through the process of testing React hooks and components using Vitest—a powerful JavaScript unit testing framework. Discover how Vitest simplifies testing setups, providing an optimal solution for both Vite-powered projects and beyond. Vitest is a javascript unit testing framework that aims to position itself as the Test Runner of choice for Vite projects and as a solid alternative even for projects not using Vite. Vitest was built primarily for Vite-powered projects, to help reduce the complexity of setting up testing with other testing frameworks like Jest. Vitest uses the same configuration of your App (through vite.config.js), sharing a common transformation pipeline during dev, build, and test time. Prerequisites This article assumes a solid understanding of React and frontend unit testing. Familiarity with tools like React Testing Library and JSDOM will enhance your grasp of the testing process with Vitest. Installation and configuration Let’s see how we can use Vitest for testing React custom hooks and components. But first, we will need to create a new project with Vite! If you already have an existing project, you can skip this step. ` Follow the prompts to create a new React project successfully. For testing, we need the following dependencies installed: Vitest as the unit testing framework JSDOM as the DOM environment for running our tests React Testing Library as the React testing utilities. To do so, we run the following command: ` Once we have those packages installed, we need to configure the vite.config.js file to run tests. By default, some of the extra configs we need to set up Vitest are not available in the Vite config types, so we will need the vite.config.ts file to reference Vitest types by adding /// reference types=”vitest” /> at the top of the file. Add the following code to the vite.config.ts ` We set globals to true because, by default, Vitest does not provide global APIs for explicitness. So with this set to true, we can use keywords like describe, test and it without needing to import them. To get TypeScript working with the global APIs, add vitest/globals to the types field in your tsconfig.json. ` The environment property tells Vitest which environment to run the test. We are using jsdom as the environment. The root property tells Vitest the root folder from where it should start looking for test files. We should add a script for running the test in package.json ` With all that configured, we can now start writing unit tests for customs hooks and React components. Writing test for custom hooks Let’s write a test for a simple useCounter hook that takes an initial value and returns the value, an increment function and a decrement function. ` We can write a test to check the default return values of the hook for value as below: ` To test if the hook works when we increment the value, we can use the act() method from @testing-library/react to simulate the increment function, as shown in the below test case: ` Kindly Note that you can't destructure the reactive properties of the result.current instance, or they will lose their reactivity. Testing hooks with asynchronous logic Now let’s test a more complex logic that contains asynchronous logic. Let’s write a useProducts hook that fetches data from an external api and return that value ` Now, let’s see what the test looks like: ` In the above example, we had to spy on the global fetch API, so that we can mock its return value. We wrapped that inside a beforeAll so that this runs before any test in this file. Then we added an afterAll method and called the mockRestore() to run after all test cases have been completed and return all mock implementations to their original function. We can also use the mockClear() method to clear all the mock's information, such as the number of calls and the mock's results. This method is handy when mocking the same function with different return values for different tests. We usually use mockClear() in beforeEach() or afterEach() methods to ensure our test is isolated completely. Then in our test case, we used a waitFor(), to wait for the return value to be resolved. Writing test for components Like Jest, Vitest provides assertion methods (matchers) to use with the expect methods for asserting values, but to test DOM elements easily, we will need to make use of custom matchers such as toBeInTheDocument() or toHaveTextContent(). Luckily the Vitest API is mostly compatible with the Jest API, making it possible to reuse many tools originally built for Jest. For such methods, we can install the @testing-library/jest-dom package and extend the expect method from Vitest to include the assertion methods in matchers from this package. ` After installing the jest-dom testing library package, create a file named vitest-setup.ts on the root of the project and import the following into the project to extend js-dom custom matchers: ` Since we are using typescript, we also need to include our setup file in our tsconfig.json: ` In vite.config.ts, we need to add the vitest-setup.ts file to the test.setupFiles field: ` Now let’s test the Products.tsx component: ` We start by spying and mocking the useProducts hook with vi.spyOn() method from Vitest: ` Now, we render the Products component using the render method from @testing-library/react and assert that the component renders the list of products as expected and also the product has the title as follows: ` In the above code, we use the render method from @testing-library/react to render the component and this returns some useful methods we can use to extract information from the component like getByTestId and getByText. The getByTestId method will retrieve the element whose data-testid attribute value equals product-list, and we can then assert its children to equal the length of our mocked items array. Using data-testid attribute values is a good practice for identifying a DOM element for testing purposes and avoiding affecting the component's implementation in production and tests. We also used the getByText method to find a text in the rendered component. We were able to call the toBeInTheDocument() because we extended the matchers to work with Vitest earlier. Here is what the full test looks like: ` Conclusion In this article, we delved into the world of testing React hooks and components using Vitest, a versatile JavaScript unit testing framework. We walked through the installation and configuration process, ensuring compatibility with React, JSDOM, and React Testing Library. The comprehensive guide covered writing tests for custom hooks, including handling asynchronous logic, and testing React components, leveraging custom matchers for DOM assertions. By adopting Vitest, developers can streamline the testing process for their React applications, whether powered by Vite or not. The framework's seamless integration with Vite projects simplifies the setup, reducing the complexities associated with other testing tools like Jest....

Redux Toolkit 2.0 release cover image

Redux Toolkit 2.0 release

Intro Redux Toolkit On December 4, 2023, Mark Erikson announced that Redux Toolkit 2.0 has been released! This change is considered a major version of the library and comes with breaking changes because, as the core team declared, it has been 4+ years since they released Redux tool kit, and they needed to clean up the code and standardize the way Redux apps are written. One of these changes is you no longer need to import the core redux library, RTK is a wrapper of it. In this article, we are going to focus on the main breaking changes and new features in RTK 2.0 Some general notes on the change * Standalone getDefaultMiddleware and getType removed * The package definitions now include an exports field to specify which artifacts to load, with a modern ESM build * UMD has been dropped * Typescript rewrite createSlice and createReducer One of the major changes is object syntax for createSlice.extraReducers and createReducer removed. In RTK 2.0, Redux team has opted to remove the 'object' form for createReducer and createSlice.extraReducers, as the 'builder callback' form offers greater flexibility and is more TypeScript-friendly, making it the preferred choice for both APIs So an example like this: ` Should be like this: ` Middlewares Now the configureStore.middleware must be a callback. That’s because before to add a middleware to the Redux store, you had to add it to the array of configureStore.middleware, but this is turning off the default middleware. Now it’s a callback function that should return an array with middlewares, the default middleware is being passed in the parameters of the callback function so the developer can include it or not in the middleware array like this: ` Enhancers Store enhancers are a formal mechanism for adding capabilities to Redux itself like adding some additional capabilities to the store. It could change how reducers process data, or how dispatch works. Most people will never need to write one. But similar to the middlewares, enhancers also must be a callback function that receives getDefaultEnhancers and should return enhancers back. ` autoBatchEnhancer In version 1.9.0, the Redux team introduced a new autoBatchEnhancer that, when multiple "low-priority" actions are dispatched consecutively. This enhancement aims to optimize performance by reducing the impact of UI updates, typically the most resource-intensive part of the update process. While RTK Query automatically marks most of its internal actions as "low-pri" users need to add the autoBatchEnhancer to the store for this feature to take effect. To simplify this process, configureStore has been updated to include the autoBatchEnhancer in the store setup by default, allowing users to benefit from improved performance without manual store configuration adjustments. AnyAction and UnknownAction The Redux TS types have historically exported an AnyAction type that lacks precise type safety, allowing for loose typology like console.log(action.whatever). However, to promote safer typing practices, they've introduced UnknownAction, which treats all fields beyond action.type as unknown, nudging users towards creating type guards with assertions for better type safety when accessing action objects. UnknownAction is now the default action object type in Redux, while AnyAction remains available but deprecated. RTK Query behavior They've addressed issues with RTK Query, such as problems with dispatch(endpoint.initiate(arg, {subscription: false})) and incorrect resolution timing for triggered lazy queries, by reworking RTKQ to consistently track cache entries. Additionally, they've introduced internal logic to delay tag invalidation for consecutive mutations, controlled by the new invalidationBehavior flag on createApi, with the default being 'delayed'. In RTK 1.9, the subscription status has been optimized syncing to the Redux state for performance, primarily for display in the Redux DevTools "RTK Query" panel. Conclusion This version is a major version of Redux Toolkit and it has a lot of breaking changes but what is important its source code has been cleaned and rewritten in TS will improve the developer experience for sure and on the final user experience as well, We discussed here only the headlines and the most used features but of course you can find all of the changes on The official Redux Github Releases page...

The simplicity of deploying an MCP server on Vercel cover image

The simplicity of deploying an MCP server on Vercel

The current Model Context Protocol (MCP) spec is shifting developers toward lightweight, stateless servers that serve as tool providers for LLM agents. These MCP servers communicate over HTTP, with OAuth handled clientside. Vercel’s infrastructure makes it easy to iterate quickly and ship agentic AI tools without overhead. Example of Lightweight MCP Server Design At This Dot Labs, we built an MCP server that leverages the DocuSign Navigator API. The tools, like `get_agreements`, make a request to the DocuSign API to fetch data and then respond in an LLM-friendly way. ` Before the MCP can request anything, it needs to guide the client on how to kick off OAuth. This involves providing some MCP spec metadata API endpoints that include necessary information about where to obtain authorization tokens and what resources it can access. By understanding these details, the client can seamlessly initiate the OAuth process, ensuring secure and efficient data access. The Oauth flow begins when the user's LLM client makes a request without a valid auth token. In this case they’ll get a 401 response from our server with a WWW-Authenticate header, and then the client will leverage the metadata we exposed to discover the authorization server. Next, the OAuth flow kicks off directly with Docusign as directed by the metadata. Once the client has the token, it passes it in the Authorization header for tool requests to the API. ` This minimal set of API routes enables me to fetch Docusign Navigator data using natural language in my agent chat interface. Deployment Options I deployed this MCP server two different ways: as a Fastify backend and then by Vercel functions. Seeing how simple my Fastify MCP server was, and not really having a plan for deployment yet, I was eager to rewrite it for Vercel. The case for Vercel: * My own familiarity with Next.js API deployment * Fit for architecture * The extremely simple deployment process * Deploy previews (the eternal Vercel customer conversion feature, IMO) Previews of unfamiliar territory Did you know that the MCP spec doesn’t “just work” for use as ChatGPT tooling? Neither did I, and I had to experiment to prove out requirements that I was unfamiliar with. Part of moving fast for me was just deploying Vercel previews right out of the CLI so I could test my API as a Connector in ChatGPT. This was a great workflow for me, and invaluable for the team in code review. Stuff I’m Not Worried About Vercel’s mcp-handler package made setup effortless by abstracting away some of the complexity of implementing the MCP server. It gives you a drop-in way to define tools, setup https-streaming, and handle Oauth. By building on Vercel’s ecosystem, I can focus entirely on shipping my product without worrying about deployment, scaling, or server management. Everything just works. ` A Brief Case for MCP on Next.js Building an API without Next.js on Vercel is straightforward. Though, I’d be happy deploying this as a Next.js app, with the frontend features serving as the documentation, or the tools being a part of your website's agentic capabilities. Overall, this lowers the barrier to building any MCP you want for yourself, and I think that’s cool. Conclusion I'll avoid quoting Vercel documentation in this post. AI tooling is a critical component of this natural language UI, and we just want to ship. I declare Vercel is excellent for stateless MCP servers served over http....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co