State Management in javascript applications is a simple concept: keeping the data model in sync with the user interface. In this article, I will explore the fluctation of complexity that happened during the last decade in the javascript world of state management.
Early web applications were not designed to hold much state on the frontend and most times it was just a matter of handling minimal interaction. In fairness, it might have not been even necessary as state could have been pushed down by the backend directly.
This approach however, as frontend applications evolved, increased the amount of bulk code resulting in maintanance hardship. The frontend software was changing: the amount of state had been gradually moving from the backend to the frontend.
In such context, working extensively with the relatively unstable browser API was quite expensive. At that time, jQuery was the solution to all of this: it provided a simple facade to all crossbrowser complexities allowing developers to concentrate on state management.
$('#my-label').$on('status-change', updateStatus);
$('#my-label').trigger('status-change', 'NEW_STATUS');
The above is what state manegement can look like in jQuery. Neat pattern, however it did not scale well. What happens if the status is updated by some other piece of code in the application? What is the source of truth? How can we determine the status correctly?
Frameworks such as BackboneJS encapsulated the best practices from jQuery (it is one of its dependencies in fact) into Models and Views along with a Pub/Sub implementation. State management became modularised.
With AngularJS first and React afterwards, these self contained interface blocks evolved into Components and state management became a different story altogehter.
Managing State with Components
State management in a Component Architecture is structurely different and components are designed with inversion of control in mind:
<MyComponent label="My label" onClick={onClickHandler} />
We can consider this component a blackbox: its state depends entirely on its properties contract and internal state.
Consider the code below:
<OrderPanel>
<Order />
</OrderPanel>
<UserPanel>
<Cash />
</UserPanel>
How can we have state made available to both Order and Cash children? Yup, we would need to pass the information across, at each level:
<Order /> ↓
<Cart /> ↓
<CartButtonBar /> ↓
<CartCash />
The turning point
Trying to solve the cross-tree state management problem proved to be a turning point of frontend applications state management: handling state in apps suddenly became a discipline with a steep learning curve, a long list of boilerplate files and over-abstractions.
Facebook introduced the Flux pattern for its frontend, celebrating the unidirectional data flow.
As you can see from the above graph, it’s not immediately obvious how the state is managed. However, in a nutshell, the application state is representated by a store, a global object interacted with through an actions api.
This approach has clear benefits such as a single source of truth in the store as well as a clear separation of concerns.
A few years later, Dan Abramov presented Redux, a library inspired by Flux to manage state in frontend applications. This library would influence frontend applications, especially React ones, for the years to come.
Redux is somewhat different to the original Flux implementation. I won’t go through it here, but it is important to pin down the fact that it introduced more overhead:
- Reducer (and function purity)
- Reducer composition (and function composition)
- Immutability
- Redux middlewares (and generators if using Redux-Saga)
When used with React, we could add these to the list:
- State to props mapping
- Component to store mapping
- Dispatch to props mapping
- Action creators
Despite the steep learning curve, Redux became very popular as it proved to succeed in production for complex applications. On the other side of the coin, Redux had unwillingly been adopted as a state management standard thanks to its smooth React integration. Is this complexity required to maintain good control over the state of the application? Not always.
You might not need Redux. – Dan Abramov
A fresh take on state management
In recent years, React introduced the Context API which suggested a new way to accessing state from deeply nested components. With the introduction of Hooks, and the ability to use them anywhere in the application, state management returned to its bitesize format that it once was:
// OrderConfirm.jsx (a StateProvider must be available)
const OrderConfirm = () => {
const [{ cash }, dispatch] = useStateValue();
return (
<>
<p>You have this { cash } </p>
<Button onClick={ () => dispatch({ type: 'confirm-order' }) }>
Confirm Order
</Button>
</>
);
}
// reducer/yourReducer.js
const reducer = (state, action) => {
switch (action.type) {
case 'confirm-order':
return {...};
default:
return state;
}
};
This example ignores performance implications around rendering children components using Context, however a new path to a simpler api seems to be available and solutions to improve this design are on the way.
A similar concept concept can be applied in Vue.js using observable and computed properties. I will talk more about this technique in the upcoming articles.
Conclusion
As frontend software evolves so will state management techniques and this is by no means the end of experimentation. The complexity curve has gone down, up and down again and this is going to be a common pattern as new technologies and tools are introduced in the ecosystem.
Useful resources
- React — Separation of Concerns
This article is originally by Sergio Martino.