Understanding Composi Runtime Programs

TruckJS
TruckJS
Feb 4 · 8 min read
Dynamic List Created with Composi

State & Effects

Composi is a functional programing library inspired by React and the Elm architecture. Composi provides a very clear way of handling a program’s state and effect. I like React, but a year ago I was thinking about the state of React as a platform. The API has become very verbose and confusing. Features that that were once popular are now labeled unsafe. I was starting to think that React was showing it’s age and becoming bloated. So I decided to make a list of what I liked about React. Essentially this would be React--the Good Parts. This was my list:

  1. Virtual DOM
  2. Functional Components
  3. Unidirectional Data Flow
  4. Passing Props Down
  5. Lifecycle Hooks
  6. Strong Story for State Management

You’ll notice there are no class-based components in the list. I decided they weren’t necessary. From my meager experience writing Elm programs I knew that it was possible to provide robust state management without the need for class components.

I had previously played with a number of virtual DOM libraries and had created several rudimentary ones myself. So coming up with a virtual DOM was not a big deal. Functional components were not hard either. All you need to make them happen was a function that could take arguments and return a virtual element that the virtual DOM engine could understand and convert into real DOM nodes. Babel would do the actual conversion during build time. Having functional components that become virtual elements means you automatically get the passing of props, which includes passing of data down. The only thing was lifecycle hooks. I had used Inferno before, which happens to implement lifecycle hooks for functional components, so I knew this was possible. These get implemented directly in the markup of a functional component. The implementation was easier than I thought.

State Management

The big question was how to provide state management for functional components. React class components do this with the setState method. Elm has the concept of a program where functional components are rendered and state is managed. This provides state management for programs in a manner very similar to Redux, using messages and actions. In fact, Elm was the inspiration for Redux.

Get with the Program

In the Elm architecture you create a program that has several pre-defined functions:

  1. init
  2. view
  3. update
  4. subscriptions

Let’s look at what each of these does for the program.

The init function sets up the initial state for a program.

view returns a representation of the program's state.

update runs actions, which are functions to handle user interactions with the view. Actions handle effects that manipulate state, store state locally, fetch data, etc. update is how you handle the effects for the program.

subscriptions are effects that launch when the program starts. These might be for fetching data, either locally or remote, or starting some time of polling interval, setting up events to monitor screen resize or key presses.

I always liked how Elm forced you to separate out all of a program’s functionality into clearly defined sections. This led me to create the Composi runtime, a JavaScript equivalent of an Elm program. This gets consumed by the Composi run function. You create a Composi program and then run it. A Composi program encapsulates the state. It also enables the automatic re-rendering of the view when state is updated.

Composi = React — the Good Parts

Composi is just 3KB in size. It has three main functions: h, render and run. If you've used React.createElement, h does the same thing. If you use JSX to define the markup of your functional components, Babel can use Composi's h function to convert them into virtual nodes. If you create a project with @composi/create-composi-app, it will automatically be set up to do this for you. The render function is like ReactDOM.render. It takes two arguments: a function component and a DOM element to render the component in. This will render the functional component into the DOM. After that, if you again render the component with new data, Composi will use its virtual DOM to patch the already rendered component to match the changes.

Run a Program

The run function is what sets Composi apart. This function runs a Composi program, which has the same format as an Elm program: init, view, update, subscriptions.

A basic Composi program

The above is a valid Composi program which we can run:

Running a Composi program

Note that unlike render, which you might call multiple times to update a functional component, you only run a program once during a browser session.

Program State

Running a basic program like this one will not do anything. We need to make the program methods do things. The first thing we can do is give it state. We’ll use a state object like the following:

Program state for a list component

Composi programs use standard JavaScript data types as state: booleans, strings, numbers, arrays, objects. In this example, we have an object with several properties, one of which is an array of objects. To use this as the state of our program we need to return it in the init method:

Show Program State

Now our program has state, but we have no way of seeing it in the browser. To do that we need to enable the program view method to do so. The view method does not know how to present the state, that’s up to us to do. We can do this by creating a functional component and having the view method use the render function. Based on our state object, we will use the following functional component:

A functional component to create a list of items

Notice that we pass two values to the list component: state and send. State will be received from the program’s view method. Send is an internal function of a Composi program that automatically gets passed to the view method. It allows us to send messages to the program. You do this when the user interacts with the program through DOM events. We’ll see how to handle sent message when we look at the program’s update method.

To render our list component, we use the render function inside the view method. Notice that we return it:

Rendering the List component

Running our program now will show a list in the browser using the current state of the program. When you pass a program to the run function, it first executes the program's init method. This returns whatever state you provide. When init returns state, this gets passed to the program's view method, which can use that state to render a representation of said state. In our example above we pass the program state to the list component to render.

Update — Handle User Interaction

So now we have a rendered list component. It has an input and an Add button and some delete buttons on the list items. But these do nothing. To bring this list to life we need to add events. To enable the events to communicate with the program when need to send messages. And to do something when messages are sent we need to add some behavior to the program’s update method.

Messages

A message could be anything — a string, a number or an object. The convention is to use an object with the following format:

Structure for a message

The type is the name for the action to perform, and value is any optional data we want to pass along with the message.

Our list has two main things going on: adding a new item and deleteing and existing item. To add a new item we need to know what is the current value of the input element. We don’t want to have the add item code to also have to query the DOM to get the value of the input. This is messy and unnecessary. Instead we can register an event on the input that will send a message when its value is updated by the user typing. Since the state object has a property to hold input value, we can use this message to udpate that value as the user types. Then the add item action can just use the value of the state’s inputValue property when adding a new item.

Update State from User Input

To get the value of the input element as the user types, we’ll use an oninput event to send a message:

Input with event that sends a message with the input value

In the above code snippet, notice that we use the oninput event to send a message object. The value that we send we get from the event target.

Creating an Actions Function

Now that we have our first message, we need to create an actions function to process it. Actions is function that processes a message to do something, such as update the program’s state. If you’ve used Redux before, this format will look familiar:

Actions for the program’s update method

Now that we have an actions function, we can have our update method use it:

Using actions inside a program’s update method

By returning the actions method, passing it the program state and whatever message was received, the program with pass the returned state to the program’s view method. If the state changed, it will re-render the view using the virtual DOM. Because state.inputValue is not used by the list component, no render will actually occur.

Add Item

Now that the user can update the inputValue of the program’s state, we can enable the Add button to add a new item to the list. To do this we’ll add an onclick event to the button and send a message:

Using an event to send a message to add an item to the list

Notice that here our message doesn’t have a value, just a type. We’ll get the value to add from state.inputValue. To add an item, we need to update the actions method to handle the new message:

Adding a new action to add an item to the list component

With the above change to our actions, we can now type in a value in the input and click on the add button. This should cause the list to render with the new item we just created. Notice how we use state.newKey++ to create a unique key for the new item.

Delete Item

To delete an item we need to add an onclick event to the list item delete button. This will send a message, so we'll need to update the actions function afterwards.

Using an event to send a message to delete a list item

Notice how we use the item key as the value of the message we send. This will allow us to filter it out from the array of items in the state.

Here is the updated actions function:

Adding an action to delete an item from the list component based on the key value in the message

We now have a fully functional, dynamic list. We can add items and delete items. The list component is very clean, just simple events that send messages. Actions hold all the buisness logic. A Composi program provides a very tight pattern for organize code to handle complex and often difficult to handle effects.

Immutable State

One thing we did not do is show how to keep state immutable. You can do this by using destructuring. You would do this in the program's update method and pass the result to the actions function:

Clone the state before passing to actions for immutability

By destructing the state in the actions method means that any changes you make to state in an action won’t affect the program’s state until you return it. Keeping state immutable like this helps prevent hard to track state bugs.

Subscriptions

Our program has a subscriptions method, but we haven’t used it yet. Now we will. We’ll use it to set up an event listener for when the user hits the Enter key while in the input element. We’ll use this to enable the user to type and then hit Enter to add a new item to the list:

Adding a subscription to the program

And that’s it. There are other things we could do. But this illustrates how to create a fully functioning Composi program.

And here’s the working example:

If you examine the code in this example, you’ll see how a Composi runtime program automatically separates out state management and effects. This leads to better code organization, more readable code, better cooperation among team members and code that is easier to maintain.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade