Handling Application State
Dynamic web applications such as the one below typically need to reflect some data stored in a database.
This data is also referred to as the application state. Often the user would have controls to manipulate state in the browser's memory and must be synced with the database.
State management refers to how you go about syncing changes to state within the UI and the database.
How is it done?
Typically your framework of choice would provide some syntax or strategy for this. There are even entire libraries dedicated to state management such as redux or mobx.
However, it's mostly about applying some structure to your code that allows the UI components of your app to easily trigger and react to changes in state. As such, this can indeed be achieved with vanilla JS.
The Observable Pattern
The Observable Pattern belongs to a set of software engineering code recipes called design patterns. It turns out this issue of handling state changes is exactly the kind of thing observables were meant for.
The Observable is a class that can allow other objects to subscribe to events that occur by the Observable. Many state management libraries actually follow this pattern.
class Observable {
constructor(value) {
this._listeners = [];
this._value = value;
}
notify() {
this._listeners.forEach(listener => listener(this._value));
}
subscribe(listener) {
this._listeners.push(listener);
}
get value() {
return this._value;
}
set value(val) {
if (val !== this._value) {
this._value = val;
this.notify();
}
}
}
The above is a typical observable implementation. It just needs to track the components subscribed to it and publish events to the components.
Components
Next, we'll define a class that represent our UI components that need to react to events that happen in our web app. In this case our components are the list of todos that render under the form and the blue box to the right that shows a summary.
Our components must react to changes in state ie when todos are created, deleted or updated.
class Component {
constructor(elem, template){
this.elem = elem;
this.template = template;
}
//update the html of the component with the new state
render(state){
this.elem.innerHTML = this.template(state);
}
}
Components store their corresponding HTML element on the page, they also store what I call a template function. Template functions takes some data and returns a template literal html string containing the data sent to it. The template function is then called in the render method should the component's html need to be updated. This would make more sense in a bit.
Putting it Together
In the main application, an observable todoObserver is created. This will store the todo's data and whenever the data changes this observer will broadcast it to any components.
const todoObserver = new Observable([]);
//initialized with empty array of todos
Next we'll create our stats component, which is the blue box that shows the todo summary.
const stats = new Component(
document.querySelector('#stats'),//html element on page
function(todos){//template function
const numDone = todos.filter(todo => todo.done).length;
const numTodos = todos.length;
return `
<div class="row">
<div class="col s12 m6 offset-m3">
<div class="card-panel blue">
<p class="white-text">
Num Todos: ${numTodos}
</p>
<p class="white-text">
Number Done: ${numDone}
</p>
</div>
</div>
</div>
`;
}
);
Notice how the template function returns how the todo data should be displayed in the html.
Then we let the stats component subscribe to the todoObserver. Essentially we are telling the stats what to do when the todoObserver has any updates to the todos. In this case we just want to re-render the stats component when the observer broadcasts new changes to the todos.
todoObserver.subscribe(function(todos){
stats.render(todos);
});
Finally any time changes are sent to the server we call getTodos() to send a request to the server and update the observer with the latest changes. When the todoObserver is updated, it would then broadcast those updates to the subscribed components.
async function getTodos(){
//sends a request to get the latest todos data from the server
todoObserver.value = await getAllTodos();
}
Conclusion
This is a neat (some what verbose) way to setup state management in a web app. Components just need to subscribe to changes from a single object. You can see the full working example at this REPL.
Top comments (0)