Communication Patterns in React

From basics to more complex things to have Alice finally talk to Bob!

Mathieu Eveillard
JavaScript in Plain English

--

Black, old phone device.
Photo by Quino Al on Unsplash

TL;DR

  • Props for parent->child communication
  • Callback props for child->parent communication
  • Common ancestor: does not scale, almost an anti-pattern
  • Event bubbling: implicit coupling, clearly an anti-pattern
  • Via the server: why not after all?
  • React Context, what else?
  • Building on React Context, let’s add some contracts
  • PubSub

React is now a mature environment for developing Single Page Applications (SPAs), with every tooling a professional developer is entitled to expect: starter projects, (structural) typing, linters, plus lots of well-maintained, production-ready libraries.

Still, one faces two main concerns when the codebase grows:

  • Composition. Its first purpose is to ensure that each component has one responsibility only, best known as SRP (Single Responsibility Principle). The second is to avoid writing the same code, again and again, known as the DRY injunction (Don’t Repeat Yourself). In React, there are only 4 composition patterns: direct invocation of a child component, higher-order components (HOC), the render-props pattern (which most people favour over HOCs), and hooks. Right.
  • But more components also mean additional techniques for having them talk to each other. Communication will be our focus today.

In this article, we’ll discuss the pros and cons of common communication patterns, then discuss the need for additional patterns.

First things first: props, callback props and common ancestor pattern.

The parent->child communication is straightforward thanks to React properties object, best known as “props”. message is one of these properties in the code sample below.

Conversely, the child->parent communication is ensured by callback props, i.e. a function provided by the parent (sendMessage(message: string): void) to be called by its child with the data of interest (message). Within the parent component, this data must live in a state (useState()) in order to be used.

This leads us very naturally to the Common Ancestor pattern, used for communication between descendants of a given component, starting with siblings (two children of the same component). Assuming Alice must communicate to her brother Bob, this pattern combines a callback property for communication from Alice to Parent, then a property for communication from Parent to Bob.

Alice --(callback property)--> Parent (state) --(property)--> Bob

This is quite systematic, but it obviously doesn’t scale. For first-degree relatives (siblings), this pattern leads to 1 callback property, 1 state and 1 property. For second-degree relatives (cousins), it’s already 2 callback props, 1 state and 2 props. And so on, for N-degree relatives, it’s N callback props, 1 state and N props. In other words, that’s the curse of props drilling and we should seek a better solution.

For the sake of comprehensiveness

Before jumping to additional, better suited for “long-distance” communication patterns, let’s mention render props. With a single render property (or whatever name is best suited), the parent component avoids to transmit a message property, since the corresponding data ("Hello World!") is used for the sole purpose of the render function. The more data is passed from parent to child for direct display, without further computation under the child’s responsibility, the more it makes sense to use the render props pattern.

You’ll also find the render props pattern under the following elegant, though quite uncommon “function as a child” variant. It is slightly restrictive, because it allows one render property only:

Any anti-patterns?

Yes… Event bubbling looks like a good candidate in this regard. Event bubbling describes the situation where a child component emits an event and does not capture it. One of its ancestors in the React tree does instead.

This pattern makes the coupling between components implicit: by looking at the sole Parent component, or likewise at the sole Child component, it’s not obvious that communication is happening. And if Child is moved outside of its Parent tree, everything breaks. Furthermore, events are very limitative when it comes to transmit data. Often, the only information available is the existence of the event itself…

Things get even worse with native DOM event bubbling. This relates to the same situation as above, but events are native DOM events, not React events (React.ReactSyntheticEvent).

Why is it worse? Because the whole point of React is the Virtual DOM, a level of abstraction on top of the native DOM. Essentially, the Virtual DOM offers a declarative API for manipulating the DOM, instead of the native, imperative API. Hence communicating between Virtual DOM components through native DOM events mixes levels of abstraction and goes against the principle of working always at the same level of abstraction.

More generally, when later introducing additional communication patterns, we’ll pay particular attention to always relying on the Virtual DOM.

Keep it simple, stupid!

Back to our main concern: how to ensure communication between distant components? By “distant”, I mean two components where none is a descendant of the other: they have a common ancestor, but it’s too high in the tree for the Common Ancestor pattern to apply.

Well, in such a case, communicating via a server is a solution that should be considered more often. The basic schema is:

1. Alice sends data to the server;
2. Later, Bob fetches the data from the server.

In the example below, the late fetch is triggered by the user, but the pattern would also work with a push notification from the server (WebSockets).

If generalised, this pattern leads the application state to live mainly on the server instead of the browser. Hence, though pragmatic, this path may question the very pertinence of a single-page application, as opposed to a regular application. As always, the business constraints help when it comes to make an architectural decision: if many users may update the same piece of data, the state must live on the server anyway.

Now, let’s build on React Context

Finally! Ok. Let’s stop beating about the bush and consider the usual suspect with respect to our concern: React Context.

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

That’s an interesting foundation to build on. One could wish for 2 improvements:

  • The capacity to update only a subpart of the object provided as context and still benefit from change detection (change detection is triggered when a new referenced is provided <MessageContext.Provider value={...}>);
  • Some kind of contractualisation, because the context object is the place where the application state will live. It has to be valued as a golden source, a single source of truth for all components.

That’s the point where the Flux architecture was introduced, followed by Redux, MobX and others. But Redux is much more than that, because it comes with event sourcing.

So let’s rewind slightly up and build the simplest Store object. The Store is the gate that protects the state. We constrain read and write operations via a getState(): State and a setState(reducer: Reducer<State>): void methods.

One shall note that getState(): State offers no protection: the return state can be mutated by mistake. Without a library that enforces immutability (think Immutable or Immer), immutability relies on a gentlemen’s agreement.

Then choices have to be made for updating the state. Two options arise here:

  • Direct update: setState(state: State): void;
  • Update via a Reducer (state: State): State and setState(reducer: Reducer): void.

The direct update has at least two drawbacks.

First, since only a part of the state is to be updated, the current state must be retrieved first, then spread. This logic const state = getState(); is repeated at each update, that we want to avoid:

const state = getState();
const newState = {
...state,
// override some attributes
};
setState(newState);

But more deeply, using a Reducer allows to chain and batch updates before a single re-render, which the first option does not.

There’s much code below and you can jump safely to the next code sample, which shows the Store in action. For those who want to dig further, the tricky point here is to allow change detection when the state is updated, while the whole Store object must not change. Here we rely on a simple renderIndex used as key.

Again, if the code above doesn’t make sense for you now, it’s definitively ok. More important is the Store in action:

This kind of contractualisation opens the door for any additional pattern one judges useful. That precisely we will do by exploring a PubSub pattern.

Publish/Subscribe (PubSub)

This pattern is well known and was even used on the Front-End side by Backbone. Many libraries, starting with PubSubJS, postal.js and EventEmitter, provide good implementations, in plain JavaScript. But as stated above, we want our implementation to work at the same level of abstraction React does, not lower. Below, Channel and PubSub are plain JavaScript classes, then we use the React APIs (Context, HOC and hooks) to make them available inside React components.

The whole point of Publish/Subscribe is decoupling, or let’s say loose coupling. Communication happens through an event bus. Thus, a publisher doesn’t know who it talks to, and conversely, a subscriber doesn’t know from whom an event originates.

Publisher --> Event bus --> Subscriber

Under the hood, it relies on the Observer pattern. Wait…what? This means tight coupling, right? Right: the subject maintains a record of its observers. Good point, so let’s make things clear: the observer pattern takes place between the event bus and the subscribers, not between publishers and subscribers:

Publisher --> Event bus --(observer pattern)--> Subscriber

By the way, other implementations would work, starting with polling.

Hmmm… Lots of talking and still no code! So here it is:

And voilà!

This, of course, is only a gist, not a production-ready implementation. For the sake of simplicity, it is synchronous, while it is generally implemented in an asynchronous fashion in the “real world”. But, after all, what would be the use of asynchronicity for communication between components, in the browser?

This offers a fairly simple API:

For the sake of comprehensiveness (again)

Again, for the sake of comprehensiveness, let’s mention React portals. The official documentation states:

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

In other words, it allows a parent-child relationship in the Virtual DOM, while their materialisations in the DOM (the rendered elements) live in different trees.

All the communication patterns already seen apply: props, callback props, context, etc.

That’s very useful in specific use cases, like modals and notifications, still one cannot envision it as a systematic communication pattern.

Here we are! This article provided an overview of React’s communication patterns between components. Most of them in fact do apply in other libraries and frameworks (Vue.js, Angular…)

I’d love to hear your feedback, especially regarding use cases of the PubSub pattern: did you meet any situation where it would have been useful?

Thanks for reading!

Further readings

More content at plainenglish.io

--

--