Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save juanmaguitar/ada050ad13972783d4d57db93c4f0f22 to your computer and use it in GitHub Desktop.
Save juanmaguitar/ada050ad13972783d4d57db93c4f0f22 to your computer and use it in GitHub Desktop.
Using Recompose to write clean HOCs

Using Recompose to write clean HOCs

In Javascript, we have a special type of functions, called Higher Order Functions that is those functions that work with other functions, either because they receive them as parameters (to execute them at some point of the body's functions) or because they return a new function when they're called

const sum = (a, b) => a + b
const multiplication = (a, b) => a * b

// Our Higher-Order Function
const getResultOperation = op => (a, b) => `The ${op.name} of ${a} and ${b} is ${op(a, b)}`

const getSumResult = getResultOperation(sum)
const getMultiplicationResult = getResultOperation(multiplication)

console.log( getSumResult(2, 5) ) // The sum of 2 and 5 is 7 
console.log( getMultiplicationResult(2, 5) ) // The multiplication of 2 and 5 is 10 

See Online

In the example above getOperationResult receives a function and returns a new one. So it is a Higher-Order Function

The most popular Higher-Order Functions in Javascript are the array methods map, filter or reduce. They all apply some function passed as a parameter over the elements of the array to get something as a result

In React, we have a similar concept for components. There's a special type of functions in React that take a component and return a new component. These functions are called Higher-Order Components

Higher Order Components

When are the Higher-Order Components useful? Well, mostly to reuse the code across components. Let's explain this with this scenario.

Let's assume we already have a component Button

Button.js

const Button = ({ type = "primary", children, onClick }) => (
  <button className={`btn btn-${type}`} onClick={onClick}>
    {children}
  </button>
);

And we want to create another ButtonWithTrack based on this Button (same props on Button should also work on ButtonWithTrack and same styles applied) but with improved behavior (like keeping track of the times it has been clicked and displaying this value on the button itself)

To do this we can do...

ButtonWithTrack.js

import Button from "./Button";

class ButtonWithTrack extends Component {
  constructor(props) {
    super(props);
    this.state = {
      times: 0
    };
  }
  handleClick = e => {
    let { times } = this.state;
    const { onClick } = this.props;
    this.setState({ times: ++times });
    onClick && onClick();
  };
  render() {
    const { children } = this.props;
    const { times } = this.state;
    return (
      <span onClick={this.handleClick}>
        <Button type={times > 5 ? "danger" : "primary"}>
          {children} <small>{times} times clicked</small>
        </Button>
      </span>
    );
  }
}

We have reused the original Button so everything ok for now.

Let's take another component Link

Link.js

const Link = ({ type = "primary", children, href, onClick }) => (
  <a style={styles} className={`badge badge-${type}`} href={href} onClick={onClick}>
    {children}
  </a>
);

And we want to add the exact same behavior we added to our Button.

What to do then? Should we repeat 90% of the code in 2 files? Or is there a way we can take out the logic added to ButtonWithTrack in a way it can be applied to both Button and Link components?

Higher-Order Components to the rescue!!

If we create a function that takes one component and returns the enhanced component with the behavior we want like this one...

const withClickTimesTrack = WrappedComponent =>
  class extends Component {
    constructor(props) {
      super(props);
      this.state = {
        times: 0
      };
    }
    handleClick = e => {
      e.preventDefault();
      let { times } = this.state;
      const { onClick } = this.props;
      this.setState({ times: ++times });
      onClick && onClick();
    };
    render() {
      const { children, onClick, ...props } = this.props;
      const { times } = this.state;
      return (
        <span onClick={this.handleClick}>
          <WrappedComponent
            type={times > 5 ? "danger" : "primary"}
            {...props}
          >
            {children} <small>({times} times clicked)</small>
          </WrappedComponent>
        </span>
      );
    }
  };

Then we can simplify the creation of the componentButtonWithTrack from Button by using the withClickTimesTrack HOC like this

import withClickTimesTrack from "./hoc/withClickTimesTrack";

const Button = ({ type = "primary", children, onClick }) => (
  <button className={`btn btn-${type}`} onClick={onClick}>
    {children}
  </button>
);

const ButtonWithTrack = withClickTimesTrack(Button);

And also now, we can easily apply the same enhancement to other components like Link

Link.js

import withClickTimesTrack from "./hoc/withClickTimesTrack";

const Link = ({ type = "primary", children, href, onClick }) => (
  <a style={styles} className={`badge badge-${type}`} href={href} onClick={onClick}>
    {children}
  </a>
);
const LinkWithTrack = withClickTimesTrack(Link);

What happens now if we go further and split the logic behind the hoc in smaller parts?

Composing HOC's

For example we would want to isolate these behaviors added on the HOC to be able to reuse them independently in other components:

  • Add times state
  • Add custom handleClick
  • Display inside the element the times state

To do this we can create 3 HOC's where each one will add a specific behavior...

withStateTimes.js

const withStateTimes = WrappedComponent =>
  class extends Component {
    constructor(props) {
      super(props);
      this.state = {
        times: 0
      };
    }
    setTimes = (times) => {
      this.setState({ times })
    }
    render() {
      const { times } = this.state
      const { setTimes } = this
      return (
        <WrappedComponent times={times} setTimes={setTimes} { ...this.props } />
      );
    }
  };

withHandlerClick.js

const withHandlerClick = WrappedComponent => props => {

  let { times, setTimes, children, onClick, ..._props } = props;
    
  const handleClick = e => {
    e.preventDefault();
    setTimes( ++times );
    onClick && onClick();
  };

  return (
    <WrappedComponent times={times} handleClick={handleClick} { ..._props }>
      {children}
    </WrappedComponent>
  );

}

withDisplayTrack.js

const withDisplayTrack = WrappedComponent => props => {
  const { children, onClick, handleClick, times, ..._props } = props;
  return (
    <span onClick={handleClick}>
      <WrappedComponent
        type={times > 5 ? "danger" : "primary"}
        {..._props}
      >
        {children} <small>({times} times clicked)</small>
      </WrappedComponent>
    </span>
  )
}

With these 3 HOC's we can then apply them to our elements in this way...

const ButtonWithTrack = withStateTimes(withHandlerClick(withDisplayTrack(Button)));

What's going on here? Well, withDisplayTrack(Button) returns a component that is used in the call of withHandlerClick that will also return a component that will be used in the call of withStateTimes that will return our final component (ButtonWithTrack)

As you can see, the idea is good because in this way we can reuse our code in this way, but creating these hoc's is a bit complicated and also applying them in this way is a bit hard to read.

Is there any improvement over this?

Recompose to the rescue!! :)

Recompose

What is Recompose?

In their own words:

Recompose is a React utility belt for function components and higher-order components. Think of it like lodash for React.

So, it's a set of methods we can use to improve the organization, creation and application of our HOC's encouraging the use of functional stateless components combined with the composition of HOC's

Let's start with the most used method of Recompose called compose

With compose we can compose multiple higher-order components into a single higher-order component.

In our scenario, with compose we can now express the application of our HOC's like this...

import { compose } from "recompose";

...

const ButtonWithTrack = compose(
  withStateTimes,
  withHandlerClick,
  withDisplayTrack
)(Button)

Much cleaner and easy to read, right?

Another useful method of recompose for our scenario is withState.

This method creates a HOC with almost the same behavior we implemented in withStateTimes.js

  • it adds a state property
  • it creates a handler to set the value of this state property
  • it allow us to set a initial value

So, with recompose, now we can express the same logic like this...

withStateTimes.js

...
import { withState } from "recompose";
const withStateTimes = withState('times', 'setTimes', 0)
...

For real? Yes, for real :)

The utility of recompose starts to make sense, right?

Let's continue improving the code of our scenario. Let's take the HOC withHandlerClick. To improve the creation of this HOC we can use the method withHandlers of recompose

withHandlerClick.js

import { withHandlers } from "recompose";

const withHandlerClick = withHandlers({
  handleClick: props => e => {
    let { times, onClick, setTimes } = props;
    e.preventDefault()
    setTimes( ++times );
    onClick && onClick();
  }
})

The method withHandlers takes an object map of handler creators. Each one of the properties of this object passed to withHandlers should be a Higher-Order Functions that accept a set of props and return a function handler. In this way we can generate a handler that have access to the props of the component

In our example, if we debug the code with the React Developer Tools the withDisplayTrack.js the component returned by this HOC is displayed as Unknown

To fix this, we can use the setDisplayName of recompose to export a final HOC that will return a component with the name ComponentWithDisplayTrack

export default compose(
  setDisplayName('ComponentWithDisplayTrack'),
  withDisplayTrack
);

Conclusion

With these methods we can now easily create another version of Button that track the clicks from 3 to zero, and adds another prop so we can change the type when the countdown reach zero

const ButtonWithTrackCountdown = compose(
  withState('times', 'setTimes', 3),
  withState('type', 'setType', 'primary'),
  withHandlers({
    handleClick: props => e => {
      let { times, onClick, setTimes, setType } = props;
      e.preventDefault()
      if ( times <= 0 ) {  setType('secondary') }
      else { setTimes( --times ) }
      onClick && onClick();
    }
  }),
  withDisplayTrack
)(Button)

As you can see, with recompose is easier to delegate the logic into Higher-Order Components and then compose these little pieces of logic to create different versions of our components reusing most of our code.

Also, recompose discourage the use of classes for creating components and using only Functional Stateless Components combined with Higher Components

Using only Function components have several key advantages:

  • They help prevent abuse of the setState() API, favoring props instead.
  • They encourage the "smart" vs. "dumb" component pattern.
  • They encourage code that is more reusable and modular.
  • They discourage giant, complicated components that do too many things.
  • In the future, they will allow React to make performance optimizations by avoiding unnecessary checks and memory allocations.

Here you have some recompose recipes that can be useful to your project

So, now that you know a bit more about recompose... What is your first impression? What do think about it? Do you think is a good way to go when creating components?

My opinion is... that i like it!! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment