Injectable services in React

Eytan Manor

How They’re Implemented and Their Similarities with Angular Services

React provides a fantastic API for building components. It’s light-weight and intuitive, and became a sensation in the dev community for a reason. With the introduction of the most recent API features: hooks and context/provider, components have became not only more functional, but also more testable. Let me explain.

So far, when we wanted a component to use an external service, we would simply implement it in a separate module, import it, and use its exported methods, like so:

auth-service.js
export const signUp = body => {
  return fetch({
    method: 'POST',
    url: `${API}/sign-up`,
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  })
}
 
export const signIn = body => {
  return fetch({
    method: 'POST',
    url: `${API}/sign-in`,
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  })
}
auth-components.jsx
import React from 'react'
import auth from './auth-service'
 
const { useCallback } = React
 
export const SignInButton = ({ username, password, onSignIn }) => {
  const signIn = useCallback(() => {
    auth.signIn({ username, password }).then(onSignIn)
  }, [username, password, onSignIn])
 
  return <button onClick={signIn}>Sign-In</button>
}
 
export const SignUpButton = ({ username, password, verifiedPass, onSignUp }) => {
  const signUp = useCallback(() => {
    auth.signUp({ username, password, verifiedPass }).then(onSignUp)
  }, [username, password, verifiedPass, onSignUp])
 
  return <button onClick={signUp}>Sign-Up</button>
}

Keep in mind that this is NOT how I would actually write my code in production, there’s no error handling, and both components are defined under a single module which I don’t see as a good practice, but for demonstration purposes it’s more than enough.

The components above would work well within a React app, because essentially they can achieve what they were implemented for. However, if we would like to unit-test these components, we would encounter a problem, because the only way to test these components would be via e2e tests, or by completely mocking the fetch API. Either way, the solutions are not in our favor. Either we completely overkill it with testing, or we make use of a not-so-simple mocking solution for an ENTIRE native API. Below is an example:

auth-components.test.jsx
import React from 'react'
import { act, fireEvent, render } from '@testing-library/react'
import { SignInButton, SignUpButton } from './auth-components'
 
describe('SignInButton', () => {
  test('invokes callback on successful sign-in', () => {
    const onSignIn = jest.fn()
 
    const { getByTestId } = render(<SignInButton onSignIn={onSignIn} />)
 
    const button = getByTestId('button')
 
    act(() => {
      fireEvent.click(button)
    })
 
    expect(onSignIn).toHaveBeenCalled()
  })
})
 
describe('SignUpButton', () => {
  test('invokes callback on successful sign-up', () => {
    const onSignUp = jest.fn()
 
    const { getByTestId } = render(<SignUpButton onSignUp={onSignUp} />)
 
    const button = getByTestId('button')
 
    act(() => {
      fireEvent.click(button)
    })
 
    expect(onSignUp).toHaveBeenCalled()
  })
})

If so, how does one suppose to overcome this problem?

Let’s Learn from Our Angular Fellows

I know what you’re probably thinking right now… What is this guy thinking, promoting Angular design patterns which are completely no match for the great React. First of all, React is not perfect, and always has places for improvements. If it was already perfect, they wouldn’t have kept working on it on Facebook. Second, I like React, and I believe in it very much, this is why I would like to make it better by ensuring best practices. So before you close your tab in anger please continue reading and listen to what I have to say :-)

In the Angular team, they came up with a clever approach. Instead of relying on hard-coded imports, what they did they provided a mechanism that would let us inject our services before we initialize the component. With that approach, we can easily mock-up our services, because with the injection system it’s very easy to control what implementation of the services is it gonna use. So this is how it would practically look like:

auth-module.ts
import { NgModule } from '@angular/core'
import { SignInButton, SignUpButton } from './auth-components'
import AuthService from './auth-service'
 
@NgModule({
  declarations: [SignInButton, SignUpButton],
  providers: [AuthService]
})
class AuthModule {}
 
export default AuthModule
auth-components.ts
import { Component, Input, Output, EventEmitter } from '@angular/core'
import AuthService from './auth-service'
 
@Component({
  selector: 'app-sign-in-button',
  template: `<button (click)="{signIn()}" />`
})
export class SignInButton {
  @Input()
  username: string
  @Input()
  password: string
  @Output()
  onSignIn = new EventEmitter<void>()
 
  constructor(private auth: AuthService) {}
 
  signIn() {
    const body = {
      username: this.username,
      password: this.password
    }
 
    this.auth.signIn(body).then(() => {
      this.onSignIn.emit()
    })
  }
}
 
@Component({
  selector: 'app-sign-in-button',
  template: `<button (click)="{signUp()}" />`
})
export class SignInButton {
  @Input()
  username: string
  @Input()
  password: string
  @Input()
  verifiedPass: string
  @Output()
  onSignOut = new EventEmitter<void>()
 
  constructor(private auth: AuthService) {}
 
  signUp() {
    const body = {
      username: this.username,
      password: this.password,
      verifiedPass: this.verifiedPass
    }
 
    this.auth.signUp(body).then(() => {
      this.onSignUp.emit()
    })
  }
}

And now if we would like to test it, all we have to do is to replace the injected service, like mentioned earlier:

auth-components.test.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing'
import AuthService from './auth-service'
 
describe('Authentication components', () => {
  test('invokes callback on successful sign-in', () => {
    describe('SignInButton', () => {
      TestBed.configureTestingModule({
        declarations: [SignInButton],
        providers: [
          {
            provider: AuthService,
            useValue: { signIn: () => {} }
          }
        ]
      }).compileComponents()
 
      const signIn = jest.fn()
      const signInButton = TestBed.createComponent(SignInButton)
      signInButton.onSignIn.subscribe(onSignIn)
      expect(signIn).toHaveBeenCalled()
    })
  })
 
  describe('SignUpButton', () => {
    test('invokes callback on successful sign-out', () => {
      TestBed.configureTestingModule({
        declarations: [SignUpButton],
        providers: [
          {
            provider: AuthService,
            useValue: { signUp: () => {} }
          }
        ]
      }).compileComponents()
 
      const signUp = jest.fn()
      const signUpButton = TestBed.createComponent(SignUpButton)
      signUpButton.onSignUp.subscribe(onSignUp)
      expect(signUp).toHaveBeenCalled()
    })
  })
})

To put things simple, I’ve created a diagram that describes the flow:

Applying the Same Design Pattern in React

Now that we’re familiar with the design pattern, thanks to Angular, let’s see how we can achieve the same thing in React using its API. Let’s briefly revisit React’s context API:

auth-service.jsx
import React, { createContext, useContext } from 'react'
 
const AuthContext = createContext(null)
 
export const AuthProvider = props => {
  const value = {
    signIn: props.signIn || signIn,
    signUp: props.signUp || signUp
  }
 
  return <AuthProvider.Provider value={value}>{props.children}</AuthProvider.Provider>
}
 
export const useAuth = () => {
  return useContext(AuthContext)
}
 
const signUp = body => {
  // ...
}
 
const signIn = body => {
  // ...
}

The context can be seen as the container that holds our service, aka the value prop, as we can see in the example above. The provider defines what value the context will hold, so when we consume it, we will be provided with it. This API is the key for a mockable test unit in React, because the value can be replaced with whatever we want. Accordingly, we will wrap our auth-service.tsx:

auth-service.jsx
import React, { createContext, useContext } from 'react'
 
const AuthContext = createContext(null)
 
export const AuthProvider = props => {
  const value = {
    signIn: props.signIn || signIn,
    signUp: props.signUp || signUp
  }
 
  return <AuthProvider.Provider value={value}>{props.children}</AuthProvider.Provider>
}
 
export const useAuth = () => {
  return useContext(AuthContext)
}
 
const signUp = body => {
  // ...
}
 
const signIn = body => {
  // ...
}

And we will update our component to use the new useAuth() hook:

auth-components.jsx
import React from 'react'
import { useAuth } from './auth-service'
 
const { useCallback } = React
 
export const SignInButton = ({ username, password, onSignIn }) => {
  const auth = useAuth()
 
  const signIn = useCallback(() => {
    auth.signIn({ username, password }).then(onSignIn)
  }, [username, password, onSignIn])
 
  // ...
}
 
export const SignInButton = ({ username, password, verifiedPass, onSignUp }) => {
  const auth = useAuth()
 
  const signUp = useCallback(() => {
    auth.signUp({ username, password, verifiedPass }).then(onSignUp)
  }, [username, password, verifiedPass, onSignUp])
 
  // ...
}

Because the useAuth() hook uses the context API under the hood, it can be easily replaced with a different value. All we have to do is to tell the provider to store a different value under its belonging context. Once we use the context, the received value should be the same one that was defined by the provider:

auth-components.test.jsx
import React from 'react'
import { act, fireEvent, render } from '@testing-library/react'
import { SignInButton, SignUpButton } from './auth-components'
 
describe('SignInButton', () => {
  test('invokes callback on successful sign-in', () => {
    const onSignIn = jest.fn()
 
    const { getByTestId } = render(
      <AuthProvider signIn={Promise.resolve}>
        <SignInButton onSignIn={onSignIn} />
      </AuthProvider>
    )
 
    // ...
  })
})
 
describe('SignUpButton', () => {
  test('invokes callback on successful sign-up', () => {
    const onSignUp = jest.fn()
 
    const { getByTestId } = render(
      <AuthProvider signUp={Promise.resolve}>
        <SignUpButton onSignUp={onSignUp} />
      </AuthProvider>
    )
 
    // ...
  })
})

One might ask: “Does this mean that I need to wrap each and every service with the context API?”, And my answer is: “If you’re looking to deliver an enterprise quality React app, then yes”. Unlike Angular, React is more loose, and doesn’t force this design pattern, so you can actually use what works best for you.

Before I finish this article, here are some few things that I would like to see from the community, that I believe will make this work flow a lot easier:

  • Have a 3rd party library that would wrap a service with the context API and would simplify it.
  • Have an ESLint rule that will force the usage of injectable React services.

What do you think? Do you agree with the design pattern or not? Are you going to be one of the early adopters? Write your thoughts in the comments section below. Also feel free to follow me on Medium, or alternatively you can follow me on:

Join our newsletter

Want to hear from us when there's something new? Sign up and stay up to date!

By subscribing, you agree with Beehiiv’s Terms of Service and Privacy Policy.

Recent issues of our newsletter

Similar articles