Reusable Higher-Order Components

02.12.2018

Intro

This is a third post about functional programming in React. In the previous posts I've demonstrated how FP applies to JavaScript and React, and explained the concept of higher-order components.

I believe that the main benefit of FP approach is reusability. If your business logic is separated from your render code, the later can be reused in different context or even application, given the interface for the render function is flexible enough. But the business logic itself can also be implemented as a higher-order function. You can take any piece of code that is present in multiple places in your project, give it a name and interface, and use functional composition to attach it to your components.

If you are not familiar with the concept of higher-order components, I recommend taking a look at Higher-Order Components in React first.

Practical example

Let's take a simple practical example. You are developing a single-page application that needs to talk to an API to get any data in or out. Your application has a sign-in form, and a sign-up form, that both send POST requests to server. They also display request state and have to react to errors.

It's tempting to duplicate the calls to your API in both of those components. They use different API endpoints, they handle errors differently, the data that they send and receive is also different. But if you consider an SPA that has a hundred of API calls, repeated across tens of modules, you will get a different picture. The amount of boilerplate code to set request state, handle errors or pass the response will become the vast majority of the code you duplicate. This makes it error-prone, hard to test and messy.

We will refactor those forms to use a new higher-order component that handles talking to an API. This HOC will itself be composed of smaller HOCs, to demonstrate the idea once more.

Form classes

Those are our two forms, implemented as classic React class components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class LoginForm extends React.Component {
    state = {
        submitting: false,
        error: null,
        success: null,
    }

    handleSubmit() {
        this.setState({ submitting: true })
        const data = {
            email: document.querySelector('#email').value,
            password: document.querySelector('#password').value,
        }
        api().post('/login', JSON.stringify(data)).then(
            (res) => {
                const error = res.error || this.state.error
                const success = res.code === 200 || this.state.success

                this.setState({ submitting: false, error, success })
            }
        )
    }

    render() {
        if (this.state.submitting) {
            return <span>Submitting...</span>
        }
        if (this.state.error) {
            return <span>An error has occured! Try again. More info: {this.state.error}</span>
        }
        if (this.state.success) {
            return <span>Logged in successfully, redirecting...</span>
        }
        return (
            <form onSubmit={this.handleSubmit}>
                <input name="email" id="email" placeholder="Email" />
                <input name="password" id="password" type="password" />
                <button type="submit" value="Log in"/>
            </form>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class RegisterForm extends React.Component {
    state = {
        submitting: false,
        error: null,
        success: null,
    }

    handleSubmit() {
        const email = document.querySelector('#email').value
        const username = document.querySelector('#username').value
        const password = document.querySelector('#password').value
        const password_confirm = document.querySelector('#password_confirm').value
        
        if (password !== password_confirm) {
            alert("Passowrds don't match!")
            return
        }

        this.setState({ submitting: true })
        const data = {
            username,
            email,
            password,
            password_confirm,
        }
        api().post('/signup', JSON.stringify(data)).then(
            (res) => {
                const error = res.error || this.state.error
                const success = res.code === 200 || this.state.success

                this.setState({ submitting: false, error, success })
            }
        )
    }

    render() {
        if (this.state.submitting) {
            return <span>Submitting...</span>
        }
        if (this.state.error) {
            return <span>An error has occured! Try again. More info: {this.state.error}</span>
        }
        if (this.state.success) {
            return <span>Account created successfully, redirecting...</span>
        }
        return (
            <form onSubmit={this.handleSubmit}>
                <input name="username" id="username" placeholder="Username" />
                <input name="email" id="email" placeholder="Email" />
                <input name="password" id="password" type="password" />
                <input name="password_confirm" id="password_confirm" type="password" />
                <button type="submit" value="Sign up"/>
            </form>
        )
    }
}

Even with two forms, you can see how much of the code is duplicated. Most of it is boilerplate that you will duplicate every time you need to get something from the API.

Creating a complex HOC

Let's refactor those components using functional programming techniques. We will create a higher-order component that injects request state, server response and a handler function into the component it will enhance.

To save time, we won't implement compose, withState or any other higher-order function. Instead, let's use Recompose - a very well-made and well-documented library full of basic HOCs like that.

Let's move all API-related code into a higher-order component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const withApi = (endpoint, method) => compose(
    withState('apiState', 'setApiState', { pending: false, error: null, response: null }),
    withHandlers((props) => ({
        apiRequest: (data) => {
            const call = api()[method]

            props.setApiState({ pending: true })

            call(endpoint, data).then((res) => {
                const error = res.error || props.error
                const response = res.code === 200 ? res.body : props.response

                props.setApiState.setState({ pending: false, error, response })
            })
        }
    })),
    omitProps(['setApiState']),
)

As you see, we've moved all the API-related code into this reusable higher-order component. It uses other simple HOCs to create pretty complex behaviour.

Using the HOC

Now, let's refactor our form components to use the withApi HOC. The goal is to remove all API implementation details, and use props that are passed down instead.

The results look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const LoginForm = withApi('/login', 'post')(
    (props) => {
        if (props.apiState.pending) {
            return <span>Submitting...</span>
        }
        if (props.apiState.error) {
            return <span>An error has occured! Try again. More info: {props.apiState.error}</span>
        }
        if (props.apiState.response) {
            return <span>Logged in successfully, redirecting...</span>
        }
        return (
            <form
                onSubmit={() => props.apiRequest({
                    email: document.querySelector('#email').value,
                    password: document.querySelector('#password').value,
                })}
            >
                <input name="email" id="email" placeholder="Email" />
                <input name="password" id="password" type="password" />
                <button type="submit" value="Log in"/>
            </form>
        )
    }
)

const RegisterForm = compose(
    withApi('/register', 'post'),
    withHandlers(props => ({
        handleSubmit: (data) => {
            if (data.password !== data.password_confirm) {
                alert("Passowrds don't match!")
                return
            }

            props.apiRequest(data)
        }
    }))
)(
    (props) => {
        if (props.apiState.pending) {
            return <span>Submitting...</span>
        }
        if (props.apiState.error) {
            return <span>An error has occured! Try again. More info: {props.apiState.error}</span>
        }
        if (props.apiState.response) {
            return <span>Account created successfully, redirecting...</span>
        }
        return (
            <form 
                onSubmit={() => props.handleSubmit({
                    username: document.querySelector('#username').value,
                    email: document.querySelector('#email').value,
                    password: document.querySelector('#password').value,
                    password_confirm: document.querySelector('#password_confirm').value,
                })}
            >
                <input name="username" id="username" placeholder="Username" />
                <input name="email" id="email" placeholder="Email" />
                <input name="password" id="password" type="password" />
                <input name="password_confirm" id="password_confirm" type="password" />
                <button type="submit" value="Sign up"/>
            </form>
        )
    }
)

As you can see, the usage of withApi is really simple - it only needs two parameters, and it passes down two new props - apiState and apiRequest. It's interface can be extended via optional arguments later.

if we want to do something to the data before sending it to the server, like validate or transform it, we can use a higher-order component again. I've done it in RegisterForm to demonstrate how to implement non-standard cases while still reaping the beneifts of functional composition.

The code above can be further improved by moving code related to API request state into a separate component, but it's out of scope of this post.

Conclussion

This post is meant to be the last in the series about functional programming in React. I wrote it to explain the concepts to a relatively inexperienced developer, or someone who is coming from an object-oriented language like Java. I believe that I gave a good overview - from showing how functional programming is native to JavaScript to creating a reusable higher-order component.

I don't think that FP is necessarily superior to object-oriented approach, but it makes a lot of sense to me in context of React. React documentation mentions FP a lot, and core team members have expressed preference for it.

My experience tells me that functional programming works well in a very big codebase that is shared across different product teams. Working with the functional code was good developer experience, it fascilitated a lot of code reuse and encouraged engineers to have good test coverage of reused code. FP became my favorite paradigm, and I have learned to apply it in other languages like Python and Go.

Useful links

02.12.2018
Tags:  ProgrammingFunctional programmingReactRecomposeJavaScript