Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Accelerate quality software

Optimizing state management with React Query

Explore how React Query transforms state management in React apps. Learn about its benefits, integration tips, and how it can simplify your development process.
Tommy Groshong
|
May 2, 2021
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

As a consultant who has worked with React since 2014, I have been fortunate to see and work with many dozens of production React codebases.

In that time, I’ve noticed many patterns of use (and pain) that crop up even between very different applications. I also regularly get asked about the React ecosystem more generally and what things I’m excited for (or dreading).

In recent months, React Query (RQ) has been at the top of that list of things I’m excited about.

The case for React Query

Why am I excited about React Query? Because it fits into this sweet spot of the ecosystem:

  • It is focused on a particular, difficult problem that it handles very well.
  • It is un-opinionated about the rest of your stack.
  • It is compatible with the future of concurrent mode and suspense.
  • It reduces the amount of “state management” your app needs.

This last point is something I’ll talk more about, because I feel it is under appreciated about React Query, and really is one of its best superpowers. I feel confident in predicting that most early-stage, production, React applications adopting RQ will find they need no additional state management solution at all for a good long while.

In my experience, the most common reason leading to introducing a 3rd party state management library to a React app is to handle remote data: fetching, mutating, and sharing between components. I’ve seen this decision pattern repeated time and again in organizations and apps of all different sizes. You will rarely find a production React application that does not need to make network requests for data, and React includes no high-level abstractions for doing it.

When a developer is given an approach or tool specialized to handle remote data, many apps have only a trivial amount of application-wide state to manage. In many cases, especially early on in a project, that surface area of state management is small enough you could even do it with a one-off local state and context solutions using React’s built-in state management tools. Remember, React itself is also a state management library. (See Application State Management with React by Kent C. Dodds.)

If you haven’t seen React Query before, it looks like this:

import {
  useQuery,
  useMutation,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
} from 'react-query'

// Two Async functions that perform API actions with fetch, axios, or other
import { getTodos, postTodo } from '../my-api'

// Create a client
const queryClient = new QueryClient()

function App() {
  return (
    // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  )
}

function Todos() {
  // Queries
  const query = useQuery('todos', getTodos)

  // Access the client for use in "onSuccess" mutation callback
  const queryClient = useQueryClient()

  // Mutations
  const mutation = useMutation(postTodo, {
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries('todos')
    },
  })

  return (
    <div>
      <ul>
        {query.data.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>

      <button
        onClick={() => {
          mutation.mutate({
            id: Date.now(),
            title: 'Do Laundry',
          })
        }}
      >
        Add Todo
      </button>
    </div>
  )
}

render(<App />, document.getElementById('root'))

(Example adapted from React Query documentation.)

This is a good single-page example, but it downplays very natural improvements in your code that are possible.

We can extract these React Query hook calls to our own custom hooks and tighten up the calling code:

// ... same earlier code

function useTodos() {
  return useQuery('todos', getTodos)
}


function useTodoCreate() {
  const queryClient = useQueryClient()

  const {mutate} = useMutation(postTodo, {
    onSuccess: () => {
      queryClient.invalidateQueries('todos')
    },
  })

  return mutate
}


function Todos() {
  const todosResult = useTodos()
  const createTodo = useTodoCreate()

  return (
    <div>
      <ul>
        {todosResult.data.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>

      <button
        onClick={() => {
          createTodo({
            id: Date.now(),
            title: 'Do Laundry',
          })
        }}
      >
        Add Todo
      </button>
    </div>
  )
}

Sometimes an app has tricky, UI/UX behavior that requires complex managing of mutable state. Those cases certainly warrant reaching for tools like Redux (preferably Redux Toolkit which is the best way to Redux), XState, MobX, or Recoil. But even in those cases, it still makes sense to also be using React Query because it reduces the surface area that must be handled by your state management solution.

My current opinion is that any data that is conceptually “owned” by a remote source is probably a poor fit for generic state management containers like Redux, MobX, or XState. These libraries are best used for managing application-wide view state, more than your application state generally (regardless of what their marketing material may say). That’s not to say you can’t use them for extra-view things, but rather that the amount of hassle is directly proportional to the amount of data you put them in charge of.

Implementation on a personal project

Take the example of a personal project I’ve been working on occasionally for about a year. This project was using MobX State Tree (MST) for its state management. MST is a great library and I really enjoyed using it, but once I made the switch to React Query, I found that MST wasn’t doing much heavy lifting. Most of the code was doing transforms between RQ and MST, and a smaller amount was doing core domain logic. I set to work extracting those core domain bits into functions outside of my MobX code and realized … I didn’t really need MobX anymore. Apart from a couple site-wide modals, all of my application state was being managed by RQ, and those modals were easy to move to a one-off Context + useReducer solution (a la that Kent Dodds post).

This pattern has repeated with other codebases. Migrating to React Query resulted in atrophied state management code ripe for removal. It also pushed my coding practice toward a “functional core” design where my domain logic was entirely pure functions that operated on data primarily owned by React Query. This design keeps your core logic very easy to test.

Next, on that same project, I decided to migrate off of GraphQL and to a REST-ful API (don’t ask, long story, maybe another blog post). It was around 50 distinct Query and Mutation types: not massive, but not trivial either. One key to React Query’s success is that its API allows you to hide away that networking layer behind the same abstraction. Is an API implemented with REST or GraphQL? Both via different endpoints? The consuming view layer (React) doesn’t need to care about any of that.

This project first made data requests with a simple GraphQL Client package (graphql-request), and was going to migrate to a simple Fetch interface. Once the migration was finished, I had not changed a single React component. I won’t lie and say the migration was painless (mostly because I was unrolling a lot of handwritten stuff), but it definitely wasn’t the root canal level hardship I was expecting at the outset.

A huge factor of that low-pain experience was how contained the change was. All the updates lived at the same layer of my application and were scoped to a very small number of files. In fact, I did almost all the work in a single file until I got it working and later extracted it for a more pleasing file structure.

The GraphQL version of the app was something like this:

import { GraphQLClient } from "graphql-request";

const gqlClient = new GraphQLClient('/api/graphql')

async function getUserDetails(userId) {
  const { users_by_pk: data } = await gqlClient.request(
    /* GraphQL */ `
      query QueryUserDetails($userId: Int!) {
        users_by_pk(id: $userId) {
          id
          first_name
          last_name
          email
        }
      }
    `,
    {
      userId: userId,
    }
  );

  return transformSnakeCase(data);
}

export function useUserDetails() {
  return useQuery("user", getUserDetails);
}

Then after the change to a REST-ful backend, it was something like this:‍

async function getUserDetails(userId) {
  const response = await fetch('/api/user');
  const data = await response.json()
  return transformSnakeCase(data);
}

// ... same custom hook

The specifics of the implementation differences of getUserDetails() isn’t what I care about here. Both versions were doing essentially the same work, and I left out the error handling and authentication stuff because that was almost identical between them. The important note is that everything above stayed the same. My custom hook didn’t need to change. My React components didn’t need to change. My tests didn’t even change.

Conclusion

For most new React projects, I recommend the first library installed after react and react-dom to be react-query. For a simple application, it’s very straightforward, but as time passes and commits accumulate, React Query has been the best remote data fetching solution at growing with your codebase. It has saved my bacon more times than once, as I needed to regain control of a complexity exploding engagement. And it has pushed me down better paths of design with clearer layer distinctions and interfaces between them.

If you haven’t tried it yet, give it a shot.

Related Insights

🔗
Model View Controller pattern in React: A deep dive
🔗
The nine best recommendations in the new React docs

Explore our insights

See all insights
Developers
Developers
Developers
You’re holding it wrong! The double loop model for agentic coding

Joé Dupuis has noticed an influx of videos and blog posts about the "correct" way of working with AI agents. Joé thinks most of it is bad advice, and has a better approach he wants to show you.

by
Joé Dupuis
Leadership
Leadership
Leadership
Don't play it safe: Improve your continuous discovery process to reduce risk

We often front-load discovery to feel confident before building—but that’s not real agility. This post explores how continuous learning reduces risk better than perfect plans ever could.

by
Doc Norton
Leadership
Leadership
Leadership
How an early-stage startup engineering team improved the bottom line fast

A fast-growing startup was burning cash faster than it could scale. Here’s how smart engineering decisions helped them improve the bottom line.

by
Jonathon Baugh
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.