DP's CODE NOTES

A React HOC for resolving promises

2018-03-21

React Higher-Order Components is a great technique and pattern that emerges from React’s compositional nature. If you want to abstract certain commonly used logic like logging and data fetching you might want to consider to move that code into an HOC. One thing that can result in quite a lot of repetetive boilerplate code is promise resolving if you use Fetch API or any other API that uses promises. Lets look at an example that uses GitHub API to fetch a quote. First we have a component that simply render a quote and can reload with a new one:


import React from 'react'
import T from 'prop-types'

const Quote = props =>
    <div>
        <p>
            {props.firstName} says:
        </p>
        <q>
            {props.theQuote}
        </q>
        <button
            onClick={props.reload}>
            Load again
        </button>
    </div>

Quote.propTypes = {
    theQuote: T.string.isRequired,
    firstName: T.string.isRequired,
    reload: T.func.isRequired
}

export default Quote

...and one simple file that contains the fetch logic


const handleError = r => {
    if (!r.ok) throw Error(r.statusText) //handle http errors
    return r
  }
  
const quoteApi = url =>
    fetch(url)
        .then(handleError)
        .then(r => r.text())

export default quoteApi

Typically you have a React class that handles the state:


import React, { Component } from 'react'

import Quote from './Quote'
import { quoteApi } from './quoteApi'


class QuoteAjax extends Component {

    state = {
        loading: true,
        data: null,
        error: null,
    }

    componentDidMount() {
        this.fetchQuote(false)
    }

    componentWillReceiveProps() {
        this.fetchQuote()
    }

    fetchQuote = (load = true) => {
        if (load) {
            this.setState({ data: null, loading: true })
        }
        quoteApi('https://api.github.com/zen')
            .then(text => this.setState({ data: text, loading: false }))
            .catch(error => this.setState({ error: error, loading: false }))
    }

    render() {
        const { data, loading, error } = this.state
        if (loading === true)
            return <span>loading...</span>
        if (error !== null)
            return <span>Error: {error.message}</span>
        return (
            <Quote
                theQuote={data}
                firstName={this.props.firstName}
                reload={this.fetchQuote} />
        )
    }
}

export default QuoteAjax
        
// assuming the call is like: <QuoteAjax firstName="Bob" />

There is some repetetive code here like error handling, state variables and displaying a loading spinner. If you move this code to an HOC the same implementation might look like this:


import Quote from './Quote'
import { quoteApi } from './quoteApi'
import promisedHOC from './PromisedHOC'

const promise = () =>
  quoteApi('https://api.github.com/zen')
    .then(text => ({ theQuote: text })) // returns object used in component

export default promisedHOC(promise)(Quote)


We saved quite a few LOC and likely reduce the risk of bugs when using this across a codebase. The HOC for this would look like this:


import React, { Component } from 'react'


const promisedHOC = (promise, mapStateToProps) =>
  (Wrapped, Loader, Failed) =>
    class extends Component {

      //displayName in developer tools
      static displayName = `promisedHOC(${(Wrapped.displayName || Wrapped.name || 'Component')})`

      state = {
        loading: true,
        data: null,
        error: null
      }

      componentDidMount() {
        this.execPromise(this.props, false)
      }

      componentWillReceiveProps(nextProps) {
        // you probably want this to be configurable
        if (this.props !== nextProps) {
          this.execPromise(nextProps)
        }
      }

      execPromise = async (props, load = true) => {
        if (load) {
          this.setState({ data: null, loading: true })
        }
        let res = null
        try {
          res = await promise(props)
        }
        catch (error) {
          this.setState({ error: error, loading: false })
          return
        }
        if (mapStateToProps !== undefined) {
          res = mapStateToProps(res)
        }
        this.setState({ data: res, loading: false })
      }

      //execute promise again
      reload = () => this.execPromise(this.props)

      render() {
        const { data, loading, error } = this.state
        if (loading === true) {
          if (Loader !== undefined){
            return <Loader />
          }
          return <span>Loading..</span>
        }
        if (error !== null) {
          console.error(error)
          if (Failed !== undefined){
            return <Failed message={error.message} />
          }
          return <span>Error occured</span>
        }
        // ... and renders the wrapped component with the fresh data!
        // Notice that we pass through any additional props
        // the "data" variable is spread to be accessible in the component
        return <Wrapped {...data} {...this.props} reload={this.reload} />
      }
    }


export default promisedHOC

The "Quote" component would then have the property "reload", the "data" object is "spread" to properties ("theQuote"), it would also contain any props that were passed through ("firstName"). This HOC example also have some additional stuff that can be done:



// we have access to the props sent
const promise = ownProps => quoteApi(ownProps.gitUrl)

// might be more convenient to map the state to props in a function
const mapToProps = state => ({ theQuote: state })

//possible to inject components to override default rendering
const Loader = () => <i className="fa fa-spinner fa-pulse fa-3x fa-fw" />
const Fail = props => <span>Error: {props.message}</span>

const TheQuote = promisedHOC(promise, mapToProps)(Quote, Loader, Fail)

//usage
<TheQuote 
    firstName="Danne" 
    gitUrl="https://api.github.com/zen" />


One question is what to do when you want to resolve multiple promises for one component, you could certainly modify the HOC above for that purpose, another approach is to use composition and/or injection, example below shows how to inject one component into another:



const Quote = props => {
    const { AnotherQuote } = props
    return {
        <div>
            <p>{props.firstName} says: </p>
            <q>
                {props.theQuote}
            </q>
            <AnotherQuote />
        </div>
    }
}

Quote.propTypes = {
    AnotherQuote: T.func.isRequired
}
      
const QuoteSimple = props =>
    <q>
        {props.theQuote}
    </q>
    


const promise = () =>
    quoteApi('https://api.github.com/zen')
    .then(text => ({ theQuote: text }))

const TheQuote = promisedHOC(promise)(Quote)

const AnotherQuote = promisedHOC(promise)(QuoteSimple)


const QuotePage = () =>
    <TheQuote
        firstName="Danne"
        AnotherQuote={AnotherQuote} />

export default QuotePage

// note: this would mean the api request's are done sequential,
// i.e "AnotherQuote" will wait for the first to render, which might not be desired
// Example below would do parallel requests:

const TheQuote = promisedHOC(promise)(QuoteSimple)

const AnotherQuote = promisedHOC(promise)(QuoteSimple)

const QuotePage = () => {
    return {
        <div>
            <p>{props.firstName} says: </p>
            <TheQuote />
            <AnotherQuote />
        </div>
    }
}

//injection could also be achived with "children" props:

<TheQuote
    firstName="Danne">
    <AnotherQuote />
</TheQuote>