Style of defining React HOC components

Viewed 281

So, in my React app, I see the following pattern for defining HOCs;

export function withMyApi() {
  // Extra function wrapper to allow for clear place for configs.  Plus, you can compose multiple HOCs if necessary
  return function(WrappedComponent) {
    class MyApiUrls extends React.Component {
      constructor(props) {
        super(props);
      }
      render() {
        return <WrappedComponent api={this.api} {...this.props} />;
      }
    }
    return MyApiUrls;
  };
}

How does the above work? Is the HIC returning a function component here? Is the above way intentionally used to be able to compose ?

So specifically, an HOC returns either a class component or a function component. However, with the above pattern, it is not clear as to what is being returned by the HOC.

2 Answers

The function in your question is an HOF (high-order-function) of name withMyApi which returns an anonymous function which accepts a component as WrappedComponent argument which, when called, returns the "Component" you passed.

Here is the syntax to use this HOF:

const MyComponent = withMyApi()(MyOriginalComponent)

Yes, you can say that withMyApi()(MyOriginalComponent) is unnecessary work, it should have just been withMyApi(MyOriginalComponent).

And I agree. So, here are refactored HOCs:

Option 1, using a class component as the wrapper:

export function withMyApiClass(WrappedComponent) {
  return class MyApiUrls extends React.Component {
    constructor(props) {
      super(props)
      this.api = 'http://www.example.com'
    }
    render() {
      return <WrappedComponent api={this.api} {...this.props} />
    }
  }
}

Syntax to use the above HOC: const Comp1 = withMyApiClass(Comp).

Option 2, using a function component as the wrapper:

export function withMyApiFunction(WrappedComponent) {
  return function MyApiUrls(props) {
    const api = useRef('http://www.example.com')
    return <WrappedComponent api={api.current} {...props} />
  }
}

Syntax to use the above HOC: const Comp1 = withMyApiFunction(Comp).


So, why do we need the first form (in question) of withMyApi HOF?

You may need or may not, depending on your requirement. A typical use-case would be to pass some config object. An example:

export function withMyApi(config) {
  const { secure } = config
  return function (WrappedComponent) {
    class MyApiUrls extends React.Component {
      constructor(props) {
        super(props)
        this.api = `http${secure ? 's' : ''}://www.example.com`
      }
      render() {
        return <WrappedComponent api={this.api} {...this.props} />
      }
    }
    return MyApiUrls
  }
}

Syntax to use the above HOF: const Comp1 = withMyApi({ secure: false })(Comp).

PS: If you ever used Redux, you must have seen connect HOF which accepts a few arguments, similar to your HOF in question.

So the basic idea behind writing HOFs is - separating config parameter from the component parameter to allow people to easily use compose function when there are multiple HOCs to use.


Your questions:

How does the above work?

HOC in question is actually high-order-function: It returns a function which, when called, returns a component.

Is the HOF returning a function component here?

No, It is returning a function. But when we call the returned function, it returns a class component. See the option 2, that returns a function component.

Is the above way intentionally used to be able to compose?

Yes, you can use compose utility (from any library like - Ramdajs, Lodash, or Redux) to create a new "composed HOC" when using multiple HOCs or HOFs. See Maximizing Composability.


Edit:

I don't like HOF. Can I still achieve composition and configuration?

Yes, an example. Let's say we have written below HOC:

export function withMyApi(WrappedComponent, config) {
  const { secure } = config
  return function MyApiUrls(props) {
    const api = useRef(`http${secure ? 's' : ''}://www.example.com`)
    return <WrappedComponent api={api.current} {...props} />
  }
}

How to use it?

const Comp1 = withMyApi(Comp, { secure: true })

/* OR, if you want to use more HOCs.
Nested (dirty? Maybe) */
const Comp1 = withRouter(withMyApi(Comp, { secure: true })) 

-----------------------------------------------------------

// But you CAN NOT compose like this:
const composedHOC = compose(
  withRouter,  // a function : OK
  withMyApi(Comp, { secure: false }) // NOT OK. Because is a React Component now.
)
/* It will give error when you will call composedHOC(...) because 
in React, we don't call components using parenthesis i.e. () but 
we invoke then using angle brackets i.e. < />. */

/* After compsing, you CAN NOT do the below otherwise you are going 
to pass `Comp` 2 times to `withMyApi` : Bad/Confusing... :( 
PLUS you will see errors due to calling a React Component using
parenthesis i.e. () as mentioned above. */
const Comp1 = composedHOC(Comp)

-----------------------------------------------------------

// But you can compose like the below:
const composedHOC = compose(
  withRouter, 
  withMyApi
)
const Comp1 = composedHOC(Comp, { secure: false })
/* It will work because it will pass the TWO parameters - "Comp" & 
"{ secure: false }" to LAST (withMyApi) HOC. And the result of that 
will be passed to FIRST HOC (withRouter). */

-----------------------------------------------------------

// But, surprisingly, there will be error when you change the order:
const composedHOC = compose(
  withMyApi, // FIRST
  withRouter // LAST
)
const Comp1 = composedHOC(Comp, { secure: false })
/* Because it will pass the TWO parameters to LAST HOC (withRouter), 
and result of that to FIRST HOC (withMyApi) and So, withMyApi will 
receive ONLY enhanced "Comp" NOT the "{ secure: false }". This is why
using "compose" is buggy for HOCs that acpect config parameters. */

/* Just to remind, this is what a compose function does:
"Composes single-argument functions from right to left. The rightmost 
function can take multiple arguments as it provides the signature for 
the resulting composite function."
compose(f, g, h) is the same as (...args) => f(g(h(...args)))
Means, it pass args to h, and the result to g and then result to f. */

Ok, I may like HOF. How to do the same using HOF?

export function withMyApi(config) {
  const { secure } = config
  return function (WrappedComponent) {
    return function MyApiUrls(props) {
      const api = useRef(`http${secure ? 's' : ''}://www.example.com`)
      return <WrappedComponent api={api.current} {...props} />
    }
  }
}

How to use it?

const Comp1 = withMyApi({ secure: false })(Comp)

// OR, if you want to use more HOCs
const composedHOC = compose(
  withRouter,
  withMyApi({ secure: false })
)
const Comp1 = composedHOC(Comp) // Looks clean :)

Conclusion:

  1. Don't use compose with HOCs if they accept "component" and one or more "config" object. Use nested approach instead.
  2. Use nested or the better - compose with HOFs and HOCs (as long as HOCs don't accept a config object)
  3. It is best to write HOFs when you want to accept one/more config objects. So that, people can use compose with your HOF.

HOF is better / cleaner if you want to support "configuration" through additional parameters to the function.

This (HOF) form may seem confusing or unnecessary, but it has a useful property. Single-argument HOCs like the one returned by the connect function have the signature Component => Component. Functions whose output type is the same as its input type are really easy to compose together.

It actually returns a function which you will be using to return a Class Component.

I think the confusion lies with the concept of HOC as the above pattern with a wrapper function.

This pattern allows us to enhance our components without breaking the concept of HOC.

We need to maintain the HOC's concept but we might need to add extra configuration or compose multiple components, the above pattern will be useful.

Component => Component

The above becomes:

WrapperFunction()(Component) => Component

But we can wonder why not directly pass another args through the HOC:

HOC(Component, <any extra config we want>)

The HOC says, it takes a Component and returns a Component and extra args doesn't seems really positive isn't it?

Related