I Also Created the Exact Same App Using AppRun

I felt it was quite fun to compare AppRun with Redux and React Context API last time. Today, I found another great post titled “I created the exact same app in React and Vue. Here are the differences.” that I like to redo the same app in AppRun and answer the same questions explored in the original post:

  1. What is the project structure?
  2. How do we mutate data?
  3. How do we create new To Do Items?
  4. How do we delete from the list?
  5. How do we pass event listeners?
  6. How do we pass data through to a child component?
  7. How do we emit data back to a parent component?
  8. Finally, what are the differences?

I have created the app on glitch.com as usual. It is very convenient using glitch.com. Here is live demo link: https://apprun-todo.glitch.me/.

1. What is the project structure?

In this example, I use the Parcel bundler. The project has only dependencies to Parcel, TypeScript and AppRun.

Project File

Parcel allows us to include the TypeScript file in index.html.

Source index.html

Parcel compiles the src/index.html to the new dist/index.html. The new HTML file references to the compiled CSS and JavaScript files.

Compiled index.html

The todo.294293ff.js is compiledfrom src/todo.tsx. The style.1190871a.css is compile from style.css. BTW, I prefer one CSS/SCSS as opposed to have separated CSS files or CSS in component files. Also, I prefer to inline CSS into HTML (Parcel dose not do it currently).

The app has 38 lines in the todo.tsx file.

Todo.tsx

The app is an AppRun global application that does not use components. I have been debating myself whether to create the app using components in order to compare closely with React/Vue. At the end, YAGNI won. If “You aren’t gonna need it” (YAGNI), don’t do it. I decided to stay with the global application mode, because it demonstrates that I have the option to chose simpler architecture for simpler apps using AppRun.

You can compare it with the Vue app source code and React app source code.

2. How do we mutate data?

Let’s start with how do we store the data. We create an data object as the initial state.

const state = {
list: [
{ 'todo': 'clean the house' },
{ 'todo': 'buy milk' } ]
};

When we update the data object, E.g., updating the ‘name’ property to be ‘John’, we create a new data object to be the new state in the event handler.

const update = {
'update-name': return ({...state, name: 'John' })
};

3. How do we create new To Do Items?

We develop an event handlers that creates a new state including the new item in it .

const update = {
'create-item': (state) => {
const input = document.querySelector('input');
if (input.value) return ({
list: [...state.list, { todo: input.value }] });
}
}

We publish the ‘create-item’ event in the DOM event handler.

<div className="ToDo-Add" onclick={() => app.run('create-item')}>+</div>

4. How do we delete from the list?

We develop an event handlers that creates a new state excluding the removed item.

const update = {
'delete-item': (state, key) => ({
list: state.list.filter((_, idx) => idx !== key) })
}

We publish the ‘delete-item’ event in the DOM event handler.

<div className="ToDoItem-Delete" onclick={()=> app.run('delete-item', item.key)}>-</div>

5. How do we pass event listeners?

We publish AppRun events in the DOM event handlers.

<input type="text" onkeypress={e => app.run('keypress', e)} />

<div className="ToDo-Add" onclick={e => app.run('create-item')}>+</div>

6. How do we pass data through to a child component?

We use the stateless component (a.k.a pure function component), which looks like a JSX tag (<ToDoItem />), but it is just a function call.

<div className="ToDo-Content">
{state.list.map((item, key) => <ToDoItem item={{ ...item, key }} />)}
</div>

We destruct the function parameters to get the data in the stateless component.

const ToDoItem = ({ item }) => <div className="ToDoItem">
<p className="ToDoItem-Text">{item.todo}</p>
<div className="ToDoItem-Delete" onclick={()=> app.run('delete-item', item.key)}>-</div>
</div>

7. How do we emit data back to a parent component?

We publish an AppRun event.

<div className="ToDoItem-Delete" onclick={()=> app.run('delete-item', item.key)}>-</div>

8. What makes AppRun different?

1) We drive the app/component update life-cycle using events.

You can see the answers to the above 5 questions out of 6 is “publishing AppRun events”.

AppRun controls the entire app/component update life-cycle, including managing the states, creating new virtual DOM and render the real DOM.

During the AppRun event life-cycle, AppRun first invokes the event handlers by passing in the current state. When the state needs to be updated, the event handler creates a new state and give it back to AppRun. AppRun then passes the new state into the view function. The view function creates the virtual DOM. Finally, AppRun renders the virtual DOM to real DOM. If the event handler does not return a new state, of if the view function does not return a virtual DOM. the event lifecycle stops. It is a unified and straightforward way for us control AppRun app logic flow.

Web events => AppRun events => Update/Event handlers => (new state) => View => (virtual DOM) => DOM

The AppRun event handlers are defined in the centralized location, the Update object.

const update = {
'keypress': (state) => {},
'create-item': (state) => {},
'delete-item': (state) => {},
}

Whenever we want to do something, we publish AppRun events by calling app.run(). AppRun finds and invokes the event handler from the Update object. E.g., When creating and deleting new to-do item, we publish the ‘create-item’ and the ‘delete-item’ events.

<div onclick={() => app.run('create-item')}>+</div>
<div onclick={() => app.run('delete-item', item.key)}>-</div>

Because we call so many times app.run(), the library itself is named after it — AppRun!

Using React/Vue, we create functions to handle the DOM events.

Vue code looks like:

<div @click="createNewToDoItem()">+</div>
<div @click="deleteItem(todo)">-</div>

React code looks like:

<div onClick={this.createNewToDoItem}>+</div>
<div onClick={this.props.deleteItem}>-</div>

In the React functions, we have to manage the state and to trigger the DOM rendering by ourselves. In the Vue functions, we mutate the state directly and let Vue reactively render the DOM. In either case, we need to do more to control the app logic flow.

2) AppRun functions do not need “this”

Although sometimes the this keyword in JavaScript gives surprises because it behaves differently than it does in other languages, looking at the Vue component, I am still surprised. What is ‘this’?

export default {  
data() {
return {
list: []
}
}
methods: {
createNewToDoItem() {
this.list.push(...);
}
}
}

How come ‘this.list’ means the list array of the object created by the data() function?

AppRun manages the states. It passes the state into the event handlers and the view function. The functions have all the data they need to execute. There is no need to use this. Furthermore, the AppRun view function is pure. AppRun view function solely operates on input state and has no side effects.

const view = (state) => <div className="ToDo">
......
</div>

In AppRun app development, we can develop and unit test the event handlers and view functions separately. It allows us to focus on them one at a time. We all know that pure functions are easier to reason, to test and to maintain.

3) Two-way data binding is possible, but think YAGNI

AppRun is capable to have two-way data binding. We can use the same method for React, which is to handle the onchange event of the <input /> node to take the value of the <input /> node to the state. Also, from this AppRun example, you can see how to implement two-way data binding using the custom directives.

In the to-do app, we don’t need two-way data binding. We can take user’s input from DOM when creating new item.

'create-item': (state) => {
const input = document.querySelector('input');
if (input.value) return ({
list: [...state.list, { todo: input.value }] });
}

Also, when AppRun renders the virtual DOM, it won’t reset the value of the <input /> node. When user adds or deletes the items, the screen is re-rendered, but the the user’s input is retained. I deliberately did not clean the <input /> after created the new item so you can see the effects.

If YAGNI, don’t do it.

I will end this post with the compiled JavaScript file size:

  • AppRun: 18.3 Kb
  • Vue: 77.84 Kb
  • React: 127.24 Kb

I encourage you to re-mix (fork) the app at glitch.com: https://apprun-todo.glitch.me.

And for more information about AppRun, please visit https://github.com/yysun/apprun.

Have fun!