You are now in control
mainapp
is a micro web application framework, where the control flow starts and is never lost. mainapp
implements a state container that you actually want to use. Update state without the ceremony.
Here is a simple counter application.
import {h, App} from 'mainapp'
App({
count: 0,
down ({count}, value) {
return {
count: count - value
}
},
up ({count}, value) {
return {
count: count + value
}
},
view ({count, down, up}) {
return <div>
<h1>{count}</h1>
<button onclick={() => down(1)}>-</button>
<button onclick={() => up(1)}>+</button>
</div>
}
}, document.getElementById('mainapp-entry'))
mainapp
is compatible with picostyle.
All webapps have the same main loop. Let's learn how mainapp
implements the main loop.
The state tree is the whole application. The state tree has both the data and code (views and actions) in the same place. This mean the application can rewrite itself, which is useful for code splitting.
When state is updated, view
s functions are used to render the application. They return DOM fragments based on the current state tree.
Actions are functions on the state tree that implement application logic and update the state tree. They are triggered by events or other actions. Actions take the current state tree and return an update. Actions can be asynchronous.
Let's take a closer look at the counter sample and show how the state tree changes.
The initial state tree is...
{
count: 0,
down ({count}, value) {
return {
count: count - value
}
},
up ({count}, value) {
return {
count: count + value
}
},
view ({count, down, up}) {
return <div>
<h1>{count}</h1>
<button onclick={() => down(1)}>-</button>
<button onclick={() => up(1)}>+</button>
</div>
}
}
mainapp
uses view
to render user interface. view
receives the current state tree as the first argument into the function. We destructure it to get the current count
and two actions. The initial DOM would be...
<div>
<h1>0</h1>
<button onclick="() => down(1)">-</button>
<button onclick="() => up(1)">+</button>
</div>
Let's say the user clicks on the +
button. First of all, notice how mainapp
uses the native onclick
event handler and how its bound to the up
action. Looking at the implementation of the up
action, we notice similar to the view
, the first argument is the current state tree. But wait, in the event handler, we don't pass in the state tree? This is the magic the makes mainapp
easy to use. When defining actions we get access to the state tree, but when calling the action, we don't need to worry about it. This is an example of partial application in action.
When the up
action is called, it returns {count: 0 + 1}
. We don't need to return the whole state tree, we just return what we want to update, in this case the count
. This will update the state tree to be...
{
count: 1,
...rest of state tree is unchanged
}
mainapp
automatically renders the user interface after the state tree is updated.
mainapp
supports components as substate trees. For example, we can extract Counter
as a component...
import {h, App} from 'mainapp'
const Counter = {
count: 0,
down ({count}, value) {
return {
count: count - value
}
},
up ({count}, value) {
return {
count: count + value
}
},
view ({count, down, up}) {
return <div>
<h1>{count}</h1>
<button onclick={() => down(1)}>-</button>
<button onclick={() => up(1)}>+</button>
</div>
}
}
App({
Counter,
view({Counter}) {
return <Counter/>
}
}, document.getElementById('mainapp-entry'))
Each component has their own state. We use the Counter
component multiple times and each will have their own count
. Notice how the root view renders the Counter
using <Counter/>
.
Components can declare lifecycle actions didMount
and willUnmount
.
didMount(state, parentKey)
: an action on a child component which is called once after mounted on the parent. TheparentKey
is the key on the parent where the child component was mounted. Use this lifecycle to do one time setup with external APIs.willUnmount(state, parentKey)
: an action on a child component which is called once before unmounted from the parent. TheparentKey
is the key on the parent where the child component was mounted. Use this lifecycle to clean up things setup withdidMount
.willUnmount
will be called transitively the entire component sub-tree if ancestor is umounted.
Here is an example on how to use didMount
and willUnmount
to attach and clean up event listeners.
const {mountOnline, unmountOnline} = App({
mountOnline() {
return {
Online: {
online: 'unknown',
wentOnline() {
return {
online: 'yes'
}
},
didMount({wentOnline}) {
window.addEventListener('online', wentOnline)
},
willUnmount({wentOnline}) {
window.removeEventListener('online', wentOnline)
},
view({online}) {
return <p>Online: {online}</p>
}
},
}
},
unmountOnline() {
return {
Online: undefined
}
},
view({Online}) {
return {Online && <Online/>}
}
}, document.getElementById('mainapp-entry'))
await mountOnline()
// didMount fires
await unmountOnline()
// willUnmount fires
Applications are simpler when the control flow is clear. When the control flow is spread all over the application, it is much harder to change. mainapp
allows you to keep control at the root state tree.
While its easy for parent nodes in the state tree to call actions on children nodes, we need a way for child nodes to call actions on their parent or even on the top of the state tree. While this seems dangerous, in practise all webapps need to do this.
All components have access to their parent state tree via $parent
and the global state tree via $global
.
import {h, App} from 'mainapp'
const Grandchild = {
name: 'grandchild',
view({$global: {name: rootName}, $parent: {name: parentName}, name}) {
return <div>
<p>Root name: {rootName}</p>
<p>Parent name: {parentName}</p>
<p>Own name: {name}</p>
</div>
}
}
const Child = {
name: 'child',
Grandchild,
view({Grandchild}) {
return <Grandchild/>
}
}
App({
name: 'root',
Child,
view({Child}) {
return <Child/>
}
}, document.getElementById('mainapp-entry'))
This will render...
<div>
<p>Root name: root</p>
<p>Parent name: child</p>
<p>Own name: grandchild</p>
</div>
Component actions can also update $global
and $parent
.