React useComponent pattern
Basarat Ali Syed

Basarat Ali Syed @basarat

About: Microsoft MVP for TypeScript, Cypress Ambassador, open sorcerer, book author, youtuber, eggheader, developer empathiser.

Location:
Melbourne, Australia
Joined:
Jan 2, 2018

React useComponent pattern

Publish Date: Feb 5 '20
85 4

I'd like to introduce the useComponent pattern, something that has seen a lot of traction in the components we have been building lately.

Objectives

Some common objectives for good React components: 

  • Components should be reusable.
  • Components should be controllable by the container.

Pattern

You provide two items: 

  • Component : the component that you want to be reusable. This will be used by the parent to render the component.
  • useComponent : a custom hook that returns everything that the component needs in order to function. This is meant to be consumed by the parent that wants to use the component.

Motivation Example 

Consider the humble Counter component:

function Counter() {
  // Some hooks the component needs
  const [count, setCount] = useState(0);

  // The rendering of the component
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This component although functional is not really reusable as it locks its logic within the render function. Say you wanted to use two counters and display the total count, then you are 💩 out of luck.

Motivation Solution

Solution: Move any logic the Counter component needs into a useCounter custom hook function.

Here is the useCounter / Counter (and inferred TypeScript CounterProps 🌹) combo:

// Some hooks the component needs
export function useCounter() {
  const [count, setCount] = useState(0);
  return {count, setCount};
}

// Infer the props
export type CounterProps = {
  use: ReturnType<typeof useCounter>
}

// The rendering of the component
export function Counter({ use }: CounterProps) {

  return (
    <div>
      <p>You clicked {use.count} times</p>
      <button onClick={() => use.setCount(use.count + 1)}>
        Click me
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Demonstration of reuse

Say you wanted to use two counters and display the total count. Easy peasy:

export function App() {

  const counterOne = useCounter();
  const counterTwo = useCounter();

  return (
    <div>
      <Counter use={counterOne}/>
      <Counter use={counterTwo}/>

      {/* Able to use the counters for any additional logic */}
      <div>Total Count: {counterOne.count + counterTwo.count}</div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note that the useCounter function can easily take initial values. You can also create local functions in the App component that can intercept any calls made by the individual Counters.

Reasons for its popularity

There are two reasons why its so popular:

  • Easy to understand: You are writing the component as would naturally, just splitting it into logic and rendering functions. This also makes it easy to deal with while developing and doing code review for business logic and how it looks.
  • Uses only React: Your components function without any third party library dependency. This allows much greater reuse across teams.

PS: a video on comparing mobx and hooks : https://www.youtube.com/watch?v=MtVGDAnveuY

Comments 4 total

  • Keff
    KeffFeb 6, 2020

    Cool pattern, i've recently started using it

  • Kuldeep Bora
    Kuldeep BoraFeb 6, 2020

    minor correction:

    button onClick={() => {use.setCount(use.count + 1)}}>
    Click me
    /button

  • Abhas Bhattacharya
    Abhas BhattacharyaFeb 10, 2020

    Really nice pattern

    But i think -

    1. In counter definition, use should have a default value of useCounter. That way none of the consumers need to explicitly pass it if they just want the default behavior.
    2. This example doesn't really show its benefits. We are not passing any customised logic to both instances of Counter. So, if this was a simple component, it would need no props and still do same thing. If we pass two different behaviours as props in two instances of counter, then only this patterns flexibility makes more sense
Add comment