Implementing a Metamatic State Container

Different Strategies of Connecting Components to the Single Source of Truth

Posted by Heikki Kupiainen/Oppikone on 09.08.2018 09:18:45

In an earlier article, I introduced the Metamatic framework, which is a toolset of utility functions to implement a clean and easy-to-use state container in a JavaScript-based application.

In this article I am going to discuss different architectural approaches to implement a state container.

There are two major strategies to implement a state container, which are the Two-Way-Events strategy and the One-Way-Events strategy. The Two-Way-Events strategy means that the central data container (MetaStore) communicates with the rest of the software only through events. In Two-Way-Events strategy, the state container uses events for both sending and receiving data. It listens for events for receiving data and dispatches events for sending data. But in One-Way-Events strategy instead, events are used only for broadcasting. The State Container does not listen for events to receive data. The data is placed inside the container directly through setter or update method invocations from outside.

The Two Way Events State Container Strategy

In the Two-Way strategy, the events flow in two directions. They flow downstream, from the state container downward to the UI components. And they flow upstream, from components upward back to the container. Downstream flow happens when the state container fires an event. The event is being handled down the stream in the UI components that listen for the events from the State container. And when they flow upstream, from the UI components back to the state container, when a UI component dispatches an event back to the State Container.

With Metamatic, both Two-Way-Events strategy and One-Way-Events strategy are available. One-Way-Events strategy enables you to write more straightforward code because it will be easier to track the program flow upstream back to container from the components but Two-Way-Events will make it possible to add interceptors to upstream data flow. However, my personal experience is that interceptors are rarely needed. I would not recommend to communicate upstream from the components to the state container through events in the first place.

Why Direct Invocation Is Natural

The main way to connect two components to each other should always be primarily through direct function invocations. Direct invocations are in most cases the superior way of sending data from one component to another component. The reason for this is that it is just simpler. If you use a proper IDE, you can easily navigate to the actual implementation of the callable function just by clicking on the function call itself and the IDE will bring you to the function definition. When your component wants to send data to another component, directly invoking functions of that other component is by definition the obvious way to go. You know exactly to whom you are sending data because you directly call a function of that recipient component.

Why Events Are Bad for Readability

The very idea of the event-based communication scheme is by nature exactly the opposite to the direct invocation variant. When a component fires an event, it does not know what party listens for that event. If you look into the piece of code that fires an event you can't tell where that event is being handled and what are the components that will process it - if any. When you want to know what happens and where when an event was fired, you must first navigate to the place where the event was defined and then explicitly search for places where the event is being listened for. This adds more steps to the coding work. For this reason it is inherently more difficult to follow the logic of an application that overly relies on event-based communication.

Why State Container Must Broadcast Events

Despite the problems there are some important use cases that justify using events. Communication through event dispatching is a better solution in a very special invocation scenario. Such scenario is the one-to-many communication case. In such case, there is one single place in the application, a state container, that holds the "master copy" of certain data. When this data changes, the central state container then must broadcast this change to all places where needed. Let's say, the central state contains user email info. When the user's email address is changed, this change must radiate to all components of the app that display the email address. So all components that display the email address in a way just mirror the original data. Then the problem of data inconsistencies can be eliminated. The practical solution to implement such broadcast mechanism is to fire a change event from the state container when the email was changed. The state container, when it triggers an event containing the changed data, does not know and does not need to know which components will catch this event. Those components for whom the data in question is relevant just need to "subscribe" themselves to receive that event when an update occurs.

If this kind of one-to-many notification logic had to be implemented without events, rather using direct invocation, it would mean that every time you add a component that shall display the data, you would need to explicitly update the central state container and add a new method call, such as yetAnotherComponent.setEmailAddress(newAddress). Not only would it make coding slow and error-prone because of the need to constantly update the state container methods when the components change, but it would also require the state container to have direct references to almost all components of the app. That would make the state container a big monster, eventually rendering the application quite unmaintainable.

For this reason, it is quite justified that the state container uses event dispatching as the strategy to broadcast events all around when something changes.

Why State Container Should Not Listen for Events

When the case is, however, that a component must notify the state container about data change, we do not have a one-to-many scenario. We have rather a many-to-one or at least one-to-one communication case. The idea of a central state container is in deed that there is just a single source of truth. Therefore it's not advisable to implement a state container that has a two-way-events communication scenario. There is absolutely no need to make the communication FROM components TO the container to use events. The better and cleaner way to implement the upstream flow, from components to the container, is simply through direct method invocation. All components that want to notify the container about a change will just need to directly invoke the container's change function.

Creating the Meta Store

Implement the Core

Create MetaStore.js file. In that file, type:

const metaStore = {};

You can also name it as you wish, I just call it MetaStore for convenience!

Enabling Meta Store to Receive and Broadcast Changes

With One Way Events Strategy...

Let's say that you want MetaStore to centrally hold some piece of data, let's say email address, and when the email address changes, to broadcast the change:

export const EMAIL_ADDRESS_CHANGE = 'EMAIL_ADDRESS_CHANGE';

export const setEmailAddress = (changedEmailAddress) => {
  metaStore.emailAddress = changedEmailAddress;
  dispatch(EMAIL_ADDRESS_CHANGE, emailAddress)
}

That's all what you need!

To see a complete example of implementing a React app with One-Way-Events type of Metamatic State Container, visit here.

With Two Way Events Strategy...

The Two-Way-Events strategy is not recommendable for most cases because of the loss of followability, but it's supported by Metamatic anyway. If you want to add interceptors such as logging to upstream events, Two-Way-Events may in deed be the desirable strategy:

export const EMAIL_ADDRESS_UPDATE = 'EMAIL_ADDRESS_UPDATE';
export const EMAIL_ADDRESS_CHANGE = 'EMAIL_ADDRESS_CHANGE';

const metaStore = {}

const initMetaStore = () => {
  
    handle(EMAIL_ADDRESS_UPDATE, (changedEmailAddress) => {
      metaStore.emailAddress = changedEmailAddress;
      dispatch(EMAIL_ADDRESS_CHANGE, emailAddress);    
    })

}

To see a complete example of implementing a React app with Two-Way-Events type of Metamatic State Container, visit here.