circuit

5 Advanced React Patterns

An overview of 5 modern advanced React patterns, including integration codes, pros and cons, and concrete usage within public libraries.




Like every React developer, you've probably already asked yourself one of the following questions :

  • How do I build a reusable component to fit with different use cases?

  • How do I build a component with a simple API, making it easy to use?

  • How do I build an extensible component in terms of UI and functionality?

These recurring questions led to some advanced patterns throughout the React community.

During this article we will consider the situation of a React developer (You) building a Component for other developers.

We will see an overview of 5 patterns. For ease of comparison, we'll use an identical structure for each of them:

a. Pattern introduction

b. Code example (based on a simple Counter component)

The code is accessible on GitHub: advanced-react-patterns

c. Pros & Cons

d. Criteria. Based on two factors:

  • Inversion of control: Level of flexibility and control given to the developers using your component.

  • Implementation complexity: Difficulty for you and developers to implement the pattern.

e. Public libraries using the pattern in production.

1. Compound Components Pattern

This pattern makes possible the creation of expressive and declarative components while avoiding prop drilling. Consider using this pattern if you want a customizable component, with a better separation of concern and an understandable API.

Example

Github: compound-component

Pros

  • Reduced API Complexity: Avoid jamming all props in one giant parent component and drilling those down to child UI components. Instead, props are attached to the Counter's child that makes the most sense.

  • Flexible Markup Structure: Provide great UI flexibility by allowing the creation of various cases. For example, the developers can easily change the order of the Counter's children or define which one should be displayed.

  • Separation of Concerns: Most of the logic is centralized in Counter. A context (CounterProvider + useCounterContext) is used to share states (counter) and handlers (handleIncrement(), handleDecrement()) across Counter's children. This gives us a clear distribution of responsibilities.

Cons

  • Too much UI flexibility: Having this level of flexibility can also lead into situations not originally anticipated (examples: Unwanted code, Wrong Counter's children order, missing mandatory child). Depending on your use case, you might not want to allow that much flexibility.

  • Heavier JSX: This pattern will increase the number of JSX rows, especially if you use a linter (EsLint) or a code formatted (Prettier). It doesn't seem like a big deal at the scale of a single component, but could definitely make a huge difference when you look at the big picture.

Criteria

  • Inversion of control: 1/4

  • Implementation complexity: 1/4

Public libraries using this pattern

2. Control Props Pattern

This pattern turns your component into a controlled component. An external state is consumed as a “single source of truth” allowing the developers to insert their own logic to modify the default component behavior.

Example

Github: control-props

Pros

  • Give more control: Since the developers control the main state, they can directly influence the Counter behavior.

Cons

  • Implementation complexity: Before, a single integration (JSX) was enough to make the component work. Now it's spread over 3 different places (JSX / useState / handleChange).

Criteria

  • Inversion of control: 2/4

  • Implementation complexity: 1/4

Public libraries using this pattern

3. Custom Hook Pattern

Let's go further in “inversion of control”: the main logic is now moved into a custom hook. This hook exposes several internal logics (States, Handlers), which gives great control to the developers.

Example

Github: custom-hooks

Pros

  • Give more control: The developers can insert their own logic between useCounter and Counter, making it possible for them to modify the default Counter behavior.

Cons

  • Implementation complexity: Since the logical part is separated from the rendered part, it is up to the developers to link both. Therefore, a good understanding of how Counter work is necessary to implement it correctly.

Criteria

  • Inversion of control: 2/4

  • Implementation complexity: 2/4

Public libraries using this pattern

4. Props Getters Pattern

The Custom Hook Pattern provides great control, but it also makes the component harder to integrate because the developers have to deal with lots of native hook props and recreate the logic on his side.

The Props Getters Pattern pattern attempts to mask this complexity. We provide a shortlist of props getters instead of exposing native props.

A getter is a function that returns many props, it has a meaningful name, making it clear to the developers which getter corresponds to which JSX element.

Example

Github: props-getters

Pros:

  • Ease of use: The complexity is hidden. The developers just have to connect the right getter given by useCounter to the right JSX element.

  • Flexibility: Overloading the getter's props is possible to adapt to specific cases.

Cons:

  • Lack of visibility: getters brought abstractions which make the component easier to integrate, but also more opaque and “magic”. The developers must have a good understanding of the exposed getter props as well as the impacted internal logic to override them properly (Typescript should help with this).

Criteria

  • Inversion of control: 3/4

  • Integration complexity: 3/4

Public libraries using this pattern

5. State reducer pattern

The most advanced pattern in terms of inversion of control. It gives an advanced way for the developers to change how your component operates internally. The code is similar to Custom Hook Pattern, but with the addition of a reducer passed to the hook. This reducer can overload any internal action of the component.

Example Github: state-reducer

_State reducer pattern can be associated with other pattern (Compound components pattern, Custom hook pattern and Props Getters Pattern).

In this exemple we use it with the Custom hook pattern._

Pros

  • Give more control: In the most complicated cases, using state reducers is the best way to leave control to the developers. All the internal useCounter's actions are now accessible from outside and can be overridden.

Cons:

  • Implementation complexity: This pattern is probably the most complex to implement, both for you and for the developers.

  • Lack of visibility: Since any reducer's action can be changed, a good understanding of the component's internal logic is required.

Criteria

  • Inversion of control: 4/4

  • Integration complexity: 4/4

Public libraries using this pattern

Conclusion

Through these 5 advanced React patterns, we have seen different ways to take advantage of the concept of “inversion of control”. They give you a powerful way to create flexible and adaptable Components.

However, we all know that “Great power comes great responsibility”. The more control is given to the developers, the more your component will move away from the “plug and play” mindset. It's why you have to choose the right pattern corresponding to the right need.

The following diagram could help you in this task. It classifies all these patterns according to “Integration complexity” and “Inversion of control”:

This article was mainly inspired by the amazing work of Kent C. Dodds, who is a famous developer within the React community (https://kentcdodds.com/).

Thank you for reading.




Continue Learning