DEV Community

Cover image for React useComponent pattern
Basarat Ali Syed
Basarat Ali Syed

Posted on • Updated on

React useComponent pattern

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

Top comments (4)

Collapse
 
nombrekeff profile image
Keff

Cool pattern, i've recently started using it

Collapse
 
bendtherules profile image
Abhas Bhattacharya

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
Collapse
 
borasvm profile image
Kuldeep Bora • Edited

minor correction:

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

Collapse
 
basarat profile image
Basarat Ali Syed

Thanks. Fixed 🌹