@composi/core is a JavaScript library that provides a virtual DOM and support for JSX functional components similar to React. It also includes a runtime environment with state management similar to Redux, but for functional components. It does this in just over 2KB. @composi/core’s functional components can have lifecycle hooks as well: onmount , onupdate , and onunmount . These are used at the element level, giving you more granular control of how you track the lifecycle of a component’s parts. There is no special syntax for component attributes. Use the normal HTML ones:

Just Three Functions

@composi/core has only three functions: h , render , and run . H is similar to React.createElement. It creates a virtual node representing a DOM tree . You can compose simple functional components together to form more complex ones. You use props to pass values to the virtual nodes and to pass values from parent to child component.

If you’ve used React before, all of this should feel very familiar.

Render

Once you’ve defined a functional component, you can output it to the DOM using the render function. This takes just two arguments, the component to render and the container to render into. This is similar to ReactDOM.render.

@composi/core’s virtual engine is very efficient. It only updates the DOM when the properties have changed. In the following example you can move the cursor over the image. This will create up to 2,000 rectangles and animate them in real time:

Hydration

The render function can also hydrate content produced on the server. Just provide the render function with a third argument — the element you want to hydrate. Render will take that element and create a virtual node from its DOM structure, using that to identify the difference with the component you are rendering. This results in efficient reuse of server-side content, faster load time and shorter time to interactivity.

Lifecycle Hooks

As we mentioned, functional components support three lifecycle hooks: onmount, onupdate, onunmount.

onmount

When you put an onmount hook on an element, the element gets passed as the argument of that hook’s callback. This is a convenient way to get a reference to a component’s DOM elements after it loads. No need for jQuery or cnnReact createRefs. In the following snippet we have an onmount hook on the input element. Its callback, setFocus , automatically gets a reference to that input, allowing us to set focus at load time.

Of course we could have just used autofocus on the input. This is just a simple example of how onmount works and what you might do with it.

onupdate

onupdate tracks the props of an element. If these change, it fires. Like onmount , onupdate gets passed a reference to the element it’s on. It can also get two more arguments: oldProps and newProps. A technical detail in this is that the element must have props and it is assumed that their values will change. If you put onupdate on an element that has no props, you will only get the element. onupdate can occur only after the element is mounted. So mounting does not count as an update.

Having access to the old and new props of an element allows you to examine them to see how the element is updating. From there you can decide what you want to do.

This following example shows a list with an onupdate hook. When new items are added to the list, it logs the old and new props in the browser console. Open your browser console and add an item to the list to see the output.

onunmount

The third lifecycle hook is onunmount . You would put this on an element that you expect will be removed from the DOM.

In the following example we are using onmount and onunmount to animate counters when the are added or deleted.

Run

Run is probably the most interesting part of @composi/core. The function takes on argument — a program to run. A program is an object literal with three methods: init , view and update .

Program

All @composi/core runtime programs will have the following structure:

State-View-Update

We call this pattern SVU — State, View, Update, with a message pipeline between View and Update for communication. State is just static data. It’s what your program consumes. It’s the one source of truth for your program. The view is a function that returns a representation of the state. The view function is agnostic to how you do this. For front end development this will involve using the render function to output markup to the DOM. On the server this could be a string for creating and HTML page. Or it could be used to output native code for a native app. View doesn’t care what the end result is, it just provides state for rendering. So, we know the view will enable rendering, but when does that happen? That’s the next piece — update . This is another function that get two arguments, a message and state. The message can come from user interaction with the view, or from a separate, running effect. According to the message, update can execute any number of provided actions. These are functions you define to manipulate program state. When an action is done manipulating state, it returns the new state. When it does so, the new state gets passed to the view method, triggering a re-render. It also updates the program’s state to the latest version. That is how SVU works in a nutshell.

Init

Init is a function that returns state and an optional effect. Note that init needs to always return an array, even if it has only one value. In fact, the most it can have is two values: state and effect. State is the initial default for the program. This can be any valid JavaScript type: undefined, null, string, number, array or object. Effect is a function that you want to run at startup. This will run asynchronously and independently from the rest of the program. You would use this to start a loop with setInterval, fetch data or open a web socket.

In the following example we have a program that has state set to 0 and an effect to run. This results in the program logging a count to the console. You’ll need to click on the Codepen icon on the top right to open it in another window and then open the console to see the result.

An effect can also have an argument: send. This is the same function passed to the view method. The send function allows an independently running effect to send messages to the program’s update method.

View

View is a function that gets two arguments: state and send. We’ve already looked at what state is above. The second argument is an internal runtime function that you can use in the view to send messages to the program’s update method. In our previous example, the program did not do anything with its view method. It was empty, so nothing was generated to the screen. To do that you need to provide the view with instructions to do so. With @composi/core you do this by providing it a functional component to render. As we saw previously, a functional component returns a virtual node representing DOM nodes to create. We can use @composi/core’s render function to output markup to the DOM.

The following snippet shows how to do a Hello World. We first create the functional component to render. We then create the program with the state to use. Then we render the component inside the view method.

And here we have a working example:

The view method can also use an optional second argument: send. This is an internal function provided by the runtime. You would use this in a functional component with an event to send a message to the update method. Continue reading to learn more about how this works.

Update

The update method is where all a program’s business logic resides. The update function is like a reducer in Redux. You define actions for it, and then pass it a message. Update examines the message and decides which action to use. Normally you would use actions for manipulating the program’s state. An action can also handle a message sent by an effect. Regardless of what an action is doing, when it is done with its task it has to return state. This is so even if the action did not modify state. Failure to return state in an action will result in a runtime error. That’s because the update method returns state to the view method. If the view is expecting to render a component and doesn’t receive state, there’ll be problems.

Actions

Here’s a simple example of an action inside the update method. With it we update the state with a new time value every second.

And here is this as a working example. In this one we have an effect that we run at startup. It runs a loop every second and uses the send function to send a message to the update method. Update then examines the message and updates the program’s state.

Here’s an example of a program whose init launches an effect that sends messages to the update method:

In the following example we have a more typical implementation of actions. Here they do indeed update state. In the update method we use a switch statement to check the type of the message to determine which action to use. If you’ve used Redux, this should look familiar. Notice how events in the view send the messages that are captured by the update method.

Messages

Message are the most important part of the runtime because they allow the three parts of a program to communicate. Essentially, the runtime is a messaging system where a program’s methods can talk to each other. There are no commands, just polite messages requesting action. It’s up to the receiver to decide whether to do anything when receiving a message. In some circles this would be referred to as signals. The messages being sent have this format:

{

type: 'do-something',

data: 'whatever'

}

Then in the update method, you would check the type to determine which action to use.

There is an alternate way to send messages — using tagged unions. These are implemented using the runtime’s union function. Its part of the runtime, so all you have to do is import it:

import { h, render, run, union } from '@composi/core'

A tagged union is an array of string values that gets associated with the update method’s actions. This allows you to use those actions directly in the view’s events. Here is how you create one:

const Msg = union(['addItem', 'deleteItem'])

We can then use this tagged union directly in the view to invoke the appropriate action. No need for a message:

<button class='add-item' onclick={() => send(Msg.addItem(inputValue))}>Add</button>

The interesting thing about using a tagged union like this is that when you send that function, what actually gets sent is a message with the same format as if you had created it without the tagged union. So under the hood tagged unions are just sending the same messages you would have to write out by hand. So, why use them? They also reduce the code in the update method, making it more compact and easier to read. In the following snippet we have actions with a tagged union, followed by actions without. Without the tagged union we have to use a switch statement with case and break and we have to directly access the message object to determine its type and use its data. With a tagged union we use its match method to determine what message was sent and we get direct access to its data through the callback:

Here’s the previous interactive list updated to use a tagged union for actions:

Composition

It is possible to compose runtime programs together. After all, this library’s name is Composi. Below is an example of a program with another child program:

Using messages and effects, you can have the parent and child communicate. In the following example we use an effect to let the parent know what the child counter reaches the maximum allowed value of 10. Click on the counter 11 times to see the result:

Summary

Despite its diminutive size, @composi/core provides powerful features for creating apps. A dedicated command line tool makes it easy to create a Composi project: @composi/create-composi-app. This gives you everything you need to develop an app with @composi/core. You can learn more about @composi/core from its website, and especially by perusing the documentation.

@composi/core combines the utility of a virtual DOM, functional components and lifecycle events inspired by React, with the simple elegance of Elm-style state management and messaging, all in about 500 lines of code. Rather than lots of features, some approved and some marked dangerous or unsafe, @composi/core is focused on reduced complexity. We believe @composi/core has all the features it needs to create complex and powerful apps. The small API surface means you should be able to be productive in an hour or even less. After one day of use you should be familiar with the entire API. Lastly, there are no special tricks or techniques to worry about to optimize performance.