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.
- Components
- Stateless / Pure / Functional
- Lifecycle
- Internal State
- Containers
- React-redux
- Modules
- Actions
- Constants
- Reducers
- Middleware
- Thunks
- Sagas
- Selectors
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.
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
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
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
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.
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
- 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.
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 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)
import { toggle } from '../modules/someName/actions'
toggle('whatever') // --> { type: '@@my-app/someName/TOGGLE_BUTTON', payload: 'whatever' }
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
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
- 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!
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
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))
}