@composi/runtime:
Runtime
State Management for Functional Components
This section assumes you already know how to create and use functional components with @composi/core. If not, please visit this section.
It's amazing how much you can do with just h
and render
. However, more often your component will need some kind of local state. Class-based components can easy accomplish this. @composi/core only provides functional components, which cannot have local state. @composi/core gets around this limitation through its runtime, which provides a scope for encapsulating state and a mechanism to re-render views when state changes. @composi/core's runtime lets you create a program where the concerns are broken down into clearly defined responsiblities.
SVU: State-View-Update
The Composi runtime is based on the concept of SVU, State, View and Update. This is similar to MVC, but subtly different. In traditional MVC, the model holds not just the data, it can validate the data and persist it in a database. With SVU, State is just raw, inert data. It has no logic for anything. In SVU, View is a function that takes the state and returns a representation of it in some form. The view is agnostic to how this is done. It's your responsiblity to instruct the view how to return the state. This could be merely returning a functional component to be consumed by something else, or using the @composi/core render function to output the result to the DOM. When you render a View, you'll implement events for user interaction. When a user interacts with those events, they send messages to the Update function. The Update function holds all the program's business logic. It handles this internally through action functions. Depending on the message sent, Update passes it to an action that modifies the state. When this happens, the runtime invokes the View with that state, causing the View to return a result. For front end, this means a re-render of the view.
If the above illustration is a little too technical for you, then here's a another one that shows how a runtime program works. As a user, you interact with the view by sending messages. These are intercepted by the update method, which then manipulates the state, causing the view to change.
Every @composi/core runtime program implements this SVU pattern. It does so with three methods: init, update and view. Init is where you define the default state for the program. At startup the runtime takes the value returned by init and uses it to render the view. The following example shows the core parts of a runtime program.
const program = {
init() {
return state
},
view(state, send) {
// Render view that can send messages to update:
return render(Component, Container)
},
update(state, msg, send) {
if (msg) {
// Handle messages to update state.
// Then return state.
return state
}
}
}
Init
Init is a function that returns an array of two values: state and effect. At startup the program uses the state that init provides to render the view. Effect is optional. If provided it will be run at startup too. Effects are asynchronous functions for timers or fetching data. Most of the time init will just provide state. A program's state can be any JavaScript type: null, undefined, number, string, array or object. Complex programs will inevitably need state as an array or object.
View
The View is the representation of the program's state. A view usually has events for user interaction. An interaction will send a message to the program's Update function.
Update
Update is where all the program's business logic resides. Update receives messages sent by the view. Depending on what message was received, Update performs actions on a copy of the program state and returns it. This causes the program to re-render the view with the new state.
Here is the simplest possible runtime program that you can make. This is valid and it is running, but doing nothing.
See the Pen @composi/core + runtime Basic by Robert Biggs (@rbiggs) on CodePen.
Order Doesn't Matter
When creating a program to run, the order of the properties is irrelevant. In our examples we use init, update, view. But you could choose to do init, view, update or any other order that you prefer.
const program1 = {
init() {},
update() {},
view() {}
}
const program2 = {
init() {},
view() {},
update() {}
}
const program3 = {
view() {},
init() {},
update() {}
}
If you do have a prefered order, stick with it for consistency.
Clicker--The Hello World of Programs
To illustrate setting up a program and running it we're going to make a simple clicker. State will be a number. View will send a message to update, and update will then increase the state value by 1.
See the Pen @composi/core + runtime - counter by Robert Biggs (@rbiggs) on CodePen.
An Object is an Object
In the above example you will notice that we have created an object program
that we pass to the run
function. This has some parallels to a class component. A class instance is an object returned by a class. The difference is in how these two are used. With class components its common to add new properties to the class to handled new functionality. In contrast, a @composi/core runtime program only has the three main methods: init, update and view. Any new funtionality that the program needs would be handled by actions in the update method. This forces a ver clean separation of concerns: init handles initial state for the program, view presents the program's state, and update handles the program's effects.
The previous clicker example is overly simplistic, but it does show the basic setup for a runtime Composi program: init, view and update. You can learn more about them from their dedicated docs from the menu at the top.
Part of Core
The runtime is part of @composi/core, so importing and using it does not add anything to the final payload of your project's JavaScript. @composi/core is 3KB gzipped.
When we say the runtime is small, we mean it. Here's all the code for the runtime minified and completely functional:
function run(a){function b(m){if(j)return c(f(i,m,b))}function c(m){m?i=m:d&&(i=d()),g&&k&&('function'==typeof
g&&g(l,b),k=!1),e(i,b)}let d=a.init;const e=a.view,f=a.update,g=a.subscriptions||a.subs,h=a.done;let i,j=!0,k=!0;const
l=()=>i;return a.send=b,c(i),()=>{j&&(j=!1,h&&h(i))}}
Download Examples
You can download examples of simple and complex runtime programs on Github.