A virtual node is an object that describes an HTML/DOM tree.
It consists of a tag, e.g. div, svg, etc., data attributes and an array of child nodes.
{
tag: "div",
data: {
id: "app"
},
children: [{
tag: "h1",
data: null,
children: ["Hi."]
}]
}The virtual DOM engine consumes a virtual node and produces an HTML tree.
<div id="app">
<h1>Hi.</h1>
</div>You can use the h(tag, data, children) utility function to create virtual nodes.
h("div", { id: "app" }, [
h("h1", null, "Hi.")
])Or setup a build pipeline and use Hyperx or JSX instead.
Any valid HTML attributes/properties, events, styles, etc.
data: {
id: "myButton",
class: "PrimaryButton",
onclick: () => alert("Hi."),
disabled: false,
style: {
fontSize: "3em"
}
}Attributes also include lifecycle events and meta data such as keys.
Use the app(props) function to create an application.
app({
view: () => <h1>Hi.</h1>
})The app function renders the given view and appends it to document.body.
To mount the application on a different element, use the root property.
app({
view: () => <h1>Hi.</h1>,
root: document.getElementById("app")
})The view is a function of the state. It is called every time the state is modified to rebuild the application's virtual node tree, which is used to update the DOM.
app({
state: "Hi.",
view: state => <h1>{state}</h1>
})Use the state to describe your application's data model.
app({
state: ["Hi", "Hola", "Bonjour"],
view: state => (
<ul>
{state.map(hello => <li>{hello}</li>)}
</ul>
)
})Use actions to update the state.
app({
state: "Hi.",
view: (state, actions) => (
<h1 onclick={actions.ucase}>{state}</h1>
),
actions: {
ucase: state => state.toUpperCase()
}
})To update the state, an action must return a new state or a part of it.
app({
state: 0,
view: (state, actions) => (
<main>
<h1>{state}</h1>
<button onclick={actions.addOne}>+1</button>
</main>
),
actions: {
addOne: state => state + 1
}
})You can pass data to actions as well.
app({
state: 0,
view: (state, actions) => (
<main>
<h1>{state}</h1>
<button
onclick={() => actions.addSome(1))}>More
</button>
</main>
),
actions: {
addSome: (state, actions, data = 0) => state + data
}
})Actions are not required to have a return value. You can use them to call other actions, for example after an asynchronous operation has completed.
app({
state: 0,
view: (state, actions) => (
<main>
<h1>{state}</h1>
<button onclick={actions.addOneDelayed}></button>
</main>
),
actions: {
addOne: state => state + 1,
addOneDelayed: (state, actions) => {
setTimeout(actions.addOne, 1000)
}
}
})An action may return a promise. This enables you to use async functions.
const delay = seconds =>
new Promise(done => setTimeout(done, seconds * 1000))
app({
state: 0,
view: (state, actions) => (
<main>
<h1>{state}</h1>
<button onclick={actions.addOneDelayed}>+1</button>
</main>
),
actions: {
addOne: state => state + 1,
addOneDelayed: async (state, actions) => {
await delay(1)
actions.addOne()
}
}
})Namespaces let you organize actions into categories and help reduce name collisions as your application grows larger.
app({
state: 0,
view: (state, actions) => (
<main>
<button onclick={actions.counter.add}>+</button>
<h1>{state}</h1>
<button onclick={actions.counter.sub}>-</button>
</main>
),
actions: {
counter: {
add: state => state + 1,
sub: state => state - 1
}
}
})Use events to get notified when your application is completely loaded, an action is called, before a view is rendered etc.
app({
state: { x: 0, y: 0 },
view: state => (
<h1>{state.x + ", " + state.y}</h1>
),
actions: {
move: (state, { x, y }) => ({ x, y })
},
events: {
loaded: (state, actions) =>
addEventListener("mousemove", e =>
actions.move({
x: e.clientX,
y: e.clientY
})
)
}
})Events can be used to hook into the update and render pipeline.
app({
view: state => <h1>Hi.</h1>,
events: {
render: (state, actions, data) => {
if (location.pathname === "/warp") {
return state => <h1>Welcome to warp zone!</h1>
}
}
}
})For a practical example see the implementation of the Router.
To create custom events, use the emit(event, data) function. This function is passed as the last argument to actions/events.
app({
view: (state, actions) =>
<button onclick={actions.fail}>Fail</button>,
actions: {
fail: (state, actions, data, emit) =>
emit("error", "Fail")
},
events: {
error: (state, actions, error) => {
throw error
}
}
})Use mixins to extend your application state, actions and events in a modular fashion.
const Logger = () => ({
events: {
action: (state, actions, data) => console.log(data)
}
})
app({
state: 0,
view: (state, actions) =>
<main>
<h1>{state}</h1>
<button onclick={actions.addOne}>+1</button>
</main>,
actions: {
addOne: state => state + 1
},
mixins: [Logger]
})