A Year of development with Redux. Part III

reactFebruary 28, 2017Dotby Alex Fedoseev

Update May 20, 2020: For new code, prefer plain React for state management, via the React context and React Hooks. Is Redux deprecated? No, it isn't. But you probably don't need it, and your code can be simpler and more maintable without out.


In the last post of this series, I’ll demonstrate writing UI code as a set of interactions and share how this facilitates integrating Redux and Flow.

Interactions

When I look at UI code, I want two things to be as obvious as possible:

  1. What interactions are possible here (e.g., A, B& C).
  2. If interaction A has happened, what changes did it make to the application state?

The clarity of these points is the key to the long and happy development of any UI application.

When my interactions were split between the actions.js and reducer.js modules, in order to read or write code, I was having to constantly switch back and forth between the two. Things were even worse if multiple reducers were involved. I realized that I should reorganize the code around the interactions, because either implementing a new or working on an existing one, I’m always working in the context of interaction, not action creators or reducers.

Based on this, I reorganized my folders into UI units, like this one:

|- components/                 # representation

  |-- index.jsx

  |-- index.css

|- interactions/               # modules w/ interactions

  |-- modalToggle.js

  |-- mapZoomLevelUpdate.js

  |-- formStateUpdate.js

  |-- serverStateUpdate.js

|- selectors/                  # data selectors

  |-- ...

|- state.js                    # state definition

|- actions.js                  # list of action creators

|- reducer.js                  # list of action handlers

|- index.js                    # container w/ `connect`

The main idea here is to represent interactions as a modules.

Easy case

The simplest possible case is when a synchronous action is dispatched and only one reducer should respond. For example, the interaction module below defines a behavior of modal dialog:

/* interactions/modalToggle.js */
const MODAL_SHOW = 'MODAL_SHOW';
const MODAL_HIDE = 'MODAL_HIDE';
// --- Show modal
// Action creator
export const showModal = () => ({ type: MODAL_SHOW });
// Action handler
export const onModalShow = {
[MODAL_SHOW]: state => state.set('isVisible', true),
};
// --- Hide modal
// Action creator
export const hideModal = () => ({ type: MODAL_HIDE });
// Action handler
export const onModalHide = {
[MODAL_HIDE]: state => state.set('isVisible', false),
};
view raw modalToggle.js hosted with ❤ by GitHub

The reducer module no longer contains any logic, it’s just an index of interactions:

/* reducer.js */
import state from './state';
import { onModalShow, onModalHide } from './interactions/modalToggle';
// ...
export default createReducer(state, {
...onModalShow,
...onModalHide,
...onMapZoomLevelUpdate,
...onFormStateUpdate,
...onServerStateUpdate,
});
view raw reducer.js hosted with ❤ by GitHub

Notice the createReducer helper from Redux’s recipes. It makes it possible to have an exact mapping of a dispatched action from an action creator to the action handler in the reducer. It’ll be required for accurate flow typings.

Advanced case

Let’s say you requested to PATCH an entity and the server responded with 200 OK. At this point to respond on the single dispatch, you must apply 2 changes to the app state:

  • reset UI unit store (turn off spinner, reset form state, etc.)
  • update the entity in the data store

UPDATE: Use redux-tree: https://github.com/shakacode/redux-tree

// Action creator: dispatched from the thunk or whatever
const successAction = (entityId, data) => ({
type: UPDATE_SUCCEEDED,
entityId,
data,
});
// Changes in the app state:
// Action handler -> ui/unitStore: resetting UI unit state
const onSuccess = {
[UPDATE_SUCCEEDED]: () => initialState,
};
// Action handler -> data/entitiesStore: updating entity in the data store
const updateEntityOnEdit = {
[UPDATE_SUCCEEDED]:
(state, { entityId, data }) =>
state.mergeIn(['entities', entityId], data),
};
// Failure handlers, thunks, etc...
// --- Exports
// Imported to the UI unit reducer
export const onServerStateUpdate = {
...onRequest, // showing spinner
...onSuccess, // resetting state
...onFailure, // handling errors
};
// Imported to the data store reducer
export { updateEntityOnEdit };

The interaction approach encapsulates all these details. Reducers are simple aggregations of action handlers that they import from various interactions:

/* ui/unit/reducer.js */
import { onServerStateUpdate } from './interactions/serverStateUpdate';
export default createReducer(state, {
...onServerStateUpdate,
});
/* data/entities/reducer.js */
import { updateEntityOnEdit } from '../ui/unit/interactions/serverStateUpdate';
export default createReducer(state, {
...updateEntityOnEdit,
});

The main wins here:

  • Changing things is easy

    All the changes in the app caused by the interaction are gathered in one place that’s easy to find, easy to reason about, and easy to change, move or remove. If a modal must be converted to inline element or a Google map must be removed: in each case, you’re dealing with files and folders dedicated to a given interaction instead of chunks of code scattered around disparate action and reducer modules.

  • Better focus

    When you’re working on google map interactions, you’re focused only on code related to the google map interactions. There aren’t any distractions from unrelated code.


Check out examples on GitHub with live demos:

https://github.com/shakacode/redux-interactions

Flow

One additional benefit here is the ability to accurately type Redux parts with Flow. Thanks to Atom, I can view Flow errors in the real-time in my editor. And thanks to Nuclide for superior Flow integration.

My goal in combining Redux & Flow was to prevent the following cases:

  • dispatching an illegal action to reducer [ example ]
  • setting an illegal property on the state object [ example ]
  • reading an illegal property from the state [ example ]

Here is the example app I’ll refer to during the rest of this post:

redux-interactions/flow

This is a dummy app, where you pick the blog post and edit its title. It uses thunks to handle asynchronicity and immutable Records as state containers.

1. Building State type

The whole State type consists of the many small parts:

Each part is defined in its context, thus all details are encapsulated. Each store is defined as an Immutable Record. In the end, all of the store types are combined in global State type.

state:

  entities:

    - postsStore

    - commentsStore

    - ...

  ui:

    postsSection:

      - dataFetchStore

      - postEditStore

      -

2. Typing Redux parts

When State is defined, we can type other Redux parts.

Instead of defining an Action via a union type as suggested in official example:

type Action = { type: ACTION_TYPE_1, payload: string } | { type: ACTION_TYPE_2 }

It’s defined as just $Subtype of string:

type Action = { type: $Subtype<string> }

Yes, it’s less accurate here, but it will be very accurate in the interactions, as you will see below.

3. Typing interactions and selectors

At this point we can implement typings for all redux parts:

Here’s an example of the Flow warnings in action, when I refactor state property name from postId to id:

4. Typing action creators in representational components

Sadly, Flow can’t infer types of action creators defined in interaction modules. It is also impossible to manually share an action creator type because function signatures are different once they’ve been bound to dispatch by Redux. So, we have to re-type action creators manually in our components.

There are some more limitations; see the README for details.


Thanks for reading this, more great stuff coming soon. Stay tuned!

A Year of development with Redux. Part I

A Year of development with Redux. Part II

A Year of development with Redux. Part III

Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right