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:
- Virtual DOM
- Functional Components
- Unidirectional Data Flow
- Passing Props Down
- Lifecycle Hooks
- 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.
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:
Let’s look at what each of these does for the program.
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.
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:
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
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.
The above is a valid Composi program which we can run:
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.
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:
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:
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
To render our list component, we use the render function inside the view method. Notice that we return it:
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
A message could be anything — a string, a number or an object. The convention is to use an object with the following format:
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:
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:
Now that we have an actions function, we can have our
update method use it:
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.
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:
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:
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.
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.
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
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.
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:
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.
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:
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.