Building a Task List Application with Thermite

by Phil Freeman on 2015/11/20


Introduction

Thermite is a simple layer on top of the purescript-react library which supports the "single state atom" approach, popularized by ClojureScript's Om and the Elm Architecture.

The library has been changing rapidly since the it was created, but the API is finally starting to become stable, so I'd like to write a blog post covering the basics of creating applications with Thermite.

Anatomy of a Thermite App

A Thermite application is defined by its specification, which is parameterized by four type arguments:

newtype Spec eff state props action

In Thermite applications, we rely less on component-local state, choosing instead to represent all application state using a single monolithic value. In the Spec type, the type of this value is named state.

The action type describes the actions which we can use inside our components to update the state and invoke external, asynchronous actions.

The props type corresponds to the React component props, and is relatively infrequently used. We won't use it here.

Finally, the eff argument represents the effects which our component will use. For an introduction to effects in PureScript, see the article on the PureScript website.

We can create a specification for our component by using the simpleSpec function:

simpleSpec ::
  forall eff state props action.
    PerformAction eff state props action ->
    Render state props action ->
    Spec eff state props action

simpleSpec takes two arguments: a PerformAction function, and a Render function. PerformAction will be responsible for interpreting actions invoked by the user, and Render will render our component as a React virtual DOM tree. The type synonyms are defined like this (eliding unimportant details using type wildcards):

type PerformAction eff state props action 
  = action -> _ -> state -> (state -> Eff eff Unit) -> Eff eff Unit

type Render state props action 
  = (action -> EventHandler) -> _ -> state -> _ -> Array ReactElement

PerformAction takes the current component state, and an action, and interprets that action. It also has access to a callback of type state -> Eff eff Unit, which can be used to update the component state.

Render takes the current component state, and an event handler which can be used to invoke an action, and returns an array of virtual DOM trees representing the component.

And that's it! We have enough information to define our first Thermite component.

Let's start simple, by defining a counter component. First we define our state and action types:

import qualified Thermite as T

import qualified React as R
import qualified React.DOM as R
import qualified React.DOM.Props as RP

data Action = Increment | Decrement

type State = { counter :: Int }

Our State type is a record containing the value of the counter, and the Action type defines two actions, Increment and Decrement.

The render function might look like this:

render :: T.Render State _ Action
render send _ state _ =
  [ R.p' [ R.text (show state.counter)
         , R.button [ RP.onClick \_ -> send Increment ]
                    [ R.text "Increment" ]
         , R.button [ RP.onClick \_ -> send Decrement ]
                    [ R.text "Decrement" ]
         ]
  ]

The send function here is the event handler that can be used to invoke actions. Notice how it gets wired up to the button elements.

The PerformAction function interprets an action by matching its constructor using pattern matching:

performAction :: T.PerformAction _ State _ Action
performAction action _ state update = 
  update { counter: case action of 
                      Increment -> state.counter + 1 
                      Decrement -> state.counter - 1
         }

The update function is the callback which allows us to update the state atom. We call the update function, providing a new record containing the new counter value.

We can use simpleSpec to create our specification from these parts:

spec :: T.Spec _ State _ Action
spec = T.simpleSpec performAction render

From here, we can use the createClass function to turn our specification into a regular React component class by specifying the initial component state:

main = do
  let initialState = { counter: 0 }
      component = T.createClass spec initialState
  ...

purescript-react defines functions for rendering a component class, or we could use the foreign function interface to use our component class from Javascript code. We won't cover those topics here, but the Thermite example project contains an end-to-end application for reference.

A Task Component

Let's start to assemble our task list application. Our application will consist of three components:

Let's start with the first and simplest component here - the task component. As before, we start by choosing action and state types for our component. In this case, these types are very simple:

data TaskAction
  = EditText String
  | RemoveTask

type TaskState = { text :: String }

We have two actions - EditText, which will update the text for a task, and RemoveTask, which will remove it from the list.

Now let's implement our PerformAction function to interpret these actions:

performAction :: T.PerformAction _ TaskState _ TaskAction
performAction (EditText text) _ _ update = update { text: text }
performAction _ _ _ _ = pure unit

The interpretation of EditText is straightforward, but RemoveTask is missing. Instead, the RemoveTask falls through the first case to the catch-all case which performs no action.

This is because the task component does not understand how to remove itself from a list of tasks - there is no list yet! As we'll see, the parent component will be responsible for interpreting the RemoveTask action instead.

Now let's implement the Render function. It is also straightforward:

render :: T.Render TaskState _ TaskAction
render send _ s _ =
  [ R.p' [ R.input [ RP.value s.text
                   , RP.onChange \e -> send (EditText (unsafeEventValue e))
                   ] []
         , R.button [ RP.onClick \_ -> send RemoveTask ] [ R.text "✖" ]
         ]
  ]

This function looks like our counter component's render function. However, here we need to deal with the onChange event of the input element. The unsafeEventValue helper function is used to extract the new text from the form event:

unsafeEventValue :: forall event. event -> String
unsafeEventValue e = (unsafeCoerce e).target.value

Thermite does not provide any tools for dealing with DOM events, hence the call to unsafeCoerce here. Other libraries may provide safer, alternative approaches.

At this point, we could call createClass to create a React component class for a single task. However, we are instead going to compose our task component with another component to build our application.

A Header Component

We are going to compose our task component with a header component to build our task list application. We will use the same action and state types for the header as for the full application component:

data TaskListAction
  = NewTask
  | TaskAction Int TaskAction
  
type TaskListState = { tasks :: L.List TaskState }

The action type identifies two classes of action: the NewTask action, which creates a new task, and the TaskAction action constructor, which identifies any TaskAction as an action in the full TaskListAction type. A TaskAction is also tagged with an Int, the index of the task which it originated from.

The state type is simply a record containing a list of task states.

The header component is defined as follows:

header :: T.Spec _ TaskListState _ TaskListAction
header = T.simpleSpec performAction render
  where
  render :: T.Render TaskListState _ TaskListAction
  render send _ state _ = 
    [ R.p' [ R.button [ RP.onClick \_ -> send NewTask ] 
                      [ R.text "New Task" 
                      ] 
           ]
    ]

  performAction :: T.PerformAction _ TaskListState _ TaskListAction
  performAction NewTask                   _ state update = 
    update $ state { tasks = L.Cons { text: "" } state.tasks }
  performAction (TaskAction i RemoveTask) _ state update = 
    update $ state { tasks = fromMaybe state.tasks (L.deleteAt i state.tasks) }
  performAction _ _ _ _ = pure unit

The interesting part here is that the RemoveTask actions from the child components are handled by the header. This is because the header has access to the full application state, so is able to modify the list at an index.

Now that we have our individual components defined, we can compose them to build our application.

Lens Primer

In Thermite, components are composed using lenses and prisms, defined in the purescript-profunctor-lenses library. I will not cover lenses and prisms in detail here, but I will show how to create simple lenses and prisms for use with Thermite.

Intuitively, a lens represents a pair of a getter and a setter for a property of one type inside another, larger type.

We can create a lens using the lens function, by passing in an explicit getter and setter. For example, we can create a lens for the tasks property on our TaskListState record:

_tasks :: LensP TaskListState (L.List TaskState)
_tasks = lens _.tasks (_ { tasks = _ })

The first type argument identifies the larger type, and the second type argument identifies the type of the property we are interested in.

The definition of the _tasks lens uses record wildcards for both the getter and setter, but desugars into this simpler expression:

_tasks = lens (\state -> state.tasks) 
              (\state tasks -> state { tasks = tasks })

If we can think of lenses as generalized property accessors, then a good intuition for prisms is as generalized data constructors. For example, we can create a prism for the TaskAction data constructor using the prism function, as follows:

_TaskAction :: PrismP TaskListAction (Tuple Int TaskAction)
_TaskAction = prism (uncurry TaskAction) \ta ->
  case ta of
    TaskAction i a -> Right (Tuple i a)
    _ -> Left ta

The power of lenses comes from their composability: lenses and prisms are both examples of the more general concept of optics, and we can compose different types of optics to reach deeply into our data structures in different ways.

Composing Components

The two most important combinators for composing components in Thermite are focus and foreach.

focus uses a lens to identify the state type of a subcomponent as a smaller part of the state type of a parent component, and a prism to identify the action type of the parent component as constructible from the action type of the subcomponent. Here is its type:

focus :: forall state1 state2 action1 action2. 
         LensP state2 state1 -> 
         PrismP action2 action1 ->
         Spec _ state1 _ action1 -> 
         Spec _ state2 _ action2

The foreach function allows us to create a specification for a component from a list of subcomponent specifications. Its type is:

foreach :: forall _ _ state action. 
           (Int -> Spec _ state _ action) -> 
           Spec _ (L.List state) _ (Tuple Int action)

Note that foreach keeps the action type the same, but modifies the state type to accomodate a list of states.

Aside: These combinators were inspired by the excellent OpticUI library, which also uses optics for composing components, but in a more general way (the list of subcomponents passed to foreach is replaced by a more general traversal of the component's state type).

With these combinators, we can assemble the specification for our application component as follows:

taskList :: T.Spec _ TaskListState _ TaskListAction
taskList = header <> T.focus _tasks _TaskAction (T.foreach \_ -> taskSpec)

Let's pick apart this definition.

First of all, the Monoid instance for Spec allows us to compose the Spec for the header with the Spec we get back from focus, by using the <> operator. The Monoid instance for Spec appends virtual DOM trees, one after the other, and composes PerformAction functions so that all action handlers get run.

Next, the expression T.foreach \_ -> taskSpec turns our task component Spec into a specification of type Spec _ (L.List TaskState) _ (Tuple Int TaskAction). Here, the Int appearing in the action type is used to apply any TaskActions from a task subcomponent to the correct state value.

Finally, the focus operator applies the _tasks lens and _TaskAction prism, to make the state and action types match the types for the application component.

And that's it! We can now create a React component class for our application and attach it to the DOM.

main :: Eff (dom :: DOM.DOM) Unit
main = do
  let component = T.createClass taskList { tasks: L.Nil }
  ...

This gist contains the full, working code from this tutorial, and the Thermite repository contains a more complete task list application, demonstrating different types of composition.

Conclusion

Thermite provides a simple way to specify React components using a purely functional approach, separating a component into a rendering function, and an action interpreter.

In addition, we can compose components using lenses and prisms, which allows us to build our application in parts, using reusable pieces. Since lenses and prisms are first-class values, we can pass them as arguments to functions, to enable powerful forms of reusability.