Jotai Unites Client and Server State with a Powerful React Query Integration

The Evolution of State Management: Jotai’s Leap into Server-Side State

The React ecosystem is in a constant state of flux, with the community perpetually seeking simpler, more efficient ways to manage application state. For years, the conversation was dominated by giants like Redux, but a new wave of libraries has emerged, championing minimalism and developer experience. This is where the latest Jotai News becomes a game-changer. While libraries like Zustand News and Recoil News have simplified client-side state, a critical division has remained: managing local UI state versus managing server-side state, or “server cache.” Developers often reach for powerful tools like React Query News, Apollo Client News, or Urql News to handle data fetching, caching, and synchronization, leading to two separate state management paradigms within a single application.

Jotai, with its atomic, bottom-up approach, has always excelled at managing client state with minimal boilerplate. However, its latest evolution introduces a dedicated jotai/query bundle, a brilliant integration that wraps the battle-tested engine of React Query directly into Jotai’s atomic model. This isn’t just a minor update; it’s a significant stride towards a unified state management model where server state and client state can coexist and interact seamlessly as first-class atoms. This development has massive implications for developers using frameworks like Next.js News, Remix News, and Gatsby News, as it streamlines one of the most complex aspects of modern web development.

Section 1: Unifying State with `atomWithQuery`

At the heart of Jotai is the concept of an “atom”—a small, isolated piece of state. This granular approach prevents the massive re-renders often associated with monolithic state stores found in older patterns discussed in Redux News. The new jotai/query bundle extends this core philosophy to asynchronous server data by introducing specialized atoms that handle the entire data-fetching lifecycle.

The Core Primitive: `atomWithQuery`

The star of the show is the atomWithQuery utility. It allows you to define an atom that fetches data from an endpoint, but it does much more than a simple `fetch` call. Under the hood, it leverages React Query’s robust machinery for caching, re-fetching, and state management. This means you get features like background refetching, stale-while-revalidate, and query invalidation out of the box, all wrapped in the simple, declarative API of a Jotai atom.

Let’s look at a foundational example. Imagine we want to fetch a list of users from a public API.

import { atom } from 'jotai';
import { atomWithQuery } from 'jotai/query';

// Define a query key, just like in React Query
const usersQueryKey = ['users'];

// Create an atom that fetches the user data
export const usersAtom = atomWithQuery((get) => ({
  queryKey: usersQueryKey,
  queryFn: async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  },
}));

In this snippet, we define an atom called usersAtom. The function passed to atomWithQuery returns a configuration object that should look very familiar to anyone who has used React Query. The queryKey uniquely identifies this data, and the queryFn is the asynchronous function that performs the fetch. The beauty here is that usersAtom is now a standard Jotai atom. You can read from it, derive other atoms from it, and compose it with your client-side state seamlessly.

Consuming the Query Atom in a Component

Using this atom in a React component is as simple as using any other Jotai atom. You use the standard `useAtom` hook, which, when used with a query atom, will automatically trigger React Suspense for loading states.

React Query logo - React Query Logo PNG Vector (SVG) Free Download
React Query logo – React Query Logo PNG Vector (SVG) Free Download
import React, { Suspense } from 'react';
import { useAtom } from 'jotai';
import { usersAtom } from './atoms'; // Assuming atoms are in a separate file

const UserList = () => {
  const [usersData] = useAtom(usersAtom);

  return (
    <ul>
      {usersData.map((user) => (
        <li key={user.id}>{user.name} ({user.email})</li>
      ))}
    </ul>
  );
};

const App = () => (
  <div>
    <h1>User List</h1>
    <Suspense fallback={<div>Loading users...</div>}>
      <UserList />
    </Suspense>
  </div>
);

export default App;

This pattern is incredibly powerful. The `UserList` component is clean and declarative. It simply asks for the `usersData` and renders it. The complexity of managing the loading state is handled gracefully by React Suspense. This approach is not just for web apps built with tools like Vite News; it’s equally effective in the mobile world, making it exciting React Native News for developers using Expo News or bare React Native.

Section 2: Practical Implementation and Composition

The true power of Jotai’s atomic model shines when you start composing atoms. The `jotai/query` integration is designed with this composition in mind, allowing you to create dynamic, dependent queries that react to changes in other state atoms, whether they are local UI state or other server state.

Creating Dependent Queries

A common use case is fetching detailed data for an item selected from a list. With Jotai, you can represent the selected item’s ID as a simple atom and create a second query atom that depends on it. The `get` function provided to the atom’s factory is the key to this composition.

Let’s extend our previous example. We’ll create an atom to hold the currently selected user ID and another `atomWithQuery` to fetch that specific user’s details.

import { atom } from 'jotai';
import { atomWithQuery } from 'jotai/query';

// Atom to hold the ID of the selected user (client-side state)
export const selectedUserIdAtom = atom<number | null>(null);

// A derived query atom that depends on the selectedUserIdAtom
export const selectedUserAtom = atomWithQuery((get) => {
  const userId = get(selectedUserIdAtom);

  return {
    // The query key now includes the userId to ensure uniqueness
    queryKey: ['user', userId],
    // The query function uses the userId to fetch specific data
    queryFn: async () => {
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
      if (!response.ok) {
        throw new Error('Failed to fetch user details');
      }
      return response.json();
    },
    // This query will only run if a userId is selected
    enabled: userId !== null,
  };
});

Here, `selectedUserAtom` reads the value of `selectedUserIdAtom` using `get(selectedUserIdAtom)`. Whenever `selectedUserIdAtom` changes (e.g., when a user clicks on a list item), Jotai automatically re-evaluates `selectedUserAtom`. React Query’s engine then determines if a new fetch is needed based on the changed `queryKey`. The `enabled` flag is a direct pass-through to React Query’s options, preventing the query from running until a user ID is actually set. This elegant composition of client and server state is a massive win for developer productivity.

Section 3: Handling Mutations and Advanced Patterns

Reading data is only half the story. Real-world applications need to create, update, and delete data. The `jotai/query` bundle provides another utility, `atomWithMutation`, for exactly this purpose. This continues the trend of bringing familiar concepts from libraries like React Query News into the Jotai ecosystem.

Performing Data Mutations with `atomWithMutation`

`atomWithMutation` creates an atom that, when written to, triggers an asynchronous mutation function. The atom’s value then reflects the state of that mutation: `isLoading`, `isError`, `isSuccess`, and the resulting `data`.

Jotai logo - Jotai: Primitive and Flexible State Management for React | by ...
Jotai logo – Jotai: Primitive and Flexible State Management for React | by …

Let’s build a component for adding a new user. This pattern is essential when working with forms managed by libraries like React Hook Form News or Formik News.

import { useAtom } from 'jotai';
import { atomWithMutation } from 'jotai/query';
import { useQueryClient } from 'react-query'; // Still need this for invalidation

// Atom to handle the user creation mutation
const addUserMutationAtom = atomWithMutation((get) => ({
  mutationFn: async (newUser) => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users', {
      method: 'POST',
      body: JSON.stringify(newUser),
      headers: {
        'Content-type': 'application/json; charset=UTF-8',
      },
    });
    return response.json();
  },
}));

// A component to add a new user
const AddUserForm = () => {
  const queryClient = useQueryClient();
  const [mutationState, mutate] = useAtom(addUserMutationAtom);

  const handleSubmit = (event) => {
    event.preventDefault();
    const name = event.target.elements.name.value;
    const email = event.target.elements.email.value;

    mutate({ name, email }, {
      onSuccess: () => {
        // When the mutation is successful, invalidate the users list query
        // to trigger a refetch of the latest data.
        queryClient.invalidateQueries(['users']);
        console.log('User added successfully!');
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" type="text" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit" disabled={mutationState.isLoading}>
        {mutationState.isLoading ? 'Adding...' : 'Add User'}
      </button>
      {mutationState.isError && <div>Error adding user!</div>}
    </form>
  );
};

In this example, `useAtom(addUserMutationAtom)` returns a tuple. The first element, `mutationState`, is an object containing the status of the mutation. The second, `mutate`, is the function you call to trigger it. Notice that we still use `useQueryClient` from React Query to perform actions like query invalidation. This is a powerful escape hatch that allows you to tap into the full capabilities of the underlying engine, a crucial feature for complex applications built with frameworks like Blitz.js News or RedwoodJS News.

Section 4: Best Practices and Optimization

Integrating server state directly into your state manager is powerful, but it requires a thoughtful approach to ensure your application remains performant and maintainable. Here are some best practices and considerations.

When to Use Query Atoms vs. Standard Atoms

  • `atom`: Use standard atoms for pure client-side state. This includes UI state (e.g., `isSidebarOpenAtom`), form inputs, and any data that doesn’t need to be synchronized with a server.
  • `atomWithQuery`: Reserve this for data that originates from and is owned by the server. Think of it as a synchronized, cached copy of your backend data. User profiles, product lists, and API results are perfect candidates.
  • `atomWithMutation`: Use this for any action that intends to change data on the server.

Structuring Your Atoms

atomic state React diagram - State Management in React with Jōtai
atomic state React diagram – State Management in React with Jōtai

As your application grows, organizing your atoms becomes crucial. A common pattern is to co-locate atoms with the features that use them. For global atoms, like user authentication state, create a central `store` or `atoms` directory. This practice is especially important in large codebases, whether you’re using a monorepo or a standard project structure. For testing, this separation makes it easier to use tools like React Testing Library News or Jest News to mock specific atoms and test components in isolation.

Leveraging the QueryClient Provider

Since `jotai/query` is built on React Query, you must wrap your application with a `QueryClientProvider`, just as you would with a standard React Query setup. This provider is what holds the cache and manages the underlying logic.

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider as JotaiProvider } from 'jotai';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';

const queryClient = new QueryClient();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <JotaiProvider>
        <App />
      </JotaiProvider>
    </QueryClientProvider>
  </React.StrictMode>
);

This setup ensures that both Jotai and the underlying React Query engine have the context they need to function correctly. This is a foundational step for any project, from simple SPAs to complex applications leveraging tools like React Router News for navigation or Framer Motion News for animations.

Conclusion: A Unified Future for React State

The introduction of the `jotai/query` bundle is more than just a new feature; it represents a philosophical shift towards a more unified and simplified state management landscape. By seamlessly integrating the de-facto standard for server state, React Query, into its elegant atomic model, Jotai has eliminated a major source of complexity for developers. No longer do we need to mentally juggle two different state management systems with disparate APIs and concepts.

This update solidifies Jotai’s position as a top-tier state management solution for the modern React ecosystem. For developers building anything from a simple website with Gatsby News to a complex cross-platform mobile app with React Native News and UI libraries like Tamagui News or React Native Paper News, this integration offers a clear path to cleaner, more maintainable, and more powerful code. By embracing this unified model, you can spend less time wrestling with state synchronization and more time building exceptional user experiences.