Skip to content

Instantly share code, notes, and snippets.

@Raynos
Last active August 18, 2022 22:31
Show Gist options
  • Save Raynos/5219544 to your computer and use it in GitHub Desktop.
Save Raynos/5219544 to your computer and use it in GitHub Desktop.
Exploration of a TodoMVC app using FRP javascript techniques.

Implementing TodoFRP in JavaScript

FRP to me means building your app by transforming values over time, from the input to the current state to the display.

This implementation is based on a the graphics library and is heavily inspired by Elm

A full implementation of TodoFRP can be found online at Raynos/graphics example server

Moving away from MVC

In building this TodoFRP app we have moved away from the MVC paradigm to a more reactive & declarative paradigm.

Our application is build of a few building blocks. An important start of the application is defining what the application state actually is.

Defining the model

// Application Model
var TodoModel = { id: "", title: "", completed: false, editing: false }
var TodosModel = { todos: [], route: "all" }

Here we have defined the possible states of the objects in our system for documentive purposes. Once we have an idea of what the possible application states are we can start describing what the inputs to the system will be.

Note that we define inputs up front and do not dynamically create mutable view objects that start randomly mutating models at run-time.

Describing inputs

// Inputs
var addTodoPool = EventPool("add-todo")
var toggleAllPool = EventPool("toggle-all")
var modifyTodoPool = EventPool("modify-todo")
var clearCompletedPool = EventPool("clear-completed")
var routes = Router()

In our application we have defined for event pools that DOM events will eventually send messages too and have also created an instance of a Router (which uses hashchange) so that we can treat URI changes as an input.

Once we have declared our inputs we should define how changes in an input allow for changes to happen in the application state.

Describing state operations

// State operations
var addTodo = transform(addTodoPool.signal, function (todo) {
    return function (state) {
        return todo.title === "" ? state :
            extend(state, { todos: state.todos.concat(todo) })
    }
})

...

var handleRoutes = transform(routes, function (routeEvent) {
    return function (state) {
        return extend(state, { route: routeEvent.hash.slice(2) || "all" })
    }
})

Here we have defined that we should transform the state of the AllTodo message pool into a function which takes our current state and returns a new state. In this case we check whether the new todo has a non-empty title and if so we just concat it onto the list.

We have also defined a transformation for the state of the routes. Which again is a function that takes current state and just updates the route property (or defaults it to "all").

We would continue to write transformations for each one of our inputs that describe how to turn the input state into a change to our application state. You can find further example of transformations in the application source

So if we have these Signals of modification functions, how do we turn that into an actual representation of the application state?

Describing the appState

var input = merge([addTodo, modifyTodo, toggleAll,
    clearCompleted, handleRoutes])

var storedState = localStorage.getItem("todos-graphics")
var initialState = storedState ? JSON.parse(storedState) : TodosModel

// Updating the current state
var todosState = foldp(input, function update(state, modification) {
    return modification(state)
}, initialState)

todosState(function (value) {
    localStorage.setItem("todos-graphics", JSON.stringify(value))
})

In our case we merge together all these modification functions into a single Signal.

We make sure we get a description of initialState either from the model we defined earlier or from local storage if we have one.

We then take our initialState and each modification function and just accumulate the representation of our current state.

Note we also listen on changes to the appState and write them to localStorage.

We now have a signal that describes our entire application state! Now how would you go about building a UI from that? Could you, maybe just transform it into some kind of representation of a scene?

// Various template functions to render subsets of the UI
function mainSection(state) {
    var todos = state.todos
    var route = state.route

    return h("section.main", { hidden: todos.length === 0 }, [
        toggleAllPool.change(h("input#toggle-all.toggle-all", {
            type: "checkbox",
            checked: todos.every(function (todo) {
                return todo.completed
            })
        })),
        h("label", { htmlFor: "toggle-all" }, "Mark all as complete"),
        h("ul.todo-list", todos.filter(function (todo) {
            return route === "completed" && todo.completed ||
                route === "active" && !todo.completed ||
                route === "all"
        }).map(todoItem))
    ])
}

...

var app = transform(todosState, function display(state) {
    return h("div.todomvc-wrapper", [
        h("section.todoapp", [
            headerSection,
            mainSection(state),
            statsSection(state)
        ]),
        infoFooter
    ])
})

We transform our application state using a display function that just builds up a representation of what our app looks like visually. In our case we use a h function that's similar to hyperscript as a terser equivalent for DOM templating. This approach works for any templating system.

In our mainSection function we do useful things like only showing the sections if we have todos and we also decorate our toggle all button with a change listener from the toggleAllPool. This means that whenever a change event happens on the toggle button a message get's send to the toggleAllPool.signal.

We can write very simple obvious logic like saying that the toggle button should only be checked if every todo in our list is completed. To create a todo item to render we can just do todos.map(todoItem) where todoItem is just a single javascript function that returns a representation of the todo state.

Putting it all together

// Render scene
render(app, false)

To actually make everything flow cleanly we call render on our application which use an efficient diff based rendering system to avoid the computational overhead of recreating large chunks of the scene each time our application state changes.

Immutability & purity

If you look at our application we never had to go mutate things or set up event handlers or write code like "if this things happen then do that thing". There is no glue code. There is just a set of pure transformations that take old values and return new values without mutating

Interesting ideas

  • We transform our inputs into transformation functions and then merge those together and apply them on current state. We could this more cleverly and write transformation functions that only update a subset of the state, we can then delegate from our foldp to a subset and avoid having to recompute large part of the state each time. This could scale in setting up a recursive structure for describing your application state where each subsection has it's own sets of inputs it cares about.
  • We use h as a nice pure JavaScript templating mechanism that has all the expressive-ness of JavaScript but still looks similar to HTML templates. This gives us our pure FRP style benefits whilst still being able to use CSS.
  • We use event pools to get around the chicken and egg problem of dynamic views wanting to create dynamic inputs that should effect the state. We declare a finite set of event pools upfront and those event pools use event delegation on the document internally.

Open questions

The inputs & state transformations are written in a reasonably clean fashion.

The part of the example that's terribly ugly is all the display logic

  1. How do you avoid recreating an entire scene object each time the state has a small change? Maybe we can use something similar to the transforming things into functions that operate on state. This is a MASSIVE blocker on production usage.
  2. There is far too much logic in the templates. Is there an intermediate layer we can move it to.
  3. The pool api will probably need some polish. It's usage to wrap elements doesn't look as nice as wanted.
  4. How can we split up all the rendering logic, updating logic & inputs into multiple files?
@mikolalysenko
Copy link

Functional reactive programming sounds like the subset of constraint solving where the only types of constraints you are allowed to use are functional (and so solving stuff is trivial). Have you seen this project? I think it does what you are describing pretty much exactly:

https://github.com/soney/constraintjs

Also regarding visualization, it FRP might work a lot better with WebGL for the rendering, since the drawing model is way simpler. (ie you basically have a function which gets called every 16ms that redraws the screen from scratch)

Anyway, just my 2c.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment