loading...

I Also Created the Exact Same App Using AppRun

yysun profile image yysun ・7 min read

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 same app in React and Vue. Here are the differences.” that I would like to add AppRun into the comparison as well.

original post

I will re-create the app and answer the same questions explored in the original post:

  • What is the project structure?
  • How do we mutate data?
  • How do we create new To Do Items?
  • How do we delete from the list?
  • How do we pass event listeners?
  • How do we pass data through to a child component?
  • How do we emit data back to a parent component?
  • Finally, what are the differences?

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

apprun app

1. What is the project structure?

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

{
  "name": "apprun-todo",
  "version": "1.0.0",
  "scripts": {
    "start": "parcel -p 8080 src/index.html --no-hmr",
    "build": "parcel build src/index.html"
  },
  "devDependencies": {
    "parcel-bundler": "^1.9.7",
    "typescript": "^2.9.2"
  },
  "dependencies": {
    "apprun": "^1.15.2"
  }
}

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

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <title>AppRun-Todo</title>
  <link rel="stylesheet" type="text/css" href="style.css"/>  
</head>
<body>
  <div id="root"></div>
  <script src="todo.tsx"></script>
</body>
</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 compiled from src/todo.tsx. The style.1190871a.css is compiled from style.css. BTW, I prefer one CSS/SCSS as opposed to having 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.

import app, { Component } from 'apprun';

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>

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

const view = (state) => <div className="ToDo">
  <img className="Logo" src="https://github.com/yysun/apprun/blob/master/logo.png?raw=true" 
    alt="AppRun logo" />
  <h1 className="ToDo-Header">AppRun To Do</h1>
  <div className="ToDo-Container">
    <div className="ToDo-Content">
      {state.list.map((item, key) => <ToDoItem item={{ ...item, key }} />)}
    </div>
    <input type="text" onkeypress={e => app.run('keypress', e)} />
    <div className="ToDo-Add" onclick={e => app.run('create-item')}>+</div>
  </div>
</div>;

const update = {
  'keypress': (_, e) => {
    e.keyCode === 13 && app.run('create-item');
  },
  'create-item': (state) => {
    const input = document.querySelector('input');
    if (input.value) return ({ list: [...state.list, { todo: input.value }] });
  },
  'delete-item': (state, key) => ({ list: state.list.filter((_, idx) => idx !== key) })
}

app.start('root', state, view, update);

The app is an AppRun global application that does not use components. I have been debating myself whether to create the app using components to compare closely with React/Vue. In 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 a 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 handler 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 to create a new To Do item.

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

4. How do we delete from the list?

We develop an event handler 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 to delete item.

<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 five questions out of six 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, or 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 a 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>

Comparing to the React app and the Vue app, React code looks like:

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

Vue code looks like:

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

In the React app, we have to manage the state and use this.setState() to trigger the DOM rendering by ourselves. In the Vue app, we mutate the state directly and let Vue reactively render the DOM.

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

2) AppRun functions do not need “this”

Although sometimes this keyword in JavaScript gives surprises because it behaves differently than it does in other languages, looking at the Vue component I am feel the confusion at a new level. 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 of having 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 the user’s input from the DOM when creating a 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 the user adds or deletes the items, the screen is re-rendered, but 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://glitch.com/edit/#!/remix/apprun-todo.

Have fun!

Posted on by:

Discussion

markdown guide
 

Awesome comparison! I have been using Apprun in a few projects now. I have loved every minute of it. My apps have been so much simpler and easier to understand!