Updated May 13 2018
Psssst! The 2023 WEB DEVELOPMENT BOOTCAMP is starting on FEBRUARY 01, 2023! SIGNUPS ARE NOW OPEN to this 10-weeks cohort course. Learn the fundamentals, HTML, CSS, JS, Tailwind, React, Next.js and much more! ✨
Redux is a state manager that’s usually used along with React, but it’s not tied to that library - it can be used with other technologies as well, but we’ll stick to React for the sake of the explanation.
React has its own way to manage state, as you can read on the React Beginner’s Guide, where I introduce how you can manage State in React.
Moving the state up in the tree works in simple cases, but in a complex app you might find you are moving almost all the state up, and then down using props.
React in version 16.3.0 introduced the Context API, which makes Redux redundant for the use case of accessing the state from different parts of your app, so consider using the Context API instead of Redux, unless you need a specific feature that Redux provides.
Redux is a way to manage an application state, and move it to an external global store.
There are a few concepts to grasp, but once you do, Redux is a very simple approach to the problem.
Redux is very popular with React applications, but it’s in no way unique to React: there are bindings for nearly any popular framework. That said, I’ll make some examples using React as it is its primary use case.
Redux is ideal for medium to big apps, and you should only use it when you have trouble managing the state with the default state management of React, or the other library you use.
Simple apps should not need it at all (and there’s nothing wrong with simple apps).
In Redux, the whole state of the application is represented by one JavaScript object, called State or State Tree.
We call it Immutable State Tree because it is read only: it can’t be changed directly.
It can only be changed by dispatching an Action.
An Action is a JavaScript object that describes a change in a minimal way (with just the information needed):
{
type: 'CLICKED_SIDEBAR'
}
// e.g. with more data
{
type: 'SELECTED_USER',
userId: 232
}
The only requirement of an action object is having a type
property, whose value is usually a string.
In a simple app an action type can be defined as a string.
When the app grows is best to use constants:
const ADD_ITEM = 'ADD_ITEM'
const action = { type: ADD_ITEM, title: 'Third item' }
and to separate actions in their own files, and import them
import { ADD_ITEM, REMOVE_ITEM } from './actions'
Actions Creators are functions that create actions.
function addItem(t) {
return {
type: ADD_ITEM,
title: t
}
}
You usually run action creators in combination with triggering the dispatcher:
dispatch(addItem('Milk'))
or by defining an action dispatcher function:
const dispatchAddItem = i => dispatch(addItem(i))
dispatchAddItem('Milk')
When an action is fired, something must happen, the state of the application must change.
This is the job of reducers.
A reducer is a pure function that calculates the next State Tree based on the previous State Tree, and the action dispatched.
;(currentState, action) => newState
A pure function takes an input and returns an output without changing the input or anything else. Thus, a reducer returns a completely new state that substitutes the previous one.
A reducer should be a pure function, so it should:
Object.assign({}, ...)
Date.now()
or Math.random()
)There is no reinforcement, but you should stick to the rules.
Since the state of a complex app could be really wide, there is not a single reducer, but many reducers for any kind of action.
At its core, Redux can be simplified with this simple model:
{
list: [
{ title: "First item" },
{ title: "Second item" },
],
title: 'Groceries list'
}
{ type: 'ADD_ITEM', title: 'Third item' }
{ type: 'REMOVE_ITEM', index: 1 }
{ type: 'CHANGE_LIST_TITLE', title: 'Road trip list' }
const title = (state = '', action) => {
if (action.type === 'CHANGE_LIST_TITLE') {
return action.title
} else {
return state
}
}
const list = (state = [], action) => {
switch (action.type) {
case 'ADD_ITEM':
return state.concat([{ title: action.title }])
case 'REMOVE_ITEM':
return state.filter(item =>
action.index !== item.index)
default:
return state
}
}
const listManager = (state = {}, action) => {
return {
title: title(state.title, action),
list: list(state.list, action)
}
}
The Store is an object that:
getState()
dispatch()
subscribe()
A store is unique in the app.
Here is how a store for the listManager app is created:
import { createStore } from 'redux'
import listManager from './reducers'
let store = createStore(listManager)
Sure, just pass a starting state:
let store = createStore(listManager, preexistingState)
store.getState()
store.dispatch(addItem('Something'))
const unsubscribe = store.subscribe(() =>
const newState = store.getState()
)
unsubscribe()
Data flow in Redux is always unidirectional.
You call dispatch()
on the Store, passing an Action.
The Store takes care of passing the Action to the Reducer, generating the next State.
The Store updates the State and alerts all the Listeners.