Technology

Stores as Stream Observers, Part 2: FRP && ReactJS


How the heck can stores “hold” state in FRP?

Before we talk about how Stores look like in functional reactive programming (FRP), we need to discuss how Stores can even store/manage state in FRP. Technically speaking, in functional programming (FP), functions are not supposed to store state between successive calls.

There are three strategies:

  1. use scan (aka reduce).
  2. move state to a global component, say ApplicationState.
  3. a combination of the two

Use the scan Luke

This approach is the most similar to Flux.

Basically, in FP, you get around not being able to store state by passing around state (aka accumulating state) in function arguments. For example, the code below shows the factorial function, FP style. Note how the function argument acc accumulates the result of successive function calls.

function factorial(n, acc=1) {
    if (n <= 1) return acc
    return factorial(n-1, n*acc)
}

In FRP, you can store successive stream values using the scan function (which works just like reduce)

//myStream = 0, 1, 2, 3, ...
const store = myStream.scan(
    (storedValues, curValue) => storedValues.concat[curValue], []
)

store.onValue(x => console.log(x))
// [0]
// [0, 1]
// [0, 1, 2]
// [0, 1, 2, 3]
// etc

Solution 1: The Straightforward Approach

This leads to a straightforward implementation of a store:

// TodoStore.js
import AppDispatcher from 'AppDispatcher'
import uuid from 'uuid' //module for generatin' IDs

// pull functionality into functions and export for easy testing
function _createTodo(todos, todo) {

    todo.id = uuid.v4() // easy-peasy way to get an id
    todos[id] = todo
    return todos
}

function _scanner(todos, action) {

    switch(action.actionType) {
        case 'create':
          return _createTodo(todos, action.payload)
        default:
          return todos;
    }
}

const todoStore = AppDispatcher
    .filter(x => x.channel === 'todo')
    .scan(_scanner, {})

export default {
    _createTodo,
    _scanner,
    todoStore
}

Solution 2: Action Streams

Alternatively, we can create action streams for each action and merge them using the FRP library. For example:

// yet another store
import AppDispatcher from 'AppDispatcher'
import uuid from 'uuid'
import Kefir from 'kefir'

const todoActions = AppDispatcher.filter(x => x.channel === 'todo')

// export for testing
// note that we return a higher-order function
function _createTodo(action) {
    return function _createTodoFunction(todos) {
      const todo = action.payload
      todo.id = uuid.v4()
      todos[id] = todo
      return todos
  }
}

function _updateTodo(action) {
    return function _updateTodoFunction(todos) {
        const todo = action.payload
        todos[todo.id] = todo
        return todos
    }
}

const createActionStream = todoActions
    .filter(x => x.actionType === 'create')
    .map(_createTodo)

const updateActionStream = todoActions
    .filter(x => x.actionType === 'update')
    .map(_updateTodo)

// the way "merge" works in FRP libraries
// is that it fires each time one of the action
// streams receives an update
const todoStore = Kefir
  .merge([createActionStream, updateActionStream])
  .scan((todos, modificationFunction) => modificationFunction(todos), {})

export default {
    _createTodo,
    _updateTodo,
    createActionStream,
    updateActionStream,
    todoStore
}

The Shootout: The Straightforward Approach vs Action Streams

The main advantages to action streams are

  • an action can be anything. (On the other hand, I can’t think of an action that can’t be handled by the straightforward approach.)
  • knowing when specific actions happened is trivial—just observe the action stream!

The main disadvantage to action streams is that the code is slightly harder to test—at the very least, you’ll need to mock out the AppDispatcher so that you can test the action streams are hooked up correctly.

Actually, everything but the functions are boilerplate. You can write a helper to create the action streams. Then another helper can create the store. Then all the unit test needs to do is check that (1) the functions work as expected and (2) the helpers are called with the right arguments.

So there you have it—Stores in FRP. In case you need remindin’, we’ve been able to replicate Flux Stores using only stock FRP functionality!

Global State

Stay tuned!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s