Skip to content

Instantly share code, notes, and snippets.

@heygrady
Last active March 21, 2019 09:08
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heygrady/04f633594765f69c091a54b91926541e to your computer and use it in GitHub Desktop.
Save heygrady/04f633594765f69c091a54b91926541e to your computer and use it in GitHub Desktop.

Key concepts in a react-redux application

Redux is a collection of tools and techniques for managing a centralized "store". Essentially, your application state (your data) is kept in a central place (the store) and every part of the app that needs to read from that state does so in a controlled manner. At it's core, redux is mapping functional programming styles -- familiar to Lisp programmers -- to a modern JavaScript environment.

Many of the core concepts of react and redux can be found in other application frameworks like Angular and Ember. Classical (class based) frameworks like Ember provide a "kitchen sink" style API -- a prescriptive approach. Redux preaches a functional programming style, where composition and convention are preferred. This leads to a much smaller API but leaves a lot up to the developer.

This lack of a prescriptive API is freeing, but can lead to confusion when constructing your apps. It's up to the individual developer to follow best practices for separating concerns. The good news is that, once you get the hang of the core concepts, redux's functional programming style makes it much easier to build stable, testable apps.

Important terms

  • Components
    • Stateless / Pure / Functional
    • Lifecycle
    • Internal State
  • Containers
    • React-redux
  • Modules
    • Actions
    • Constants
    • Reducers
    • Middleware
      • Thunks
      • Sagas
    • Selectors

Components

A basic component should always be a plain, functional component. In the vast majority of cases, components don't need fancy things like context or internal state. More commonly, your component will need access to lifecycle methods. But, unless your component is special, it should probably just be a function.

Regardless of how fancy your component is, at the end of the day it's just a template. It's important to read this foundational document on the differences between "presentational" and "container" components. Back in the day, these were referred to as "dumb" and "smart" components. Those words should be avoided because, really, let's not get into name calling. But it's a good way to think about it.

Key idea: A component receives props from somewhere else.

Stateless / Pure / Functional components

Don't be confused by the existence of the PureComponent (the replacement for the PureRenderMixin). When you're using react-redux's connect method, you are already getting a shallow compare (see here).

Functional, sateless components are a big win (unless you have a use-case-specific exception).

What is the difference between a PureComponent and a functional component? Mostly it's the enforcement of shallow compare for props (to control when a component should re-render). A functional component doesn't give you access to the shouldComponentUpdate() method, so it's not possible to enforce shallow rendering unless you use a higher-order-component, like pure() from recompose. In practice, the differences are minor. If you're following good practices you should be passing in shallow, immutable props anyway.

It's important to keep in mind that, in a redux app, connected "container" components already implement shallow compare for props. In most cases, a "real" PureComponent won't be a big performance gain.

Below is a simple stateless component.

import React from 'react'
import PropTypes from 'prop-types'

const SomeName = ({ name }) => {
  return (
    <div>
      {name}
    </div>
  )
}
SomeName.propTypes = {
  name: PropTypes.string
}
export default SomeName

Lifecycle components

While most components should be functional components, occasionally you need access to React's lifecycle methods. In those cases you need to create a lifecycle component.

Below you can see that we're implementing our lifecycle component using the PureComponent class.

import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'

const isFunc = func => typeof func === 'function'

class SomeName extends PureComponent {
  componentDidMount () {
    const { onLoad } = this.props
    if (isFunc(onLoad)) {
      onLoad()
    }
  }

  render () {
    const { name } = this.props
    return (
      <div>
        {name}
      </div>
    )
  }
}
SomeName.propTypes = {
  name: PropTypes.string,
  onLoad: PropTypes.func
}
export default SomeName

Internal state components

In a redux application, internal state is an anti-pattern. If you are using this.state in your component you are likely doing something very wrong. However, there are a few cases where internal state is the best way to do something. Inevitably there will be times when storing a component's state in Redux is inappropriate -- like tracking if an element is hovered. In those rare edge cases, it may be appropriate to utilize internal state.

Note: If you're using internal state you're probably doing it wrong. Make sure you have a good reason. If your "good reason" becomes a pattern that you find yourself applying often, either you're doing something extraordinarily special or you're doing it wrong.

import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import cx from 'classnames'

class SomeButton extends PureComponent {
  constructor () {
    super()
    this.hover = this.hover.bind(this)
    this.state = { hovered: false } // <-- initialize state
  }

  onHover (hovered) {
    this.setState({ hovered })
  }

  render () {
    const { label, onToggle, toggled } = this.props
    const { hovered } = this.state // <-- internal state comes in here

    return (
      <button
        className={
          cx('SomeButton', {
            'SomeButton--toggled': toggled,
            'SomeButton--hovered': hovered
          })
        }
        onClick={onToggle}
        onMouseEnter={() => this.onHover(true)}
        onMouseLeave={() => this.onHover(false)}>
        {label}
      </button>
    )
  }
}
SomeButton.propTypes = {
  label: PropTypes.string.isRequired,
  onToggle: PropTypes.func,
  toggled: PropTypes.bool
}

export default SomeButton

Containers

In a redux application, your component's state belongs in the redux store. If a component is always a stateless functional component, how does it get it's state? React-redux provides a connect() method that uses context to hook your components to the redux store.

Many developers try to optimize the number of files in their application and put their container in the same file as their component -- this is a huge mistake. In practice, a container is very different from a component and should be maintained entirely separately. This is typically known as a "separation of concerns". In the same way that CSS is different from HTML, a component is different from a container.

A key part of redux's elegance is this separation of "state" from "presentation". Embracing this separation can help your app become more testable and easier to maintain.

Key idea: A container selects props from the state and dispatches actions to reducers.

Using react-redux

React-redux introduces the concept of "connecting" a component to the store using a higher-order-component, called a container. Best practice dictates keeping this "container component" separate from the presentational component it wraps. In functional programming terms this is known as "composition".

Below you can see a typical container component.

Note: We are importing a presentational component and wrapping it using the connect() function provided by react-redux. You can also see that the functions for interacting with redux are imported from a "module".

import { connect } from 'react-redux'
import SomeButton from '../components/SomeButton'
import { selectToggled } from '../modules/someName/selectors'
import { toggle } from '../modules/someName/actions'

const mapStateToProps = (state, ownProps) => {
  const toggled = selectToggled(state) // <-- read a value from the state
  return {
    toggled // <-- becomes a prop on the SomeButton component
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    onToggle () {
      dispatch(toggle()) // <-- sends the action to the reducer (which then alters the state)
    }
  }
}

const SomeButtonContainer = connect(mapStateToProps, mapDispatchToProps)(SomeButton)
export default SomeButtonContainer

Modules

  • Constants — used as an action type
  • Actions — a simple object with a type and a payload
  • Reducers — a function that alters the state based on an action
  • Selectors — a function that reads from the state

It's important to read the fondational document on "ducks" as well as this document about structuring ducks. These days, people call them "modules" but the effect is the same. A module is a collection of related constants, actions and reducers. For completeness / sanity, it's customary to add in selectors. I would also add sagas to the mix, but they're covered in more detail below.

Constants

As annoying as it seems, constants need to be in their own file. Some early flux dogma stipulated keeping each constant in its own file. Perhaps a more sane approach is keeping related constants in the same file. In practice, these constants will all relate to a single module.

Below you can see the constants file for our example module.

Notice how the constant string values are namespaced. This has numerous positive sideeffects.

  • makes naming collisions much less common
  • ensures your constants won't overlap with a 3rd party package
  • ensures that your developers are using constants as variables instead of strings
  • encourages creating constant names that makes sense in the scope of your module
export const TOGGLE_BUTTON = '@@my-app/someName/TOGGLE_BUTTON'
export const ACTIVATE_BUTTON = '@@my-app/someName/ACTIVATE_BUTTON'
export const DISABLE_BUTTON = '@@my-app/someName/DISABLE_BUTTON'

Actions

Actions are another annoying part of redux. Like constants, a little discipline goes a long way. It's good to keep related constants in the same file.

Actions (really action creators) are functions that return an object. Redux requires that every action object have a type. There is a flux standard actions spec that adds a payload and a meta to action objects. In practice, using standardized actions greatly simplifies your workflow. Thankfully there's a library, redux-actions, that makes it really easy to create a standard action.

Below you can see our actions file exports three action creators.

import { createAction } from 'redux-actions'
import {
  TOGGLE_BUTTON,
  ACTIVATE_BUTTON,
  DISABLE_BUTTON
} from '../constants'

export const toggle = createAction(TOGGLE_BUTTON)
export const activate = createAction(ACTIVATE_BUTTON)
export const disable = createAction(DISABLE_BUTTON)

Example action

import { toggle } from '../modules/someName/actions'

toggle('whatever') // --> { type: '@@my-app/someName/TOGGLE_BUTTON', payload: 'whatever' }

Reducers

Reducers are probably the most obtuse concept in redux. A reducer is a function that alters the state based on an action. The concept is borrowed from map / reduce and is very similar to the Array.reduce() method in Javascript. New developers consistently struggle to understand "how" a reducer relates to the global state. In the most basic terms, a reducer receives a state and returns a new state. Narrowing the focus of a reducer to its inputs and outputs brings their simplistic beauty into focus.

Part of the confusion with reducers is the switch-case boilerplate that is encouraged in the redux manual. Thankfully the redux-actions library offers a handleActions function that abstracts away much of the boilerplate and makes it dead-simple to create a reducer function.

Unlike constants and actions (and more like components and containers), each reducer should be in its own file. This is important for maintainability and testability -- a reducer should be about one small part of the state. Mixing multiple reducers into the same file can cause confusion.

Below you can see a reducer that simply manages the boolean state of our button based on which action was dispatched.

import { handleActions } from 'redux-actions'
import {
  TOGGLE_BUTTON,
  ACTIVATE_BUTTON,
  DISABLE_BUTTON
} from '../constants'

export default handleActions({
  [TOGGLE_BUTTON]: (state, action) => !state,
  [ACTIVATE_BUTTON]: (state, action) => true,
  [DISABLE_BUTTON]: (state, action) => false
}, false) // <-- initializes to false

Example reducer

The example below completely ignores how the reducer is attached to the state. In practice, your reducers are "hooked up" to the state as part of the store initialization. Ideally, each reducer function should manage only a single "key" in the state object. For instance, the reducer below might receive the toggled value from the redux state.

import buttonReducer from '../modules/someName/reducers/buttonReducer'
import { toggle } from '../modules/someName/actions'

const initialState = buttonReducer() // --> initializes to false

const imaginaryReduxState = {
  someName: {
    toggled: initialState
  }
}

const state = imaginaryReduxState.someName.toggled
const action = toggle()
const newState = buttonReducer(state, action)

newState // --> true

Middleware

  • Thunks -- a special type of action; a function that can dispatch other actions. Useful for asynchronous calls.
  • Sagas -- a generator function that can manage asynchronous calls; not an action itself. Can dispatch actions.

Perhaps the most confusing part of a redux app is the middleware. If everything is synchronous, how do you make a fetch call? Middleware! Of Course!

Thunks

Many applications have settled on using thunks because they are simple. In a larger application, the ability to embed an asynchronous function call into the action flow can get a little bit hairy. Thunks break the standard unidirectional, synchronous flow of redux. Overusing thunks can lead to apps with numerous unpredictable side effects.

Under the hood, the thunk middleware inspects every dispatched action and if it sees a function instead of an action object it will call that function, passing dispatch and getState.

Although you dispatch a thunk, a thunk isn't an action -- it's a thunk. There is a huge pressure to lump your thunk middleware functions in with your actions but this temptation should be avoided. Like reducers, components and containers, thunks are functions that perform a specific action. As such, they should be in their own standalone file.

Below you can see a simple thunk file that demonstrates how a stand-alone thunk might look.

import { beginFetchingThings, endFetchingThings, errorFetchingThings, receiveThings } from '../modules/someName/actions'

const fetchThings = payload => (dispatch, getState) => {
  const subreddit = payload
  const request = new Request(`https://www.reddit.com/r/${subreddit}.json`)

  dispatch(beginFetchingThings(payload))

  fetch(request)
    .then(result => result.json())
    .then(data => dispatch(receiveThings(data)))
    .catch(err => dispatch(errorFetchingThings({ originalPayload: payload, err })))
    .finally(() => dispatch(endFetchingThings(payload)))
}

export default fetchThings

Sagas

Some advanced development shops have adopted redux-saga for managing their asynchronous calls. At first blush, sagas are extraordinarily complex. However, once you get past the initial configuration they end up working mostly like reducers -- you dispatch an action to trigger something to happen. While reducers are designed to make synchronous mutation of the application state, sagas are designed to manage an asynchronous workflow.

The core of what makes sagas work is their reliance on generator functions. Under the hood, redux-saga is doing a little bit of magic to make working with generator functions ridiculously easy. Most developers are unfamiliar with generators but writing a basic saga is very easy.

Below we can see a saga that does roughly what the thunk above accomplishes.

import { put, call } from 'redux-saga/effects'
import { beginFetchingThings, endFetchingThings, errorFetchingThings, receiveThings } from '../modules/someName/actions'

export default function * (action) {
  const { payload } = action
  const subreddit = payload
  const request = new Request(`https://www.reddit.com/r/${subreddit}.json`)

  yield put(beginFetchingThings(payload))

  try {
    const result = yield call(fetch, request)
    const data = yield call(result.json)
    yield put(receiveThings(data))
  } catch (err) {
    yield put(errorFetchingThings({ originalPayload: payload, err }))
  }

  yield put(endFetchingThings(payload))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment