Introduction
Recently I was asked to refresh an old application that has the so-called spaghetti code. There are different levels of problems. Many can be solved just refactoring to use new JavaScript language features such as using modules. However, two problems are hard to solve without the help of a framework, which I call them:
- Direct State Update
- Rendering Fragments
In the post, I will show you how AppRun can help to solve the problems. Therefore we can avoid spaghetti code.
Example
I cannot show the real production code I am dealing with, so I made an abstracted example to demonstrate the problems. As usual, I am using a counter example that has two buttons. One to increase the counter. The other one to decrease the counter. Also, I made it a litter complicated to show how many times each button is clicked.
Problem Code
The code below uses jQuery. jQuery is a library that provides the convenience to access and manipulation of the DOM. It does not provide any architectural guidance. jQuery code is similar to the vanilla JavaScript code that can go wild.
$(function () {
// global state
let count = 0
let count_plus = 0
let count_minus = 0
function plus() {
// state update
count ++
count_plus ++
// rendering
$('#total').html(count)
$('#plus').html(`+ (${count_plus})`)
}
function minus() {
// state update
count --
count_minus ++
// rendering
$('#total').html(count)
$('#minus').html(`- (${count_minus})`)
}
$('#plus').on('click', plus)
$('#minus').on('click', minus)
})
You can see from the above code that event handlers plus and minus have the problem patterns. They update the state directly. They also render the DOM in different pieces.
But the real problem is that there isn't a way to break them further. The state has to be shared globally. And the rendering has to be different in each click event.
In much more complicated real applications, the logic could be long and tangled even more.
AppRun
AppRun is the framework that can solve the two problems.
If you are new to AppRun, read the AppRun Book or visit AppRun Docs.
State Management
AppRun is a state management system. It is also an event-driven system that has an event lifecycle. During an AppRun event lifecycle:
- AppRun let you update the state when needed
- AppRun let you create a virtual DOM out of the state when needed
- AppRun renders the virtual DOM when needed.
Sort of following the Hollywood Principle (Don't call us. We call you.) here, we provide code pieces to AppRun and wait for AppRun to call them.
We write functions to update the state. AppRun gives the current state. We create a new state based on the current state.
const minus = (state) => ({ ...state,
count: state.count - 1,
count_minus: state.count_minus + 1
});
const plus = (state) => ({ ...state,
count: state.count + 1,
count_plus: state.count_plus + 1
});
We will be able to concentrate on the parts that are needed to update. We can spread out the rest of the state using the spread operator. Also, because there is no reference to a shared global object, it is very easy to unit test the state update logic.
DOM Rendering
We also write a view function that AppRun will call with the state as the input parameter. We usually use JSX in the view function to create a virtual DOM, which is just data structure. The view function does not render the DOM. AppRun renders the DOM using a diffing algorithm. It only renders the DOM that is needed to change. Therefore, we only need one view function for all events. AppRun takes care of the differential rendering accordingly.
const view = ({ count, count_plus, count_minus }) => html`
<h1>${count}</h1>
<button onclick="app.run('minus')">- (${count_minus})</button>
<button onclick="app.run('plus')">+ (${count_plus})</button>`
The view function always returns the same result as long as the state is the same. It also does not change the state or anything outside the function, which means it has no side effects. We can make the view function a pure function. There are many benefits of using pure function, including but not limited to the unit testing. It makes the UI code easy to unit test.
Using AppRun, we have a counter application made from the state, _view, and update as shown below.
// initial state object
const state = {
count: 0,
count_plus: 0,
count_minus: 0
}
// one view function to render the state, its' a pure function
const view = ({ count, count_plus, count_minus }) => html`
<h1>${count}</h1>
<button onclick="app.run('minus')">- (${count_minus})</button>
<button onclick="app.run('plus')">+ (${count_plus})</button>
`
// collection of state updates, state is immutable
const plus = (state) => ({ ...state,
count: state.count - 1,
count_minus: state.count_minus + 1
});
const minus = (state) => ({ ...state,
count: state.count + 1,
count_plus: state.count_plus + 1
});
app.start(document.body, state, view, {plus, minus});
With the AppRun state management and DOM differential rendering in place, we no longer have the problem mixing state update with DOM rendering.
Usually, at this moment, I will show you the live demo on glitch. This time, I will show the interactive notebook I made on observable HQ. I feel I like the notebook more and more.
https://observablehq.com/@yysun/apprun-helps-to-avoid-spaghetti-code
Conclusion
No matter how complex the application is, we will always have three parts, the state, view, and update. We no longer mix the state update with DOM rendering. Because the three parts are totally decoupled, our codebase is so much easier to understand, test, and maintain.
Top comments (2)
I suppose the code has a "seeded bug"
in plus update function need: count: state.count +1 instead of state.count - 1.
Likewise in minus update function need: count: state.count - 1
🙏 Thank you for letting me know. I have updated it.