TL;DR Redux
Redux is a tiny Javascript library that manages the state of your application in a more consistent and practical way.
Think state as a repository in the memory which stores data from a database, an api, the local cache, a UI element in the screen like a form field and more.
The creator of Redux, Dan Abramov, started working on this project while he was preparing his speech for React Europe.
He wanted to create a predictable state container that supports logging, hot reloading, time traveling and universal apps.
Redux is a simpler implementation of the Flux pattern, an architecture that Facebook uses for building their web and mobile apps.
It implements core concepts of functional programming, inspired by Elm.
It works great with modern libraries and frameworks like React, Vue, Angular and many more.
How It Works
We store everything in a plain JavaScript object:
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}]
}
Actions
To perform a change we have to dispatch an Action. These are plain JavaScript objects as well:
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
Action Creators
Usually we create functions that return these objects. We call them Action Creators:
function addTodo(text = '') { return { type: 'ADD_TODO', text }}function toggleTodo(index = 0) { return { type: 'TOGGLE_TODO', index }}
Reducers
To modify the state we use plain functions. They return the new state according to the dispatched action. We call these functions Reducers:
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }])
case 'TOGGLE_TODO':
return state.map(
(todo, index) =>
action.index === index
? { text: todo.text, completed: !todo.completed }
: todo
)
default:
return state
}
}
As your app grows, the complexity of the state object increases. You can organize it with composable reducers and conbine them to a bigger one:
function todoApp(state = {}, action) {
return {
todos: todos(state.todos, action),
files: files(state.files, action),
folders: folders(state.folders, action),
notifications: notifications(state.notifications, action),
filter: filter(state.filter, action),
searchField: searchField(state.searchField, action),
...
}
}
Store
To use Redux you have to create a store. This object manages everything for you. We initialize it with our root reducer:
import { createStore } from 'redux'
import todoApp from './reducers'// create the store
let store = createStore(todoApp)// read the state object
store.getState()// dispatch actions that eventually modify the state
store.dispatch(addTodo('Learn about actions'))
store.dispatch(toggleTodo(0))
Finally, the store needs to be subscribed to a listener. Every time we mutate the state it will notify your app:
// log state to the console every time it gets updated
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
)// stop listening to state updates
unsubscribe()
This is the complete lifecycle. We follow the same flow for every single change. We describe the behaviour of the app without touching the UI.
The Famous Three Principles
When you think about Redux you have to think the following:
Single source of truth: The state of your whole application is stored in an object tree within a single store.
There are no multiple stores like other flux libraries. This simplifies testing, debugging. Things like time traveling, universal apps become a reality.
State is read-only: The only way to change the state is to emit an action, an object describing what happened.
No one writes directly to the state. Never! We always use actions. They know how to mutate the state better than us. And we trust them. Always!
Changes are made with pure functions: Never mutate the state directly, always return a new object.
A pure function is a function where the return value is only determined by its input values, without observable side effects.
Every reducer should return a new object and not to mutate the existing one. This is a simpler way of having immutable data in your app.
How To Use Redux With React
Redux works great with libraries like React. Every time you have a state mutation, the root component gets the update and re-renders the UI. Pretty simple right?
Install 👩💻
There is a package called react-redux
that handles the binding for you:
npm install --save react-redux
Provide 🤗
First of all you have to use a Provider:
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import store from './pathToStore'
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
It’s a Higher Order Function that uses Context to make the store available to every component in the tree. It just makes your store available everywhere.
Connect 👪
The first step is to decide which component you want to bind with Redux. In React we have two kind of components:
Presentational Components describe how things look. They don’t know redux. They use props to read data and to invoke callbacks to update them.
Container Components describe how things work. They just pass the state down using props. They subscribe to Redux.
Picking where to put the containers in your component tree is similar to the topic which components should have state.
To bind a React component with Redux use the HOF connect()
:
import { connect } from 'react-redux'
import TodoList from './TodoList'const TodoListContainer = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default TodoListContainer
It creates a parent component that does the binding and passes everything as props to the child component.
It also does performance optimizations so you don’t have to use shouldComponentUpdate()
in your React components at all.
As you can see it accepts two callbacks. Each of them returns an object. Use the first one to map Redux state to props:
const mapStateToProps = state => {
return {
todos: state.todos
}
}
The second one maps dispatch functions to props as callback functions:
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
In our example the component <TodoList>
will accept the following props:
TodoList.propTypes = {
onTodoClick: PropTypes.func.isRequired,
todos: PropTypes.object.isRequired
}
Enjoy 🍾
You have succesfully connected your store with your view. Now remember:
- Every time something happens you call an action
- The reducers decide which mutations they have to apply to their state
- Your containers get the updated state and they pass the data down to the presentational components
Async Redux
Redux is synchronous by default. There are multiple ways to add async behaviour.
Async Actions
Every async behaviour usually has 3 different kinds of actions:
FETCH_POSTS_REQUEST
: The request has began. You may store in your state a flagisFetching
and if it’s true to show a loading indicator.FETCH_POSTS_SUCCESS
: The request has finished successfully. Change the flagisFetching
to false, remove the loader and show the posts.FETCH_POSTS_FAILURE
: The request has failed. Change the flagisFetching
to false, store and display the error in the screen.
Async Action Creators
The simplest implementation is to use thunks. You must install and integrate a middleware called redux-thunk
. Here’s how it works:
export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
// This is a thunk action creator!
export function fetchPosts(subreddit) {
return function (dispatch) { // dispatch the request
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
error => console.log('An error occured.', error)
)
.then(json =>
dispatch(receivePosts(subreddit, json))
)
}
}
We will dispatch fetchPosts()
as before. This will immediately dispatch requestPosts()
and then receivePosts()
when it will receive the data.
Middleware
Middleware is some code you can put between the framework receiving a request, and the framework generating a response.
In Redux they provide a third-party extension point between dispatching an action and the moment it reaches the reducer.
People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more.
Here’s how to apply middleware in action:
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { selectSubreddit, fetchPosts } from './actions'
import rootReducer from './reducers'
const loggerMiddleware = createLogger()
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware, // lets us dispatch() functions
loggerMiddleware // neat middleware that logs actions
)
)
In this example we added a logger that logs each action in the console. We also added redux-thunk
, as we described above.
DevTools
Redux has an outstanding set of developer tools, which will improve your debugging experience. Extensions are available for Chrome and Firefox.
To enable this functionality you should install the package redux-devtools-extension
. It’s just another middleware you have to apply to your store.
Just inspect the page and open the Redux tab. You can replay every action, skip actions, analyze state mutations, or even export automated unit tests!
Testing
Because most of the Redux code you write are functions, and many of them are pure, they are easy to test without mocking.
Here’s how to test an action creator using Jest:
import * as actions from '../../actions/TodoActions'
import * as types from '../../constants/ActionTypes'
describe('actions', () => {
it('should create an action to add a todo', () => {
const text = 'Finish docs'
const expectedAction = {
type: types.ADD_TODO,
text
}
expect(actions.addTodo(text)).toEqual(expectedAction)
})
})
When it comes to a reducer, you have to test it’s behavior in every single action with two objects, one for before and one after:
describe('todos reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual([
{
text: 'Use Redux',
completed: false,
id: 0
}
])
})
it('should handle ADD_TODO', () => {
expect(
reducer([], {
type: types.ADD_TODO,
text: 'Run the tests'
})
).toEqual([
{
text: 'Run the tests',
completed: false,
id: 0
}
])
expect(
reducer(
[
{
text: 'Use Redux',
completed: false,
id: 0
}
],
{
type: types.ADD_TODO,
text: 'Run the tests'
}
)
).toEqual([
{
text: 'Run the tests',
completed: false,
id: 1
},
{
text: 'Use Redux',
completed: false,
id: 0
}
])
})
})
Tips & Tricks
- Instead of
Object.assign()
you can use the spread operator to simplify your reducers:
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return { ...state, visibilityFilter: action.filter }
default:
return state
}
}
- Make your apps load faster with universal rendering.
- Use
reselect
to compute derived data. - Relational data? Just normalize the shape of your state to make your life easier.
Resources
- The official Redux documentation contains many useful articles and practices for Redux and React in general: http://redux.js.org
- An amazing video series for starters: https://egghead.io/courses/getting-started-with-redux
- The same great video series for advanced Redux concepts: https://egghead.io/courses/building-react-applications-with-idiomatic-redux
The goal of this article is to give you a quicker overview of the library. If you decide to use Redux please read the official documentation. We used most of the ideas, examples and definitions directly from there. 📖