React & RxJS: Pushing state down

Kyle Hughes
disney-streaming
Published in
6 min readOct 28, 2021
Image Credit: onrec.com

Intro

State management in React is a difficult problem to solve because finding the right place to manage state is challenging. Many patterns typically involve pushing state further away from the component(s) actually using the piece of state in order to share state between component trees.

But if we could somehow keep certain one-off states closer to the component using it without bloating parent components? This is where RxJS comes in.

This article will compare a few common ways to manage and share state and then will introduce RxJS and demonstrate how it can be used to push state down by using subscriptions.

State management

When designing dynamic components, state will be needed somewhere but the following questions arise:

Where should this state live?

What other component needs this state?

How can I share this state with other components?

Once we know which components are dependent on a particular piece of state, we can then determine how to share it. Components are built in a tree hierarchy, so the natural way to share state is to pass it down the component tree to those who need the state to each component as props. There is no problem with this approach but as applications grow, teams may find it cumbersome to pass state down through each component in the tree to its consumer. Luckily when this happens, there are two common patterns typically used to remove all of the prop passing: Context API and Redux.

The Context API works by wrapping a component in what is known as a Provider. State can then be managed using the Provider, allowing each child component the ability to consume the state directly. Redux works in a similar fashion but manages the state in a global store. With the global nature of Redux, any component can consume state managed in the store. Further, unlike the Context API, Redux is global so it is not necessarily coupled to any tree of components. Read on to see examples of these patterns.

A case for local state

Why can local state be easier to manage?

When looking at a component, it provides all the context needed to understand what is going on in a particular component without needing to dig through related components to find where and how the state is being set.

Every case is different. But as state is pushed up the tree further away from the components depending on it, it requires the extra mental model of understanding the full component tree. Pushing state up will always require more flexibility as new features come in, but when state is local to the component using it, maintenance can be improved.

Example — Where should state live

Let’s take a look at a fairly common occurrence — state needing to be shared between components in different tree structures. Let’s assume the Notification component below at some point wants to render a notification triggered by FormComponent.

Example 1: Notification triggered by FormComponent

Approach 1 — Props

Since the components needing to communicate live in two different component hierarchies, the first approach would be to define this state in the parent component of both which in this case is App. Then we can send the appropriate props down and through each component until the notification state reaches Notification and the setNotification reaches FormComponent. With this approach every component needs to have reference to either notification or setNotification even though they may not use them. Don’t like all of that drilling? Let’s see how the Context API can help.

Approach 2 — Context

The state will still need to live in the App component since it is the parent shared by both components. The main difference here is that drilling down the props is unnecessary. Notification & FormComponent can now read directly from the values managed in the Provider.

Example 2: Context approach

Approach 3 — Redux

The previous two approaches both have the state living in the App component. With the Redux approach, the state will now be moved into the store which can then be consumed from anywhere in the application. There is some extra boilerplate with this approach to update the state but overall it gives a nice solution to update state in a deterministic way with pure/reducer functions. Notification and FormComponent will eventually end up looking similar to the Context solution.

Example 3: Redux approach

Local state approach

Each approach above has the notification value maintained by another component and passed down to Notification in some way or another. These approaches work very well but for arguments sake, assuming the Notification component was the only component using the value, how could the notification state be pushed down into the only component using it?

That’s where RxJS comes in!

What is RxJS

RxJS or Reactive extensions for Javascript, is a library providing a way to compose event streams (https://www.learnrxjs.io/learn-rxjs/concepts/rxjs-primer) and essentially is a way to implement the observer pattern. Popular in the Angular community, it doesn’t seem to have received the same level of interest in the React world but can just as easily be utilized. Let’s go over some of the concepts that will be discussed in the example to follow.

Observables

These are the main building blocks of RxJS. They essentially provide a stream of values to a subscriber.

They can be considered the read-only side of the stream as they only expose subscribe/unsubscribe methods.

https://rxjs-dev.firebaseapp.com/guide/observable

Subjects

A special type of Observable that maintains a list of subscribers. In addition to being an Observable, it is also used to push values onto the stream by exposing a next(value) method which then multicasts the value to all subscribers.

https://rxjs-dev.firebaseapp.com/guide/subject

With Subjects composing the read & write side of a stream, they can be used to create direct connections between components!

Unlike Redux & the Context API, where state is essentially pushed up the tree, to be consumed further down, RxJS streamlines things greatly. With RxJS we cut out the extra dependencies and go directly from component to component with one simple indirection.

Example — RxJS

Using the same example from above, let’s use RxJS and see how we can push the notification state down into the Notification component and communicate between Notification & FormComponent.

First let’s create a Subject by encapsulating it in a NotificationService. This is the only indirection needed to connect the two components.

Example 4: RxJS — Creation of a Subject

In this example the entire Subject is not exposed, only the Observable.This gives components the chance to separate their concerns by being more explicit in how they interact with the stream.

  • read — subscribe to the observable
  • write — invoke publishNotification

With the stream now in place, let’s take a look at the FormComponent — the write side:

Example 5: RxJS — Publish event to stream

By exposing a simple function, the write side of the stream stays relatively simple and doesn’t need to know about any RxJS concepts. Invoking publishNotification will emit a value to all of the components subscribed to the observable. The read side has a little more going on, so let’s see how the Notification would read the value emitted from the form.

Example 6: Notification reading value from FormComponent

We import the notificationObservable and will eventually need to subscribe to the stream to read the emitted values but first a little information on the useEffect hook. The useEffect hook allows a component to run a piece of code when the component first mounts to the DOM or if a value in the dependency array changes. In the example above, the dependency array is empty []. This means the code inside of this hook will only execute once, on the first mount of the component.

Since we know this code will only run once, this is a great place to subscribe to the observable and guarantee there is only one subscription per component. Inside of the subscription, the notification state can now read the values emitted through the stream. It is important to note that this subscription needs to be cleaned up in the event the component is unmounted from the DOM.

The useEffect hook allows code to be run when un-mounting from the DOM by providing a callback function. Unsubscribing from the observable when un-mounting is needed to prevent the a component from keeping duplicate subscriptions in memory.

Thus, the only component responsible for maintaining notification state is the Notification component itself!

Conclusion

Not every piece of state can be local, but RxJS can move some of those one-off states previously added to Context or Redux into local state for easier management. Should RxJS replace all of the other state management libraries? Not necessarily. But because RxJS has the flexibility of being used with React, it can be integrated fairly quickly and provides another way to solve the state management problem without needing to worry about component trees.

--

--