Extreme Decoupling

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

See the demo and source on github

UIs can be seen as having three distinct layers:

view : components, html, styles, layout, etc.

: components, html, styles, layout, etc. data : state, text, labels, properties, etc.

: state, text, labels, properties, etc. integration: selectors, derived data, event handlers, etc.

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).

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. Eventually logic leaks between concerns, the lines muddy, the cycle repeats, and a refactor is required. Managing coupling is one of the most difficult and important responsibilities of a lead engineer or architect.

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.

Rather than loosely managing concerns in abstract '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:

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 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 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" }

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.

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:

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:

react components remain minimal

react component logic is limited to display concerns

react components are generalized and reusable for multiple purposes

react components can be developed with smaller display-specific skill sets

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:

prefer arrays over objects

prefer nested, hierarchical, denormalized data

name things relative to UI elements not domain knowledge: 'onClickButton' instead of 'onClickAddTodo'

name things with as little specificity as possible, but no less

pass in all strings, labels, and text as props

prefer props over state

only use local state for: forms, performance, temporary data, or special circumstances

de-structure objects into individual props as you descend down the hierarchy tree

group inner component interfaces into higher-level objects as you climb up the hierarchy tree

child components should have no knowledge of parent components or higher-order structures

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:

state should be flat, shallow, normalized, and flexible

prefer objects over arrays

avoid nesting objects, use ids to denote relationships

any value that can be derived or calculated should not be stored in state

name things relative to domain, not visual elements: 'addTodo' instead of 'onClickButton'

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.

selectors should be deterministic and free of side-effects

selectors should only operate on state and constants

selectors can be used by other selectors

selectors should memoize all computation

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:

testing methodology (integration tests vs unit tests)

project dependencies (display libs vs data libs)

required skill-sets (html/css vs js)

build and deploy processes

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.

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:

Each concern can be worked on, versioned and deployed completely independently of the others

Different teams with different skills can easily work on different concerns

Each concern has a smaller set of dependencies to manage

Each concern can have its own custom testing strategy

Concerns can be shared normally through NPM

The line between concerns further widens reaching an extreme degree

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.



baz@thinkloop.com github.com/thinkloop

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