DEV Community

loading...
Cover image for How to mock React hooks using dependency injection

How to mock React hooks using dependency injection

Emma Goto 🍙
Front-end developer. In my spare time I like to make Trello Power-Ups ✨
Originally published at emgoto.com on ・3 min read

When using hooks in React you may have used or seen this sort of pattern where you use data from a hook to render your component:

import { useData } from 'use-data';

const App = () => {
    const [data] = useData();
    return <Component data={data} />;
}

export default App;

However when it comes time to unit test this component or view it using Storybook, it may fail to render! A common cause of this would be if the hook was calling a REST endpoint - which won't be accessible in a testing or Storybook scenario.

We can use a pattern known as dependency injection to get around this problem.

What is dependency injection?

Wikipedia says:

In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object.

In the above example, we would say that the App component has a dependency on the useData hook. And right now it's sourcing its own dependency by importing it.

With dependency injection , whatever renders App can supply the useData dependency. In React, we can do this via passing in useData as a prop.

Using dependency injection

Dependency injection in its most basic form would look like this:

const App = ({ useData }) => {
    const [data] = useData();
    return <Component data={data} />;
}

App no longer is responsible for knowing where useData comes from, and just uses it!

The main problem with this is that each time another component rendered App, they would have to do the work of importing and passing in useData for it:

import { useData } from 'use-data';

//usage:
<App useData={useData} />

Which is kind of silly considering most of the time we're always going to be passing in the same useData.

Using dependency injection with default props

In React we can make use of default props to allow the use-data import to be used by default:

import { useData as useDataDI } from 'use-data';

const App = ({ useData = useDataDI }) => {
    const [data] = useData();
    return <Component data={data} />;
}

If a useData prop is not passed in, by default we will use useDataDI - which is the hook imported from the use-data.

Then for all normal uses of this component, we can just render it as normal:

<App /> // we don't need to do anything else!

And when we want to use the component in unit tests or Storybooks, we can pass in our own useData hook with mock data:

const mockData = { foo: "bar" };
const mockUseData = () => [mockData];

// Usage:
<App useData={mockUseData} />

Alternatives to dependency injection

One of the downsides of dependency injection is it does require you to add an extra prop to your component that you wouldn't otherwise have. If you don't want to use the dependency injection pattern there are some alternatives:

  • When testing with Jest, it is possible to mock imports and this includes hooks too
  • You can mock endpoint calls using libraries like fetch-mock (which will also work with Storybook)
  • If you're looking to mock hooks with Storybook, there aren't any popular solutions out there (that I know of) but there is a react-magnetic-di library that may suit your needs, if you would like to give it a try

Thanks for reading!

Discussion (5)

Collapse
gabcimato profile image
Gabriele Cimato

Hey Emma this was a pretty interesting pattern to discover, thanks for sharing it!

Collapse
emma profile image
Emma Goto 🍙 Author • Edited

My pleasure!

Collapse
samkahchiin profile image
Sam Kah Chiin

Hi Emma, thanks for the sharing. It helps me to test my component on storybook!

Collapse
hollister009 profile image
Andrew Zhukevych

Ok, this was useful. Thanks a lot!

Collapse
marcagba profile image
Marc Agbanchenou • Edited

I never comment articles on dev.to, but when I do, it's for telling you that I found your article definitely helpful! Thanks !