Enterprise applications typically require their state to be persisted over time, and allow users to view and manipulate this data in a variety of ways. State management can become one of the most complex areas of a large application when given the need to access and edit the same data, concurrently, across multiple locations while maintaining consistency.
State is usually persisted in a data store or database that is external to the web application components, meaning some state management complexity needs to be solved outside of the application. However for instances where data is flowing between the application and its users, several paradigms can help minimize the risks of complex state management.
Reactive data modification
Applications written in an imperative manner describe what data should get changed, how the change should occur, as well as specifying when and where the changes must occur. If several pieces of data are logically connected via some form of computation or assignment, their connection is only represented at a discrete point in time. Outside of this point in time, any of the data values may get changed in a way that violates their intended logical connection.
Applications written in a reactive manner instead try to elevate the logical connections between data, and relinquish control of specifying exactly when and where changes get made in favor of having the logical data connections kept consistent over time.
Complex applications with multiple service layers may have even more representations of the same data point as it flows around various locations in the application - a common pattern for this is the use of data transfer objects. Maintaining integrity of application state becomes exponentially complex the more representations a given piece of data may have.
Any application with a UI that presents dynamic state - including web applications - will encounter the problem of maintaining logical data connection consistency. A piece of data in these applications will always have at least two representations.
Example problem illustration
Given a todo list application that stores a set of tasks, a single task will have the following two data representations when shown to a user:
- The task’s current description (its “source of truth”, such as what its value is in a data store)
- A copy of the task’s description that is presented to a user via a UI element, such as a label or textbox.
If users can only view tasks, there are several issues related to how changes to a task’s description can be made visible to the users.
If a task is changed in the underlying data store, its new description needs to be propagated up through the UI so users aren’t viewing stale data. If the task is displayed in more than one location in the UI, all instances need to be updated so that users aren’t viewing inconsistent data between locations.
If users can also modify tasks (such as changing their description), there are additional issues that need solving.
A task description now has two sources of truth: the old value in the datastore, and the new value that a user has entered in a textbox UI element.
A change request then needs to be propagated back down to the underlying data store so the old value can be replaced with the new. Once the change is made, the new task description needs to be sent back up to the user so they see the correct value that includes their change. Any errors that may occur when trying to change the task description also need to be factored into this data exchange.
State management in Dojo
For the most basic state management requirements, a widget can manage its own state via locally-scoped variables. While this approach favours isolation and encapsulation, it is only appropriate for very simple use cases such as widgets that appear in a single location within an application, or are disconnected from all other state an application deals with.
As the need to share state between widgets increases, Dojo favors Reactive Inversion of Control. State can get lifted up into parent container widgets and injected into contained child widgets via the child’s properties interface. This lifting of state can traverse the entire widget hierarchy if needed, where state is centralized within the root application widget and portions of it are then injected into relevant child branches.
For more complex requirements, or for large widget hierarchies where passing state between unrelated intermediate layers is undesirable, an externalized data store may prove the best approach. A central data store can help applications that deal with substantial amounts of state, allow complex state edit operations, or require the same subsets of state in many locations.
Dojo provides a Stores component which supports a variety of advanced state management requirements, such as:
- Inherent support for asynchronous commands, such as making calls to remote services for data management.
- Deterministic sequencing of state manipulation operations.
- State operation recording, allowing operation rollbacks/undo
- Middleware wrapping of data manipulation processes, for cross-cutting concerns such as authorization or logging.
- Built-in support for a localStorage-based data store, aiding PWAs.
- Support for optimistic data updates, with automatic rollback on failure