The
Tao of
Redux

...or why getting in the React/Redux

mindset may mean not using either

Cory Brown

@uniqname

  • All opinions are my own and not necessarily held by my employer, Facebook or virtually anyone else on the planet
  • Every statement in this presentation is an opinion.
  • Due to the rapidly changing front-end landscape, I refuse to be held to statements made more the two hours ago.
  • These are lessons I, personally, have learned during the course of my nearly 10yr career using a variety of tools, libraries and frameworks and in going all in on React/Redux the last couple of them.
  • Your milage may vary.
  • My milage has varied!

Obligatory Disclaimer

<brief-history>

Killing
Sacred
Cows

Life has nuance,

dogma does not.

React (and the ecosystem the grew up around it) shook up the front-end development world by unceremoniously killing several sacred front-end cows.

  • Never mix presentation, markup & logic.
  • Separation of concerns === separation of technologies.
  • Never, EVER use `on[Event]` attributes.
  • Logicless templates are the bomb.com and totes reusable.
  • MVC is the best and only way to make "enterprise" level applications.
  • 2-way data binding. (mic drop)

Solving for problems

What we got for trading in these long held sacraments was an paradigm that was dramatically simpler to reason about and work in.

  • It's just components.
  • Components manage their own state and don't care about anyone else.

Where's

The
Beef?

Trouble in paradise.

React uses an architectural paradigm called flux that featured unidirectional data flow (good) where each component connects to it's own store to get data (less good).

Enter the hero

Redux comes along and using patterns, paradigms and architectures borrowed from functional languages, tweaks the standard flux model to "reduce" everything to a single, immutable store. Sanity is restored, complexity is diminished and peace reigns supreme again.

</brief-history>

Rules?
There Are
No Rules!

Rules for Redux

  • If it should persist on a browser refresh, it belongs in your store
  • If it should persist to a database, it belongs in your store
  • If it is messaging across different parts of the app, it may go in the store.

What goes in the store?

Rules for Redux

  • If it isn't serializable, it does not belong in your store
  • If it should reset on a browser refresh, it does not belong in your store. (It belongs in component state)
  • If it is derivable from other parts of your store, it does not belong in your store (maybe put it in a selector)

What doesn't go in the store?

Rules for Redux

  • Favor a flatter structure over a deep one.
  • Do not tie store structure to your navigation structure.
  • Every discrete type of data should get it's own reducer
  • Treat collections of data types as a database; keep them in a dictionary ({}) keyed by id
    • This prevents id clashes across different instances of an object type.
  • Every collection should have a corresponding array of just ID's.
    • This is primarily for reconstituting a list of object types from their ids.

Store Structure

Rules for Redux

  • Network requests (and other types of async actions) should take place in neither components, nor redux store.
  • Async actions (as the name implies) should happen in action creators.
  • Typically a network request will correspond to three actions.
    • request -- this is an opportunity to put relevant parts of your app into a "loading" or "pending" state.
    • response -- this represents the successful completion of the request and is the time to update your redux store with the received data
    • error -- something didn't go as planned. This is your opportunity to revert any optimistic state updates you may have made, or to do some other failover logic.

Network requests

Rules for Redux

  • Thunks
  • Sagas
  • ...other

Network requests

99 times out of 100 I've found a simple thunk to be completely adequate for orchestrating network requests

Rules for Redux

  • Organize by feature, not by technology or pattern.
  • A feature may consist of any, all or one of:
    • Stateless/Statefull Component
    • Connected/HOC Component
    • styles
    • reducer(s)
    • actions
    • action creators
    • services
  • Generally, the flatter the better.
  • Features should not have hierarchy
  • It is irrelevant whether a feature is used in only one place, or extensively throughout the app. A feature, is a feature is a feature.

File structure

The

Tao of
Redux

Don't fall into old patterns

Dogmatically avoid dogma

  • Don't trade one set of dogma for another.
  • There is no universally "right" way to use React/Redux.
  • Nor is React/Redux universally the "right" tool for every front-end project.
  • Do what simplifies the mental model of the application.
  • Allow for flexibility where flexibility is needed.
  • Constrain where constraints clarify ambiguity.

Judiciously apply patterns

Avoid falling in line too quickly

  • Instagram/Facebook built react to solve Instagram/Facebooks problems.
  • You aren't Instagram/Facebook. Do you have the problems React solves?
  • Redux solves the problem of managing application state.
  • Does your application have an application state problem?
  • Do you even have an application.
  • Not all front-end projects are front-end applications.

Don't create problems by solving imaginary ones.

Use a tool only when you need the tool

  • There are a lot of great tools out there for working with React, Redux, or just building applications.
  • Most of them you don't need.
  • You are making your job harder by including unnecessary patterns, tools or abstractions.
  • When a problem is imminent, find a tool that solves it, but not until it is imminent.
  • Use the minimum amount of tooling to solve for the maximum amount of problems you are likely to encounter.

How to Redux

onEvent(actionCreator())

reducer(state, action) => newStateSlice

 

compose(reducers)

new state

store.getState()

connect()

store.dispatch(action)

combineReducers

Reducers

The `reducer` is pivotal to what Redux is.

const reducer = (newThingSoFar, currentData) => ({
  ...newThingSoFar,
  [currentData.key]: currentData.value
})

const someNewThing = someArray.reduce(reducer, {})
const reducer = (stateSlice, action) => ({
  ...stateSlice,
  [action.type]: action.value
})

const newState = store.reduce(reducer, appState)
const reducer = (stateSlice, {type, ...action}) => {
  switch (type) {

    case 'RECIEVE_DATA':
      return { ... stateSlice, ...action.data }

    default:
      return stateSlice
}

Think `[].reduce` callback

A Redux reducer takes a slice of the current state, and an action.

It returns a NEW slice of state depending on the action.type

The code branching based on action.type is conventionally done with a `switch` statement

Reducers

It's best to make your action types exported constants.

export const RECIEVE_DATA = 'RECIEVE_DATA'

export default (stateSlice, {type, ...action}) => {
  switch (type) {

    case RECIEVE_DATA:
      return { ... stateSlice, ...action.data }

    default:
      return stateSlice
}

Mostly this prevents typos, and provides a documented api for how to change your state.

Actions

The store is informed of desired changes with `actions`

import { ACTION_TYPE_NAME } from './reducers.js'

export default = ({
  type: ACTION_TYPE_NAME,
  ...payload
})

Actions are plain objects with a `type` property. Everything else is up to you.

Typically you will want to use action creators to keep actions of the same type consistent

import { ACTION_TYPE_NAME } from './reducers.js'

export default = (payload) => ({
  type: ACTION_TYPE_NAME,
  ...payload
})

Actions

Action creators are where Async actions occur, with the help of redux-thunk

import { ACTION_TYPE_NAME, SUCCESS_ACTION, ERROR_ACTION } from './reducers.js'

export default = (payload) => (dispatch) => {
  dispatch({ type: ACTION_TYPE_NAME })
  return fetch('/api/endpoint')
  .then((result) => 
    dispatch({ type: SUCCESS_ACTION, ...result}))
  .catch((error) => 
    dispatch({ type: ERROR_ACTION, ...error }))
}

Selectors

Selectors are functions that know how to get the bit of state a components wants without the connected component needing to know the structure of the state itself

export const selectUserName = (appState) => 
    appState.user.name

They typically live in your reducers file, along side your action types and reducer functions.

const MY_ACTION_TYPES = 'MY_ACTION_TYPES'

...

export default (state, action) => { /* reducer */}

...

const selectSomeState = (appState) => appState.path.to.thing 

MapStateToProps

Connected Components

Higher Order Componets connect to the redux store and provide the proper parts of state as props to the target component

import { connect } from 'react-redux'
import MyComponent from './my-component.js'
import { getSomeBitOfState } from './reducers.js'

export default = connect(
    mapStateToProps,
    mapDispatchToProps,
    customMerge,
)(MyComponent)

`connect` allows for us to map some bit of state to be passed as props to a target component

const mapStateToProps = (appState) => ({
  someProp: appState.someState,
  someOtherProp: appState.someOtherState
})

MapStateToProps

Connected Components

MapDispatchToProps allows us to pass functions that can dispatch actions to the Redux store as props to the target component

const mapDispatchToProps = (dispatch) => ({
  onSubmit: (payload) => dispatch(onSubmitAction(payload))
})

MapDispatchToProps

Connected Components

customMerge allows us be more specific about how a components mapStateToProps, mapDispatchToProps, and own props objects are merged

const customMerge = (
    stateProps,
    dispatchProps,
    ownProps
) => ({
    ...stateProps,
    ...dispatchProps,
    ...ownProps
})

CustomMerge

deck

By Cory Brown

deck

  • 1,081