Research on Reactive-Ephemeral State Management

Table of Contents

As this is too much text I'm afraid the important things at the end will get lost, I put it here and quote one of Richard Feynman's rules he stuck to when teaching:

Give credit where it's due Richard Feynman

  • @ngrx_io - that listened to my questions and gave me useful feedback

  • @yjaaidi and @niklas_wortmann - may be the only 2 persons on earth that read ALL THAT

  • @juristr - that pretended he will use it in his projects

  • @mvmusatov - that sent a PR and fixed my messy demo :D


In most component-oriented applications, there is a need to structure container components. Even after a well-thought refactoring into more display components and grouping logic into responsibilities, it's always hard to handle.

The data structure you manage inside these components is only here for their very component. Not for any other components. This data structure appears with the component and disappears when the component is removed.

This is a good example of an ephemeral state.

If you have a well thought and structured approach on how to manage ephemeral state, such components get a breeze to write. You could master a fully reactive architecture in a scalable and maintainable way.

This article provides you with some fundamental information about my findings in reactive ephemeral state management. It is applicable to every framework that is component-oriented and has some life cycle hooks for creation and destruction.

The below examples are done with Angular as a framework, as it has DI built-in, which comes in handy here. As a Reactive programming library with cold Observables by default, I picked RxJS as it is well supported.

reactive-local-state-intro michael-hladky

TL;DR

If you are into reactive programming, you will learn about some cool topics.

  • unicast vs. multicast

  • hot vs. cold

  • subscription-less components

  • higher-order operators like mergeAll

If you are also into ephemeral state management, you can learn how to detect it:

We defined 3 rules of thumb to detect ephemeral state

  • No horizontal sharing of state

  • The lifetime of the state is dynamic

  • It processes local relevant events

Your understanding of the fundamental implementation will get a good boost! Examples of how to introduce different architecture patterns in your component structure are only the tip of the iceberg:

  • Initiation and coupling state to e.g. a component

  • Interaction

  • derivation of state

are just some of the nitty-gritty details included!

Here are the Important resources:

Tackling Component State

Methodology

reactive-local-quote-gang-of-four michael-hladky

If you go back in history, you will find almost all our nowadays "cutting edge problems" already solved. When I realized for the first time that life is a "constant evolutionary repetition" I started to change my strategy on solving problems.

Before I almost always started to implement a half-backed cool idea which I was certain is most up to date with technologies.

After I made many mistakes (without them I would not be here today) and got some incredibly helpful insights, I now started to change my way of solving problems completely. Let me quote The Gang Of Four to give you the first glimpse of my fundamental changes in how I approach problems:

So here is what the gang of four says about Object-Oriented-Software-Design-Patterns:

If you stick to the paradigms of object-oriented programming, the design patterns appear naturally.

With that in mind, I did not work on a solution for ephemeral state management, but tried to look at all the different problems that we will face when managing any local state in general, with the hope, in the end, a solid solution will appear naturally.

Let's start with the first chapter and some general information to get you on track.

Layers of State

Of course, there are WAY more, but in this article, I will introduce 3 layers of state:

  • (Persistent) Server State

  • Persistent Client State (Global State)

  • Ephemeral Client State (Local State)

reactive-local-state layers-of-state michael-hladky

Persistent Server State is the data in your database. It is provided to the consumer over a data API like REST, GraphQL, Websocket, etc. This is very different from Meta State, which is information related to the status of a resource that provides us with a state. E.g., Loading, Error, Success, etc.

For persistent and ephemeral client states, I will try to use simpler wording. I will use Global State for the persistent client state and Local State for the ephemeral client state. Both live on the client, but they demand a completely different way of treatment.

In this article, I want to focus on the ephemeral state.

What is the ephemeral state?

reactive-local-state ephemeral-state michael-hladky

The ephemeral state is just one of many names for data structures that needed to be managed on the client under special conditions. Other synonyms are a transient state, UI state, local state, component state, short-term data, etc...

It is the data structure that expresses the state of an isolated unit, like, for example, a component in your application.

As the word "isolated" is a bit vague, let me get a little bit more concrete.

It's the state that lives in your components, pipes, directives and some of the services that are created and destroyed over time. The state is not shared between siblings and is not populated to global services.

Global vs Local Accessibility of Data Structures

The term global state is well-known in modern web development. It is the state we share globally in our app e.g., a @ngRx/store or the good old window object ;)

This is in this article called persistent state.

reactive-local-state global-accessible michael-hladky

As we can see, one global source distributes state to the whole app.

If we compare this to a local state, we see that this data structure is provided and managed only in a certain time frame of your app. For example, in a component or directive.

reactive-local-state local-accessible michael-hladky

This is our first rule of thumb to detect local state:

No horizontal sharing of the state e.g. with sibling components or upwards.

Static vs Dynamic Lifetime of Data Structures

In Angular global state is nearly always shared over global singleton services. Their lifetime starts even before the root component. And ends after every child component. The state's lifetime is ~equal to the Apps lifetime or the browser windows lifetime.

This is called a static lifetime.

reactive-local-state lifetime-global-singleton-service michael-hladky

If we compare this to the lifetime of other building blocks of Angular, we can see their lifetime is way more dynamic.

reactive-local-state lifetime-angular-building-blocks michael-hladky

State in these building blocks is tied to the lifetime of their owners and their hosts, and if shared, this state is shared then only with children.

The best example of a dynamic lifetime is data that gets rendered over the async pipe.

reactive-local-state lifetime-async-pipe michael-hladky

The lifetime depends on the evaluation of the template expression, a potential *ngIf that wraps the expression or e.g., a directive.

For our second rule of thumb we detected for the local state is:

The lifetime is dynamic e.g. bound to the lifetime of a component or an async pipe

Global vs Local Processed Sources

Where our global state service nearly always processes remote sources:

  • REST API's HTTP, HTTP2

  • Web Sockets

  • Browser URL

  • Browser Plugins

  • Global Static Data

  • The window object

And the logic is located in the more abstract layers of our architecture.

reactive-local-changes processing-global-sources michael-hladky

Code dedicated to the local state would nearly always focus on the process of the following sources:

  • Data from @InputBindings

  • UI Events

  • Component level Side-Effects

  • Parsing global state to local

reactive-local-changes processing-local-sources michael-hladky

The third rule of thumb to detect local state is:

It processes mostly local sources e.g. sort/filter change


Recap Ephemeral State

We defined 3 rules of thumb to detect ephemeral/local state

  • No horizontal sharing of state

  • The lifetime of the state is dynamic

  • It processes local relevant events

Some real-life example that matches the above-defined rules are:

  • sorting state of a list

  • form errors

  • state of an admin panel (filter, open/close, ...)

  • any dynamic appearing data

  • accumulations from @Input data

  • extended global state or derived global state for a container component

You rarely share this data with sibling components, it only shares data structures only locally and focuses mostly on local sources. In other words, there is no need to use many of the concepts of global state management libraries e.g., actions.

Still, we need a way to manage these data structures.

Problems to Solve on a Low-Level

As a first and foundational decision fact, we have to know we work with a push-based architecture. This has several advantages but, more importantly, defines the problems we will run into when implementing a solution.

As we defined the way how we want to distribute our data, let me list a set of problems we need to solve.

Timing

As a lot of problems I ran into in applications are related to timing issues, this section is here to give a quick overview of all the different things to consider.

reactive-local-state timing-component-lifecycle michael-hladky

Shouldn't reactive code be by design in a way that timing of certain things becomes irrelevant?

I mean, not that there is no time in observables or that it does not matter when we subscribe to something, but when we compose observables, we should not care about when any of our state sources exactly emit a value...

In a perfect reactive setup, we don't need to care about those problems. However, as Angular is an object-orientated framework, we often have to deal with different problems related to the life cycles of components and services, router events, and many more things.

In RxJS timing is given by the following:

  • For hot observables, the time of creation

  • For cold observables, the time of subscription

  • For emitted values, the scheduling process

In Angular, timing is given by the following:

  • For global services, the creation as well as the application lifetime

  • For components, the creation, several life-cycle hooks as well as the component lifetime

  • For local services, the creation of the component as well as the components lifetime

  • For pipes or directives in the template, also the components lifetime

All timing relates things in Angular are in an object-oriented style, very similar to hot observables. Subscription handling can be done declaratively over completion operators. The scheduling process can be controlled both over imperative or over operators and can influence the execution context of the next error or complete callback.

We see that there are two different concepts combined that have completely different ways of dealing with timing. Angular already solved parts of these friction points, but some of them are still left, and we have to find the right spots to put our glue code and fix the problem.

angular-timeline michael-hladky

This chart shows a minimal Angular app with the different building units and their timing: In this example, it marks:

  • the global store lifetime

  • the component store lifetime

  • the async pipe lifetime

As we can see, It makes a big difference where we place observables and where we subscribe to them. It also shows where we need hot observables and where we need to replay values.

Subscription Handling

Let's discuss where subscriptions should take place and for which reason they are made.

Subscriptions are here to receive values from any source, cold or hot.

In most cases, we want to render incoming values to the DOM.

For this reason, we use a Pipe or a Directive in the template to trigger change-detection whenever a value arrives.

The other reason could be to run some background tasks in a ComponentDirective or Service, which should not get rendered. E.g., a request to the server every 30 seconds.

As subscriptions in the Pipe or Directive are handled over their life-cycle hooks automatically, we only have to discuss the scenarios for side effects.

Let's take a quick look at the diagram from before:

lifecycle michael-hladky

So what could be a good strategy related to the timing of subscriptions and their termination?

One way to solve it would be to subscribe as early as possible and unsubscribe as late as possible.

On a diagram, it would look like that:

reactive-local-state subscription-handling michael-hladky

(used RxJS parts: timertaptakeUntilSubject)

We already have a declarative subscription handling. But this code could get moved somewhere else. We could use the local service that we most probably will need if we implement the final implementation for component state handling.

Service

Component

In this way, we get rid of thinking about subscriptions in the component at all.

Sharing State and State Derivations

In many cases, we want to subscribe to more than one place to some source and render its data. Even with such a simple operation as retrieving and displaying the data several things needs to be considered.

The interesting parts here are the data structure and derivation logic.

We will skip the data structure and focus on the things related to RxJS. The derivation of data.

Uni and multi-casting with RxJS

As we have multiple sources, we calculate the data for every subscription separately. This is given by the default behavior of RxJS. It is uni-cased by default.

reactive-local-state uni-case-vs-multi-cast michael-hladky

Basic uni-cast examples:

(used RxJS parts: Observable)

If we want to multicast the values, we could do something like that:

Basic multi-cast examples:

We see how we can share data, now let's take a look at operations:

reactive-local-state uni-case-vs-multi-cast-operators michael-hladky

Operators uni-cast examples:

(used RxJS parts: map)

Do you remember the subscriber function we saw in the first example with the observable? Operators internally maintain a similar logic. We apply an operator the inner subscriber functions are chained. This is the reason we see the log for the transformation 2 times. For every subscriber one time.

There are also operators that help to add multi-casting in operator chains.

Operators multi-cast examples:

(used RxJS parts: share)

Sharing Work

With this knowledge, let's take a look at some examples:

In our view, we could do some processing for incoming data. An example could be an array of items from an HTTP call.

reactive-local-state uni-case-vs-multi-cast-work michael-hladky

The work that is done in mapServerToClient is executed once per subscription. In our example 2 times. Even if we change the HTML and use ng-container to maintain only one subscription in the template, in the class there could be multiple other subscriptions we can't solve in the template.

To save work, we need to share the subscription.

(used RxJS parts: shareReplay)

Here we use shareReplay to cache the last value, replay it, and share all notifications with multiple subscribers.

Sharing Instances

This is a rare case but important to know if you work fully reactive.

To start this section, let's discuss the component's implementation details first. We focus on the component's outputs.

Let's take a closer look at the EventEmitter interface:

And Subject looks like this:

The important part here is that we can pass everything that holds a subscribe method.

Which means the following would work:

(used RxJS parts: interval)

An observable, for example, provides a subscribe method. Therefore we can directly use it as a value stream for our @Output binding.

Knowing that enables us to take some very nice and elegant shortcuts as well as reducing the amount of code we need to write.

With this in mind, let's focus on the original problem, sharing work and references.

In this example, we receive a config object from the parent component, turn it into a reactive form and emit changes from the form group created out of the config object.

Every time we receive a new value from the input binding, we:

  • create a config object out of it

  • use the FormBuilder service to create the new form. As an output value, we have to provide something that holds a subscribe method. So we could use the form group valueChanges to provide the form changes directly as component output events.

(used RxJS parts: startWith)

If we run the code, we will see that the values are not updating in the parent component.

We faced a problem related to the fact that nearly all observables are cold, which means that every subscriber will get its instance of the producer.

You might be even more curious now, as our source that produces the formGroup is a ReplaySubject. Which are multi-casting values and sharing one producer with multiple subscribers...

What we forgot here is that our formGroup$ observable ends with a map operator, which turns everything after it again into a uni-cast observable.

So what happened? We subscribed once in the template over the async pipe to render the form. And another time in the component internals to emit value changes from the form.

As we now know that the map operator turned everything into a uni-cast observable again, we realize that we created a new FormGroup instance for every subscription. The subscription in the template, as well as the subscription, happens internally over @Output().

This can be solved by adding a multicast operator like share or shareReplay at the end of formGroup$.

As we also have late subscribers, the async pipe in the template, we use shareReplay with bufferSize 1 serve them the actual formGroup instance.

shareReplay emits the same value to subscribers.

So the subscription in the template and the subscription in the component's internals receive the same instance of FormGroup.

Important to notice here is that shareReplay is cold but multicast. This means it only subscribes to the source if at least one subscriber is present. This does not solve the problem of cold composition, but it is fine to share specific work or, in this case, a reference.

Later in this article, we will remember this problem to provide a way to share work instances.

The Late Subscriber Problem

In this section, I faced the first time a problem that needed some more thinking.

Incoming values arrive before the subscription has happened.

For example, state over the @Input() decorator arrives before the view gets rendered and a used pipe can receive its value.

We call this situation a late subscriber problem. In this case, the view is a late subscribe to the values from @Input() properties. There are several situations from our previous explorations that have this problem:

  • Input Decorators

    • transporting values from @Input to AfterViewInit hook

    • transporting values from @Input to the view

    • transporting values from @Input to the constructor

  • Component and Directive Lifecycle Hooks

    • transporting OnChanges to the view

    • getting the state of any life cycle hook later in time (important when hooks are composed)

  • Local State

    • transporting the current local state to the view

    • getting the current local state for other compositions

A quick solution here would replay the latest notification to use a ReplaySubject with bufferSize 1. This would cache the latest emitted value and replay it when the async pipe subscribes.

Primitive Solution

(used RxJS parts: ReplaySubject)

This quick solution has 2 major caveats!

First Caveat: The downside here is that we can only replay the latest value emitted. Replaying more values would cause problems for later compositions of this stream, as a new subscriber would get all past values of the @Input Binding. And that's not what we want.

More important here is the fact that we push workload to the consumer. We can not assume everybody adopts that.

If we made every source replay at least the last value, we would have to implement this logic in the following places:

  • View Input bindings (multiple times)

  • View events (multiple times)

  • Other Service Changes (multiple times)

  • Component Internal interval (multiple times)

It would also force the parts to cache values and increase memory. Furthermore, it would force the third party to implement this too.

IMHO, not scalable.

reactive-local-state-sate-subscriber-replay-caveat-workload michael-hladky

Another downside is the bundle size of ShareReplay. But it will be used anyway somewhere in our architecture, so it's a general downside.

Second Caveat: The second and more tricky caveat is composition is still cold. We rely on the consumer to initialize state composition.

reactive-local-state-sate-subscriber-replay-caveat-cold-composition michael-hladky

Cold Composition

Let's quickly clarify hot/cold and uni-case/multi-cast.

First, let's remember what we learned about uni- and multi-casting in the earlier chapter.

Uni-cast. The producer is unique per subscription. Any creation operator is uni-cast (publish operators are not yet refactored to creation operators, but they would be the only exception.) interval, for example, would call setInterval for every subscriber separately.

Multi-cast. The producer is shared over all subscriptions. Subject, for example, emits its value to multiple subscribers without executing some producer logic again.

Cold. The internal logic of the observable is executed only on subscription. The consumer controls the moment when internal logic is executed by the subscribe is called. The interval creation operator, for example, will only start its internal tick if we subscribe to it. Also, nearly every pipe-able operator will execute only if we have an active subscriber.

An interesting example for a cold operator is share. Even if it multi-casts its notifications to multiple subscribers, it will not emit any notification until at least one subscriber is present.

So it's cold at the beginning but multi-cast after the first subscriber. :)

Hot. The internal logic is executed independently from any consumer. Subject, for example, can emit values without any present consumer.

There is also an operator that turns all the above logic into a hot path. multicast and every publish operator returns a ConnectableObservable. If we call connect on it, we connect to the source and start to execute the logic and all the operators in between publish and its source observable.

reactive-local-state-hot-cold unicast-multicast

With this in mind, we can discuss the problem of cold composition in the case of our local state.

As we will have to deal with:

  • View Interaction ( button click )

  • Global State Changes ( e.g., HTTP update )

  • Component State Changes ( triggered internal logic )

Putting all this logic in the component class is a bad idea. Not only because of the separations of concerns but also because we would have to implement it over and over again.

We need to create the logic that deals with the problems around the composition in a way that can be reused and is independent!

So far, our sources got subscribed to when the view was ready and we rendered the state. As the input from the view is a hot producer of values and injected services too, we have to decouple the service that handles component state from other sources.

So what is the problem?!!

We have hot sources and we have to compose them. As we already learned in the section sharing work and instances, nearly every operator returns a cold source. No matter if it was hot before or not.

If we compose state, we have to consider that our scan operator returns a cold observable.

So no matter what we do before, after an operation we get a cold observable, and we have to subscribe to it to trigger the composition. I call this situation a cold composition.

Some of our sources are cold. This can be solved in two ways:

  • a) Make all sources replay at least their last value (push workload to all relevant sources)

  • b) Make the composition hot as early as possible (push workload to the component-related part)

We discuss a) already in the previous section. This solution pushed the workload to all involved parties, and the initiation will still be controlled by the consumer.

What would be the scenario with b)?

We could think of the earliest possible moment to make the composition hot. From the diagram about lifecycle hooks and the different instances in Angular, we know that a service, even if locally provided, is instantiated first, before the component.

If we put it there, we could take over the workload from:

  • View Input bindings (multiple times)

  • View events (multiple times)

  • Component Internal interval (multiple times)

  • Locally provided services

  • Global services

We could also get rid of the dependency on their subscription.

Let's see a simple example where we rely on the consumer to start the composition.

Service:

(used RxJS parts: scan)

In this service, we could try to solve our problem by using:

  • share()

  • shareReplay({refCount: true, bufferSize: 1})

  • shareReplay({refCount: false, bufferSize: 1})

Component:

If we run the code and click the button first and then open the result area, we see we missed the values emitted before opening the area.

No matter which of the above ways we try, nothing works. We always lose values if no subscriber is present.

reactive-local-state-sate-subscriber-replay-cold-composition-problem michael-hladky

Even if the source is hot (the subject in the service is defined on instantiation), the composition over scan made the stream cold again. This means the composed values can be received only if there is at least 1 subscriber. In our case, the subscriber was the component's async pipe in the template.

Let's see how we can implement the above in a way we could run hot composition:

reactive-local-state-sate-subscriber-replay-cold-composition-solution michael-hladky

Hot Composition Service:

(used RxJS parts: publishReplaySubscription)

We kept the component untouched and only applied changes to the service.

We used the publishReplay operator to make the source replay the last emitted value by using 1 as bufferSize.

In the service constructor, we called connect to make it hot subscribe to the source.

Subscription-Less Interaction with Component StateManagement

So far, we only focused on independent pieces and didn't pay much attention to their interaction. Let's analyze the way we interact with components and services.

Well-known implementations of state management like @ngrx/store in Angular, which is a global state management library, implemented parts of the consumer-facing API imperatively. Also, all implementations of REDUX in React did it like that.

The provided method is dispatch which accepts a single value, an action, that gets sent to the store.

Let's look at a simple example:

Imperative Interaction Service

Imperative Interaction Component

Why is this imperative? Imperative programming means working with instances and mutating state. Whenever you write a setter or getter your code is imperative, It's not composable.

If we now think about the dispatch method of @ngrx/store, we realize that it is similar to working with setter.

While in this example, it sits inside another un-compose-able thing, the instance method and therefore is ok. Everything else would resolve in more refactoring.

However, we cannot use it to work with composable sources.

Let's think about connecting RouterState or any other source like ngrx/store to the local state:

Imperative Interaction Component

As we can see, as soon as we deal with something, compose-able setters don't work anymore. We end up with very ugly code. We break the reactive flow, and we have to take care of subscriptions.

reactive-local-state-declarative-interaction-breaking-flow michael-hladky

But how can we go more declarative or even reactive? By providing something compose-able :)

Like an observable itself. :)

By adding a single line of code, we can go fully declarative as well as fully subscription-less.

reactive-local-state-declarative-interaction-connector michael-hladky

Declarative Interaction Service

(used RxJS parts: mergeAll)

Declarative Interaction Component

Let's take a detailed look at the introduced changes:

  1. In StateService we changed stateSubject = new Subject<{ [key: string]: any }>(); to stateSubject = new Subject<Observable<{ [key: string]: any }>>(); It now accepts Observables instead of state objects.

  2. In StateService we added the mergeAll() operator to our state computation logic.

  3. In StateService we replaced the setState method:

that took a single value with connectState:

By providing the whole observable, we can handle all related mechanisms of subscription handling, as well as value processing and emission in the service itself, and hide all this away from others.

reactive-local-state-declarative-interaction-connector-code michael-hladky

We now have not only way less and maintainable code but also a "subscription-less component".

This simple change will enable us to create way more than just subscription-less components. But this document is dedicated to the very fundamentals.

As a last additional benefit in this section, we can talk a little bit about side-effects:

Subscription-Less Handling of Side-Effects

If we recap the above snippet, we can see that we not only introduced a subscription-less way for state management interaction, but also a very elegant way to handle side-effects over our service.

Let's implement some more lines of code:

Service

Declarative Interaction Component

(used RxJS parts: publish)

Note that the side-effect is now placed in a tap operator and the whole observable is handed over.

Recap Problems

So far, we encountered the following problems:

  • sharing work and references

  • subscription handling

  • late subscriber

  • cold composition

  • moving primitive tasks as subscription handling and state composition into another layer

  • Subscription-less components and declarative interaction

As you may already realize, all the above problems naturally collapse into a single piece of code. :)

Also, if you remember from the beginning, this is what "the gang of four" quote says about Object-Oriented Design Patterns. :)

We can be happy as we did a great job so far. We focused on understanding the problems, used the language-specific possibilities the right way, and naturally, ended up with a solution that is compact, robust, and solves all related problems in an elegant way.

Let's see what the local state service looks like.

reactive-local-state-first-draft michael-hladky

Basic Usage

Service Design

State Logic

Service Implementation

Now let us see some minimal examples of how to use the service:

Extending the service

Injecting the service

Service Usage

Now let's see some basic usage:

Connecting Input-Bindings

Connecting GlobalState

Selecting LocalState

(used RxJS parts: withLatestFrom)

Handling LocalSideEffects

This example shows a material design list that is collapsable. It refreshed data every n seconds of if we click the button. Also, it displays the fetched items.

Basic Example - Stateful Component*:

This shows some fundamental interaction for template global state and ephemeral state. The next snippet shows how you would implement architecture patterns based on that service. In this case I picked a simple implementation of the MVVM design pattern.

Basic Example - Design Pattern MVVM*:

The lase example showed how MVVM in implemented based on the reactive state class. What is interesting here is that the template only accesses the ViewModel, nothing else.

But this is part of another document I would suggest. ;p

Summary

How to differ global from the ephemeral state:

  • No horizontal sharing of state

  • The lifetime of the state is dynamic, bound to e.g. a component

  • It processes local relevant events

If we take a look at our operator reference list at the end of this document we can see it was a lot about:

  • unicast vs. multicast

  • hot vs. cold

The main outcome here was we should ensure that the moment of computation of states is not controlled by the subscriber. It should be hot.

We learned how to can have a fully reactive flow with

  • higher-order operators like mergeAll The combination with our logic bound to a certain life-time enabled us to create

  • subscription-less components

An example implementation of our learning can be found in the resources.

Based on that we used in a minimal example and also made the first test with some design patterns like MVVM.


Resources

🎥 Angular Vienna, Angular, and RxJS - Tackling Ephemeral State Reactively

💾 research-on-reactive-ephemeral-state-in-component-oriented-frontend-frameworks