This article was published on Monday, June 24, 2019 by Eytan Manor @ The Guild Blog
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:
``ts filename="auth-service.js"
${API}/sign-up`,
export const signUp = body => {
return fetch({
method: 'POST',
url:
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)
})
}
```tsx filename="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:
```tsx filename="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:
```ts filename="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
```ts filename="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()
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()
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:
```ts filename="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:
```tsx filename="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 {props.children}
}
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`:
```tsx filename="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:
```tsx filename="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:
```tsx filename="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:
Top comments (0)