The UI that you can see and manipulate on screen is the result of painting a visual representation of data. The shape of data hints at the kind of controls you provide for visualizing and manipulating this data. For example, if you have a list of items, you will likely show a
List control that has an array of
ListItems. Operations may include searching, paginating, filtering, sorting, or grouping the items in the list. The state of these operations is also captured as data and informs the visual representation.
The following diagram shows the direct relationship of an array with a List control:
In short, it is the data that takes on a pivotal role in describing the UI. Handling the structure and managing the changes that can happen to this data is what we commonly refer to as state management. State is just a synonym for the client-data that is rendered on the UI.
State management is the act of defining the shape of data and the operations that are used to manipulate it. In the context of the UI, it is called client-side state management.
As the complexity of the UI increases, more state is accumulated on the client. It gets to a point where state becomes the ultimate source of truth for whatever we see on the screen. This approach to UI development, where we elevate the importance of the client-state, has been one of the biggest shifts in the frontend world. There is an interesting equation that captures this relationship between UI and state:
fn is a transformation function that is applied on the state (the data) that produces a corresponding UI. In fact, a subtle meaning that is hidden here is that, given the same state,
fn always produces the same UI.
In the context of React, the preceding equation can be written as follows:
The only difference here is that
fn takes two inputs,
state, which is the prescribed contract of a React component.
However, the preceding equation is only giving half the story of a UI. It's true that the visual representation is derived from the state (through the transformation function,
fn), but it does not account for the user operations that occur on the UI. It's like we have completely ignored the user in the equation. After all, the interface is not just used to visually represent data (state), but to also allow the manipulation of that data.
This is where we need to introduce the concept of actions that represent these user operations, which results in a change in state. Actions are the commands that you invoke as a result of various input-events that are fired. These actions cause a change in the state, which is then reflected back on the UI.
We can visualize the triad of State, UI, and Actions in the following figure:
It is worth noting that the UI does not change the state directly, but instead does it via a message-passing system by firing actions. The action encapsulates the parameters that are required to cause the appropriate change in state. The UI is responsible for capturing various kinds of user events (clicks, keyboard presses, touches, voice, and so on) and translating them into one or more actions that are then fired to change the state.
When the State changes, it notifies all of its observers (subscribers) of the change. The UI is also one of the most important subscribers that is notified. When that happens, it re-renders and updates to the new state. This system of data flow from the State into the UI is always uni-directional and has become the cornerstone of state management in modern UI development.
One of the biggest benefits of this approach is that it becomes easy to grasp how the UI is kept in sync with changing data. It also cleanly separates the responsibilities between rendering and data changes. The React framework has really embraced this uni-directional data flow and you will see this adopted and extended in MobX as well.