Extreme Decoupling

React, Redux, Selectors
By Baz on Oct. 4, 2016

See the demo and source on github

UIs can be seen as having three distinct layers:

It is beneficial for these layers to remain separate to maximize maintainability and reusability, while minimizing complexity and iteration time (hence the rise of MV* frameworks).

Coupling is the Devil

The problem is, it is difficult to consistently identify, and adhere to, the right division of responsibilities. Sometimes the lines are blurry, other times it is too easy to overload existing abstractions instead of creating new ones. QA'ing is even harder. There is too much contextual knowledge and subtlety involved for code reviews to catch anything meaningful. Eventually logic leaks between concerns, the lines muddy, the cycle repeats, and a refactor is required. I would argue that managing coupling is the single most difficult and important responsibility of a lead engineer or architect.

Monolithic UIs

The way we structure our apps exacerbates the situation. The current state-of-the-art is to have a monolithic UI project broken into features folders (i.e. todos, comments, etc.) and subfolders of concerns (i.e. actions, components, etc.). The right code should go in the right places, but the app runs with few repercussions either way. Developing a feature involves working tightly between view, state and selectors, treating them like a single mass, with few clear divisions. The process of deciding where to implement abstractions becomes a subtle skill-driven task with little guidance.

Hello Interfaces

Rather than loosely managing concerns in 'layers', we can turn them into first-class programmatic citizens. We can encapsulate the entire view, state and selectors each in an independent stand-alone object that exposes a structured interface. Putting together a final app becomes a process of integrating defined APIs - a process of deliberately and judiciously introducing coupling at specific points of an app. For example, the following code imports an encapsulated view, and renders it with data and logic imported from a selectors object:

// import view
import { render } from 'my-entire-view';

// import selectors
import { selectors } from 'my-selectors';

// get dom element to render ui in
const domElement = document.getElementById('app');

// render full app
render(selectors, domElement);

This code has no dependency on things like react, or redux, or anything at all for that matter. Those are implementation details blackboxed inside their respective components. Each import is an independent self-contained object that exposes a minimal interface.

Here is what the interfaces could look like:

View Interface

The view's primary purpose is to export a render function that renders the full UI:

export {
  render(data, domElement): "function that renders view with given data and dom element",
  constants: "object of constants related to view"
}

State Interface

State stores data and provides actions that can transform it. It exports an interface with getState, actions and subscribe:

export {
  getState(): "function that returns latest state",
  actions: "object of available actions",
  subscribe(callback): "function that invokes a callback every time state changes",
  constants: "object of constants related to state"
}

Selectors Interface

Selectors take state, and turn it into view-friendly data structures. It is where coupling between state and view is isolated. Its primary export is a property by the same name selectors:

export {
  selectors: "object of all selectors",
  actions: "object of available actions",
  state: "getter that returns latest state",
  subscribe(cb): "function that invokes callback every time state changes",
  constants: "object of constants combined from view, state and new constants"
}

Ethos

With these interfaces we can build many combinations of apps, with high code-reuse. For example we can make an entirely new view (i.e. for mobile, or a different locale, or a different audience, etc.) and plug it into the same shared state container. Or we can make a new state container with custom logic that plugs into an existing view. State and view remain fundamentally separate, generalized for many purposes, while selectors do the dirty work of coupling.

Let's See Some Code

There are many ways and technologies to implement these objects. It doesn't really matter which to use, as long as the interfaces are fulfilled. With that said, react and redux are especially well-suited to the task.

A full example todo app using react, redux, and selectors, is available on github:

  1. view: react components
  2. state: redux container
  3. integration: selectors

View Object (React)

The primary goal of the view object is to export a render function that renders the UI using passed in data. Component-based view frameworks like react make this easy. React allows for the hierarchical composition of encapsulated view elements into a single top-level component that can render the entire UI.

Here is an example of a render implementation that uses react to render the top-most visual element, which renders all other child elements:

import React from 'react';
import { render as reactRender } from 'react-dom';

import TodosPage from './todos/todos-page';

export default function (data, domElement) {
  let page = <TodosPage { ...data.todos } siteHeader={ data.siteHeader } />;

  // render to dom using react
  reactRender(page, domElement);
}

The view could export itself and render like this:

import render from './render';
import * as TODO_STATUSES from './todos/constants/statuses';

// export api for other apps to use
export default {
  render,

  constants: {
    TODO_STATUSES
  }
};

A 3rd-party app could implement it like this:

// import view
import { render, constants } from 'todo-react-components';

// render entire ui with empty data
render({}, document.getElementById('app'));

// dump available constants
console.log(constants);

There are no backend services, or state containers or ajax requests or business logic in the view object - only a hierarchy of react components responding to passed in data. It is built selfishly in isolation with the sole goal of modeling itself in the simplest possible way. This helps ensure that:

The data argument of render is a generic object that contains all the data the view may need, in the shape the view defines. It is the responsibility of implementers to provide a correct structure that allows the view to work. Here is an example of data for a simplified todo app:

{
  "selectedPage": "HOME",

  "url":"/",

  "todos": {
    "newForm":{
      "placeholder": "What do you need to do?"
    },

    "list":[
      {
        "description": "Buy tomatoes from grocery store",
        "dateCreated": "2016-09-19T18:44:15.635",
        "isComplete": false,
        "id": "10",
        "buttonLabel": "delete"
      }
    ]
  }
}

Tips on structuring view data:

State Object (Redux)

The state object holds all of an app's data, and offers actions that transform it in well defined ways. Its primary exports are getState(), which returns a snapshot of the latest state, and actions, which is a collection of available transformations. Here is an example of code importing a state object, running some actions, and logging the new state between them:

// import the state project
import { getState, actions } from 'todo-redux-state';

// run load-todos action
actions.todos.loadTodos();
console.log(getState());

// run add-todo action
actions.todos.addTodo('demo test 1');
console.log(getState());

// run remove-todo action
actions.todos.removeTodo('3');
console.log(getState());

Redux is a state container. It is unrelated to views, or react, and can be used for many purposes. It manages the transformation of data based on defined actions. The key insight of redux is that it groups all mutations of a particular part of state in the same place, regardless of what originating actions triggered them. This makes it easy to reason about changes over time. Redux is an excellent choice of technology for the state object. Here is an example export of a redux-powered state object:

 // import redux store
import store from '../src/store';

// import actions
import addTodo from './todos/actions/add-todo';
import loadTodos from './todos/actions/load-todos';
import removeTodo from './todos/actions/remove-todo';

// import constants
import * as TODOS_STATUSES from './todos/constants/statuses';

// export interface
export default {
  getState: store.getState, // borrow from redux

  actions: {
    todos: {
      addTodo,
      loadTodos,
      removeTodo
    }
  },

  subscribe: store.subscribe, // borrow from redux

  constants: {
    TODOS_STATUSES
  }
};

Having a dedicated state object allows state to remain as simple and minimal as possible. Guidelines:

Selectors Object

Now that we have the two pillars of an app in a beautiful, maintainable, decoupled form, how do we combine them into a functioning app? How do we populate a rich, hierarchical, nested view that was built in isolation, using simple, flat, shallow state that was also built in isolation?

Selectors the unsung heros

Selectors are functions that take state, and turn it into data for views. For example, say our state had a collection of todos like this:

{
  "#123": { "description": "buy groceries" },
  "#456": { "description": "book flight" }
}

But the view required an array of todos like this:

[
  { "id": "#123", "description": "buy groceries" },
  { "id": "#456", "description": "book flight" }
]

Our selector would take the relevant parts of state and transform them into the final shape for the view:

/*
* todos selector
*/

export default function (state) {

  // get relevant state
  const { todos } = state;

  // generate view-specific structure
  return Object.keys(todos).map(key => {
    return {
      id: key,
      description: todos[key].description
    };
  });
}

Whenever a selector is called, it runs on the latest snapshot of state, and returns an up-to-date result. Given the same state, a selector will return the same result.

Selectors do not introduce new data or functionality. They simply handle the dirty work of joining views to state. They allow views and state to evolve independently and freely.

Selector Guidelines

Logistics

There are many architectural benefits to developing view, state and selectors as independent, stand-alone objects. There are many logistical benefits too. Consider the contrasting requirements between view and state when it comes to:

The view may depend on react and some 'classnames' utility, while state could depend on redux, thunk, and agent. The view may need a selenium test suite, while state and selectors would focus on unit tests. The view favors a display-oriented mindset with skill in html, css, and design, while state is heavy on data transformation, remote services, data modeling, etc.

Multi Repo Approach

The final step to extreme decoupling, is to physically separate the concerns from each other. Make each concern its own complete, runnable, stand-alone project in its own repo. Each with their own focused dependencies, versioning, release schedules, test suites, file structure:

Todo Example on Github

For a complete implementation of all the concepts discussed, see the example todo app on github:

Baz

Web engineer working with startups for 15+ years.

[email protected]      github.com/thinkloop

Check out State-Driven Routing with React, Redux, Selectors