Have you ever struggled to test this little fetch()
call or this window.location
in your React App? The thing with those Web APIs is that you cannot mock them directly. Of course you can globally mock the fetch API during test setup like this or use an npm package to do the same thing. But what to do with the next fancy API? I say you can solve the problem much easier and end up with a cleaner architecture at the same time by wrapping the APIs into a React.Context.
First let us define a very thin wrapping layer of all APIs we need to use
export interface Api {
fetch: typeof fetch
}
export const createApi: () => Api = () => ({ fetch })
export const mockedApi: () => Api = () => ({ fetch: jest.fn() })
You can create the Api in two ways. One in your production code with createApi
and one in your tests with mockedApi
. The problem is that you can't just invoke fetch()
wherever you like anymore. You first have to retrieve the Api object from somewhere. If you call the createApi()
method whenever you need the object, you still cannot replace the Api by a mock during testing. You need to somehow pass the object through your whole App and put it into the Props of all your Components. This is not very elegant and a lot of work!
Luckily, React comes with a solution. You can create a React.Context object, put your Api into it and consume this context wherever you need it.
I do not like to use my API directly from my Components, so I first create Service objects.
export interface Services {
users: UsersService
contacts: ContactsService
rest: RestService
}
const createServices = (): Services => {
const api = createApi()
const contacts = new ContactsService(api)
const rest = new RestService(api)
const entities = new EntityService(api)
return { contacts, rest, entities }
}
When testing these services you can easily wrap the Api and focus on the interaction with the Api. For a component to use these services you have to put them into a React.Context.
export const Services = React.createContext<Services | undefined>(undefined)
const AppWithContext = (): JSX.Element => (
<Services.Provider value={createServices()}>
<App />
</Services.Provider>
)
This is exactly how you provide a Redux Store to your App. Let us write something very similar to redux's connect function.
export const injectServices = <P extends object>(
WrappedComponent: React.ComponentType<P & Services>
): React.ComponentType<P> => props => (
<Services.Consumer>
{services => services && <WrappedComponent {...services} {...props} />}
</Services.Consumer>
)
This function takes a Component that has some Props P & Services
and returns a Component that has only Props P
. You can easily use it like this
type Props = Services & OwnProps
export injectServices(
(props: Props): JSX.Element => <></>
)
and you can even put a connected Component into this function.
export const dispatchToProps: (
dispatch: Dispatch,
props: Service & OwnProps
) => Callbacks = (dipatch, props) => ({
onSave: () => dipatch(createActionUsingService(props))
})
export default injectServices(
connect(
stateToProps,
dispatchToProps
)(MyComponent)
)
Now you can use the services even in your mapping function, which is probably exactly where you want them.
If you like this post, why not have a look at one of my other (probably more light-hearted) posts. This post was sponsored by itemis AG. That's the place were I work and drink.
Top comments (0)