AppRun Architecture¶
AppRun is designed to simplify web application development by implementing the architecture, which ont only enhances code readability but also improves maintainability and scalability, making your development process more efficient.
Architecture Overview¶
Application logic is broken down into three separate parts in the AppRun architecture.
- State (a.k.a. Model) — the state of your application
- View — a function to display the state
- Update — a collection of event handlers to update the state
Use a Counter as an example.
const state = 0;
const view = state => <div>
<h1>{state}</h1>
<button onclick={()=>app.run('-1')}>-1</button>
<button onclick={()=>app.run('+1')}>+1</button>
</div>;
const update = {
'+1': state => state + 1,
'-1': state => state - 1
};
app.start(document.body, state, view, update);
State¶
The state can be any data structure, a number, an array, or an object that reflects the state of the application. In the _ Counter_ example, it is a number.
const state = 0;
Note
You define the initial state. AppRun manages the state. Therefore the initial state is an immutable constant.
View¶
The view generates Virtual DOM based on the state. Finally, AppRun calculates the differences against the web page element and renders the changes.
const view = state => <div>
<h1>${state}</h1>
<button $onclick="-1">-1</button>
<button $onclick="+1">+1</button>
</div>;
Note
AppRun allows you to choose your favorite virtual DOM technology in the view function. The example above uses JSX. You can also use lit-html, uhtml, and etc.
Update¶
The update is a collection of named event handlers or a dictionary of event handlers. Each event handler creates a new state from the current state.
const update = {
'+1': state => state + 1,
'-1': state => state - 1
}
Note
There are a few other ways to define event handlers.
When the three parts, the state, view, and update are provided to AppRun to start an application, AppRun registers the event handlers defined in the update and waits for AppRun events.
app.start(document.body, state, view, update);
Next, let's review some of the benefits of AppRun Architecture.
Avoid Spaghetti Code¶
AppRun solves two challenging problems commonly found in application development:
Let's make the Counter a bitter complicated to show how many times each button clicked.
// 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 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
});
app.start(document.body, state, view, { plus, minus });
The code below uses jQuery. jQuery is a library that provides the convenience to access and manipulate the DOM. It does not give any architectural guidance. jQuery code is similar to the vanilla JavaScript code that can go wild. A jQuery Example
$(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
- The state is shared globally. The two event handlers plus and minus both update the state directly.
- The two event handlers also render the DOM in different pieces.
Therefore, the jQuery code has two problems:
- Direct State Update
- Rendering Fragments
In much more complicated real applications, the logic could be long and tangled even more.
How can we solve the problems using AppRun?
AppRun Code¶
AppRun includes state management, an event system, and Virtual-DOM rendering. Following the Hollywood Principle (Don't call us. We call you), we provide code pieces to AppRun and wait for AppRun to call them.
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.
When using AppRun to update the state, AppRun gives the current state. Then, 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
});
Because there is no reference to a shared global object, it is very easy to unit-test the logic. Also, we can focus on the parts of the state that are needed to update and ignore the rest of the state using the spread operator.
We only write a view function that creates a virtual DOM. AppRun renders the DOM using the diffing algorithm. It only updates the DOM that is needed to change. Therefore, although we have only 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. Therefore, the view function is a pure function. There are many benefits of using the pure__ function, including but not limited to unit testing.
Finally, We have a _ counter _ application shown below by putting the state, _view, and update together.
// 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 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
});
app.start(document.body, state, view, { plus, minus });
You can see that with the help of AppRun state management and DOM differential rendering, we no longer have the Direct State Update with Rendering Fragments problems.
AppRun Benefits¶
No matter how complex the application is, we will always have three parts, the state, view, and update. We don't mix the state update with DOM rendering. Because the three parts are decoupled, our codebase is so much easier to understand, test, and maintain.
Ceremony vs. Essence¶
There was the 'Ceremony vs. Essence' discussion that happened about ten years ago. At that time, Ruby was on the rise. So people compared Ruby with C#.
The fundamental idea of the Ceremony vs. Essence idea appears to be that, all other things being equal, programming languages should attempt to allow programmers to clearly express the essence of their programs without being caught up in excessive ceremony provided by the programming language. -- From this post.
Let's take a look at some of today's frontend technologies from the Ceremony vs. Essence point of view. We will use a simple button-click counting application as an example.
const add = count => count + 1;
const view = count => <button $onclick={add}>
Clicks: {count}
</button>;
const rendered = count => console.log(count);
app.start(document.body, 0, view, null, { rendered });
console.log('mounted!');
The Essence¶
The essence of the application is to display a button that adds the count by one and shows the count. Also, it will log some messages in the console to mimic effects after the rendering cycle.
The concept is as simple as below.
<button onclick="count+1">
Clicks: {count}
</button>
console.log(count); // upon very click
console.log('mounted!'); // upon mounted
We will compare the 95-character essence code above with a few front-end frameworks, such as AppRun, Svelte, React Hooks, and the Vue Composition API.
A framework defines a skeleton where the application defines its features to fill out the skeleton. -- you can find this quote from googling.
We need to write code to plugin the essence code into the frontend frameworks, which is the ceremony. We don't want them. Less of them is better.
The Ceremony¶
AppRun¶
Application logic is broken down into three separate parts in the AppRun architecture.
const add = count => count + 1;
const view = count => <button $onclick={add}>
Clicks: {count}
</button>;
const rendered = count => console.log(count);
app.start(document.body, 0, view, null, { rendered });
console.log('mounted!');
In the example above,
- The application's state is a number that has a default value of 0.
- The add function is the event handler to update the state.
- The view function displays the state.
- The rendered function runs after the DOM is rendered.
- The app.start function ties them all together to the document.body element.
Now, we identify and cross out the ceremonies.
With AppRun, the ceremony is mainly required by the JavaScript syntax, like the module import and the arrow functions. Overall, it has 226 characters, which means 58% of the code are ceremonies.
Svelte¶
Svelte uses a single file for a component. The file consists of a script section for the code and the UI template. It requires a compiler to turn it into runnable JavaScript code.
<script>
import { onMount } from 'svelte'
let count = 0;
const add = () => count + 1;
$: console.log(count)
onMount(() => console.log('mounted!')
</script>
<button on:click={add}>
Clicks: {count}
</button>
Behind the scene, the svelte compiler creates the component class boilerplate. Then, the compiler extracts the script block, wires up the reactivity ($:), and adds the rendering template into the boilerplate. The boilerplate does not exist in our codebase. Therefore, the svelte application has very few ceremonies.
Svelte code ceremony is also mainly the JavaScript syntax requirements. Only the script tags are required by the Svelte compiler, which is worth trading with what the compiler saves.
It has 217 characters, which means 56% of the code is ceremony.
React Hooks¶
The React code is a slightly modified version of the React Hooks Docs](https://reactjs.org/docs/hooks-overview.html).
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
const add = () => setCount(count + 1)
useEffect(() => {
console.log(count);
});
return (
<button onClick={add}>
Clicks: {count}
</button>
);
}
The React code has more ceremonies than the AppRun code and Svelte code above. It has 272 characters and a 65% ceremony.
The setCount, _useState, and useEffect functions are the code that deals with the React framework itself. They don't help us to express the essence of the application. They are framework ceremonies.
Vue Composition API¶
The Vue code is a slightly modified version of the Vue Composition API Docs.
<template>
<button @click="add">
Clicks: {{ count }}
</button>
</template>
import { ref, watchEffect, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
function add() {
count.value++
}
watchEffect(() => console.log(count.value))
onMounted(() => console.log('mounted!'))
return {
count,
add
}
}
}
The ref, watchEffect, onMounted, setup, _count.value, and returning an object of count, and add are all required by the Vue framework. Sometimes, they may make writing code more difficult.
Expression Comparison¶
We are not stopping at only comparing the character counts or how many extra boilerplates are forced on you by the frameworks. We also compare how you express the business logic. For example, let's see how we express Increase the Counter as an example again.
// AppRun
const add = counter => counter + 1;
//Svelte
let count = 0;
const add = () => counter + 1;
// React
const [count, setCount] = useState(0);
const add = () => setCount(count + 1);
// Vue
const count = ref(0);
const add = () => count.value++;
Both the AppRun code and the Svelte code express the essence well and have less ceremony. So AppRun and Svelte are easy to understand. React Hooks and Vue Composition API are cool. However, they both add a lot more ceremonies to our codebase.
Remember, the ceremony has no business values but just challenges to understand and maintain.
In addition, AppRun has a few other benefits
- AppRun is lightweight that can run in browsers directly without a compiler.
- AppRun uses pure functions when it is possible.
- AppRun app codebase can easily be strongly typed if you wish.
I hope you enjoy it. If you haven't tried, try to edit and run the AppRun code above.