Technology

Global State aka No More Stores, Part 3: FRP && ReactJS


An app written in the functional-programming style has no clear way of storing state—functions are not supposed to store state between successive calls (in an imperative way, like using an object variable.)

A common approach in FP for solving the state problem is global state—a data structure represents the global state. Top-level functions operate on the global state.
As we’ll see, in contrast to Action Streams, this approach diverges from the Flux architecture.

Dude, Where Are My Stores?

Solution 1: The Redux

Let’s suppose that we store global state in a single place called AppState. It would be a really dumb idea to put all (unrelated) logic in one place. Therefore, as in a normal Flux app, it still makes sense to have stores (like TodoStore) handle updates for their respective domains. However, because stores aren’t allowed to store/hold/accumulate state, stores become nothing but pure functions (containing domain-specific logic). Essentially, we just stumbled on the Redux architecture.

// TodoStore.js aka TodoReducer (in Redux parlance)
function createTodo(todos = {}, action) {
    // Adds an entry to todos with new id as key
    const todo = {...action.payload, id: uuid.v4()}
    const newTodoEntry = {[todo.id]: todo}
    return {...todos, newTodoEntry}
}

function updateTodo(todos = {}, action) {
    const todo = action.payload
    const newTodoEntry = {[todo.id]: todo}
    return {...todos, newTodoEntry}
}

function todos(todos = {}, action) {
    if (action.channel === 'todo') {
        switch(action.actionType) {
            case 'create' :
              return createTodo(todos, action)
            case 'update' :
              return updateTodo(todos, action)
        }
    }
    return todos
}

export default {createTodo, updateTodo, todos}
// AppState.js
import AppDispatcher from './AppDispatcher'
import TodoStore from './TodoStore'
// assuming UserStore is created similar to TodoStore
import UserStore from './UserStore'

function scanner(state, action) {

    return {
        todos: TodoStore.todos(state.todos, action),
        users: UserStore.users(state.users, action)
    }
}

const appStateStream = AppDispatcher.scan(scanner, {})
const todosStream = appState.map(appState => appState.todos)
const usersStream = appState.map(appState => appState.users)

export default {
    appStateStream,
    todosStream,
    usersStream
}

Notes:

  1. We’re using default parameters.
  2. We’re using the spread operator.
  3. We’re using computed property names.
  4. We use the spread operator to create copies of structures (instead of mutating the original structure directly) i.e., each store function is a pure function.
  5. Recall AppDispatcher is just an FRP stream accepting Action inputs.

Solution 2: Redux with Action Streams

One thing we lost with Solution 1 is the ability to know when an action occurred. Let’s remedy that:

// AppState.js
import AppDispatcher from './AppDispatcher'
import TodoStore from './TodoStore'
// Again, assuming this is created like TodoStore
import UserStore from './UserStore'
import Kefir from 'kefir'

const todoActionStreams = {
    createTodo: AppDispatcher
        .filter(x => x.channel === 'todo')
        .filter(x => x.actionType = 'create')
        //note that we're returning a higher-order function
        .map(x => todos => TodoStore.createTodo(todos, x)),
    updateTodo: AppDispatcher
        .filter(x => x.channel === 'todo')
        .filter(x => x.actionType = 'update')
        .map(x => todos => TodoStore.updateTodo(todos, x))
}

const todos = Kefir
  .merge([
      todoActionStreams.createTodo,
      todoActionStreams.updateTodo
  ])
  //ensures this fires right away, so todos starts out as {}
  .toProperty(() => ({}))
  .scan((todos, modificationFunction) => modificationFunction(todos), {})

const userActionStreams = // defined like above
const users = // defined like above

const appState = Kefir
  .combine([todos, users], (todos, users) => ({todos, users}))

export default {
    todos,
    todoActionStreams,
    users,
    userActionStreams,
    appState
}

And there you go—we have action streams again.
If you’re worried about boilerplate, it’s fairly straightforward to create functions that createActionStreamsFromStore and createStoreStreamFromActions.

Shootout: Redux vs Action streams

One problem with our Redux solution is that there’s no way for stores to talk to each other.
On the surface, Action Streams solve this problem. For example, to know when
todos have been created, you can just add an observer to the createTodo action stream:

// Somefile.js
todoActionStreams.createTodo.onValue(() => console.log('todo created'))

There are two problems:

  1. We don’t know which todo was created. Adding back this functionality results
    in code that can’t be easily auto generated.
  2. Having these observers scattered throughout the code base results in unmaintainable code.
    The loose coupling makes it hard to reason about code.

More generally, we don’t yet have a way for stores to talk to each other.
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