A rocky beach in southern Mexico, with a tree covered hill overlooking it. The phot is taken from underneath a wooden pier.

Integrating Providers with React Testing Library

Jake Robins - Dev

I recently started learning and integrating Tanner Linsley's fantastic Tanstack Query package into a client's React project. Formerly known as React Query, this library provides some really smart hooks for managing database state in your project. For a long time I was a "roll your own" state management kind of guy, creating little hooks around each query and dropping it into React Context to make it accessible everywhere. I've written hooks similar to this many times:

const useWidgets = () => {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [widgets, setWidgets] = useState([]);

  useEffect(() => {
    fetchWidgets()
      .then((w) => {
        setWidgets(w)
      })
      .catch((err) => {
        setError(err)
      })
      .finally(() => {
        setLoading(false)
      })
  }, [])

  return {
    widgets,
    widgetsError: error,
    widgetsLoading: loading
  }
}

Looking back, I now realize I was writing Tanstack Query, just worse and with fewer features. It works great for simple applications but for complex applications with lots of state and lots of interactivity, I wish I had been smart enough to just suck it up and figure out this library. Live and learn, I guess!

However, this post is not about espousing the benefits of the library. When I first imported it into a client's project, I ran into a problem with my unit and integration tests, and I thought it would be fun to share some of the solutions I explored.

The Problem

Tanstack Query requires a common Query Client to be instantiated if you want to share state between components and cache things, etc. This provider is implemented with React Context and needs to live high enough in your component tree to reach all the places you want to access it from. A common implementation might be to put it around your root component so that the features are available globally throughout your application.

const queryClient = new QueryClient();

root.render(
  <QueryClientProvider client={queryClient}>
    <App /> // the whole app in here
  <QueryClientProvider/>
);

With the client in place, you can implement your queries with the useQuery hook and leverage this context for caching and other features. This was all well and good until I tried to run my unit tests. This application had a suite of around 400 unit and integration tests, implemented with Vitest and using React Testing Library. Every test of a component where I used React Query threw this error:

Error: Uncaught [Error: No QueryClient set, use QueryClientProvider to set one]

Thankfully, this error is pretty clear, and understanding the issue was not an impediment. React Testing Library renders components in isolation, without the entire app context around it. This is useful for performance and of course to ensure that your smaller unit tests are functional without outside dependencies. But if I want to test a component with Tanstack Query, what do I do?

Solution 1: Throw Code At It

The first and simplest solution would be to just instantiate a new query client and wrap your component you want to test, directly inside React Testing Library's render function. Something like this:

import { render } from '@testing-library/react';

const queryClient = new QueryClient();

render(
  <QueryClientProvider client={queryClient}>
    <MyComponentToTest />
  <QueryClientProvider/>
);

This solves the immediate issue. But this tactic is not very maintainable, because now our test is tied to the implementation of our code, which is an anti-pattern. If we later decide to drop Tanstack Query for whatever reason (maybe it gets replaced, maybe it stops being maintained, etc.), we'd be up against the exact same problem all over again. Our tests would break because we changed our implementation, even if the user's experience (and therefore the test result) is no different. In addition, since you want to render tests in isolation from each other, you'll need to instantiate a new Query client with each test, meaning this adds 3 lines of the exact same code over and over for every test that needs it.

Solution 2: Custom Wrapper Component

Another idea is to create a separate TestWrapper component which could be imported, putting some distance between the test and the implementation, since the React Query part would exist only in one file and could be easily modified if that implementation changes. In this example, I also use the wrapper option from the render function to show a cleaner setup, but you could do it the original way above, too.

// TestWrapper.jsx

export const TestWrapper = ({ children }) => (

  const queryClient = new QueryClient();

  <QueryClientProvider client={queryClient}>
    {children}
  </QueryClientProvider>
);

// MyComponentToTest.test.js

import { render } from '@testing-library/react';
import { TestWrapper } from '@test-utils/TestWrapper.jsx';

render(<MyComponentToTest />, { wrapper: TestWrapper });

This is much better, but could be slow to implement, since you'd need to adjust every render call in every test file that needs it. That might be ok as one-time chore, especially since you'd only need to bring it in to tests where you're using Tanstack Query. If you're sprinkling this in to a few components to start that could be totally manageable. I also might do this if I was starting a project from scratch. What I like about this solution most is that it is really clear what is happening, and easy to adjust later. However, if you are replacing some existing Context implementations and many of your tests would be impacted by the very first use of Tanstack Query in your app, it might be less viable to make all these changes.

Solution 3: Custom Render Function

One solution suggestion I read about was to abstract the entire render function away into a custom render function, further extending our custom wrapper component, like this:

import { render } from '@testing-library/react';

const TestWrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>
    {children}
  </QueryClientProvider>
);

export const customRender = (component, options) => {
  let newOptions = options;

  if (newOptions) {
    newOptions.wrapper = TestWrapper;
  } else {
    newOptions = {
      wrapper: TestWrapper,
    };
  }

  return render(ui, newOptions);
}

This little helper essentially passes on the options and component passed to it to the actual render function, wrapping the component in our QueryClient first. In any test where you need it, you would call the custom function instead of the default one from Testing Library. React Testing Library recommends a similar approach.

import { customRender } from '@test-utils/customRender.js';

customRender(<MyComponentToTest />);

The test file looks a little cleaner, but I think I actually like this solution the least. Here are some problems I see with this:

  • You are still saddled with the above problem of having to adjust every render call in every test that needs the wrapper (swapping out the original render function for your new one)
  • Other developers on your team may not know about customRender and go off and implement their own solutions instead with the default render.
  • It's not immediately clear what customRender is doing, which might slow troubleshooting. You could name it better (like tanstackQueryWrapperRender), but this limits its use case, and if you are solving this same problem for a different provider later, you'll need a whole other function. If you need a test that requires both providers, your custom wrapper will need to be adjusted to have dynamic wrapper components.

In short, I think Solution 3 suffers from a bit too much abstraction.

Solution 4: Go Nuclear

A final solution, which I call the nuclear option, is one where we literally replace the render function. This takes a couple steps, but essentially builds off the previous implementation.

// custom-react-testing-library.jsx

import { render as rtlRender } from '../../node_modules/@testing-library/react';

const TestWrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>
    {children}
  </QueryClientProvider>
);

export const render = (component, options) => {
  let newOptions = options;

  if (newOptions) {
    newOptions.wrapper = TestWrapper;
  } else {
    newOptions = {
      wrapper: TestWrapper,
    };
  }

  return rtlRender(ui, newOptions);
}

export * from "../../node_modules/@testing-library/react";

// override React Testing Library's render with our own
export { render };

Here, I've changed the names of the imports so that my custom render function can be called the same name. Then, I re-export everything from the original library and the new render function. This file now serves as a complete stand-in for the entire React Testing Library import. Anything you could import from there, can be imported from this file.

One important distinction is that the imports are not coming from @testing-library/react anymore, but directly from the node_modules path. I don't love doing this, but it's necessary for what we're about to do next in our framework configuration - alias the module name. Otherwise, we'll get an infinite import loop. This project uses vite so I'm showing the Vite config, but you can do a similar thing with Next.JS and other frameworks as well.

return defineConfig({    
  resolve: {
    alias: {
      "@testing-library/react": path.resolve("src/test-utils/custom-react-testing-library.jsx"),
    },
  },
})

So how does this work? Well, now the build tool will resolve `@testing-library/react` to your custom file instead of the node_modules location. Which means that automatically, all the render calls in all your files are now calling your wrapper render function, giving the Context to each test. Wow, magical! Definitely the fastest solution to getting up and running.

So do I love that solution? Well, not exactly! Here are some problems with this:

  • This still obfuscates what's happening behind the scenes, now even more so since at first glance in a test file, it's not in any way clear that your render function is wrapped.
  • The example implementation I gave also overrides the wrapper option, meaning that if you tried to wrap another thing around it, it would fail with no error. Imagine another dev on your team frantically browsing documentation on testing library trying to figure out why the wrapper isn't working, having no idea you've intercepted this package.
  • It can be overkill - the Wrapper is rendered on every component, whether you need it or not, which might affect performance.
  • There is no interface to dynamically choose your wrapper, meaning this is an all or nothing approach.

Conclusion

So which solution to use? Well, as in all things:

A fake programming book in the style of O'Reilly, titled "It Depends - The Definitive Guide". An elephant is pictured @ThePracticalDev
It Depends - The Definitive Guide

Here's where I ended up. I decided that Solution 2 was the solution which allowed a good flexibility of options:

  • It's readable. It's very clear that a wrapper component is being added to the test.
  • It uses the library's documented options without modification
  • It provides an interface to dynamically add providers as needed (see below)
  • It's selective, maximizing performance.

It did require that I modify every test in my suite that needed it. It ended up taking me about 30 minutes, including around 10 minutes of fiddling with Github co-pilot to see if I could make it figure it out for me. Still some opportunities for AI, I guess.

The only thing missing from the implementation was the ability to add multiple providers to my wrapper as needed, dynamically. This app also uses React Router, which means occasionally I need to wrap a component with <BrowserRouter> to solve a similar issue. In cases where I needed both, I put together a function which dynamically builds a provider tree based on an array of inputs. I found a few different ideas for this on the internet, and settled on a bit of a Frankenstein of them all. I had to make some changes to integrate it with Typescript, too, but the version below is plain ole' Javascript for simplicity.

export const buildProviderTree = (providers) => {
  const A = providers.shift();

  if (!A) {
    return (props) => <>{props.children}</>;
  }

  const B = providers.shift();

  if (!B) {
    return A;
  }

  return buildProviderTree([
    ({ children }) => (
      <A>
        <B>{children}</B>
      </A>
    ),
    ...providers,
  ]);
};

This essentially recursively builds a new component over and over until the array is empty, nesting each one in to the next. And it allows me to do something like this:

render(
  <MyComponentToTest />,
  { wrapper: buildProviderTree([
    BrowserRouter, 
    TanstackQueryProvider
  ])}
);

Now I can make a single wrapper component for each provider, and just call them as needed into any test. Beautiful! As a bonus, I can use this buildProviderTree function (and the wrappers) in my actual production code for the real application provider stack, avoiding what we call provider hell and adding fidelity to the tests, since they use the real thing.

// just say no

<Provider1>
  <Provider2>
    <Provider3>
      <Provider4>
        <Provider5>
          <PleaseKillMe>
            <App /> // help me
          </PleaseKillMe>
        </Provider5>
      </Provider4>
    </Provider3>
  </Provider2>
</Provider1>

By the way, this pattern is known as a "pipe function", and can be implemented in many different ways, like using the built-in reduce method, as shown here. This pattern is so ubiquitous in functional programming that there is even a proposal open now to add a specific piping syntax to Javascript, a pipeline operator |>. So in the future, maybe I'll be able to refactor this! It will be neat to see how this might incorporated into React, should it be adopted. Recursive component rendering is pretty much a core function of React.

What option would work best for your project?