the primary mental model for the Flux programmer.
A predictable state container for JavaScript apps
(state, action) => state
The selector pattern solves the problem of how the application should fetch values from this centralized state.
(state, ...args) => derivation
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
A store enhancer is a higher-order function that composes a store creator to return a new, enhanced store creator.
The Redux DevTools platform is one such store enhancer.
Redux uses only one store for all its application state. Since all state resides in one place, Redux calls this the single source of truth.
According to Redux docs,
"The only way to mutate the state is to emit an action, an object describing what happened."
This means the application cannot modify the state directly. Instead, "actions" are dispatched to express an intent to change the state in the store.
The store object itself has a very small API with only four methods:
store.dispatch(action)
store.subscribe(listener)
store.getState()
replaceReducer(nextReducer)
var action = {
type: 'ADD_USER',
user: {name: 'Dan'}
};
The dispatched action "describes" the state change and an intent to change state.
Reducers are functions that you write which handle dispatched actions and can actually change the state.
A reducer takes in current state as an argument and can only modify the state by returning new state.
Reducers are called "pure" because they do nothing but return a value based on their parameters. They have no side effects into any other part of the system.
// The Reducer Function
var userReducer = function(state, action) {
if (state === undefined) {
state = [];
}
if (action.type === 'ADD_USER') {
state.push(action.user);
}
return state;
}
// Create a store by passing in the reducer
var store = Redux.createStore(userReducer);
// Dispatch our first action
// to express an intent to change the state
store.dispatch({
type: 'ADD_USER',
user: {name: 'Dan'}
});
Here's a brief summary of what's happening:
const userReducer = function(state = [], action) {
if (action.type === 'ADD_USER') {
var newState = state.concat([action.user]);
return newState;
}
return state;
}
{
userState: { ... },
widgetState: { ... }
}
In order to create a store with nested objects, we'll need to define each section with a reducer:
It's still "one store = one object" for the entire application, but it has nested objects for userState and widgetState that can contain all kinds of data.
import { createStore, combineReducers } from 'redux';
// The User Reducer
const userReducer = function(state = {}, action) {
return state;
}
// The Widget Reducer
const widgetReducer = function(state = {}, action) {
return state;
}
// Combine Reducers
const reducers = combineReducers({
userState: userReducer,
widgetState: widgetReducer
});
const store = createStore(reducers);
Then the state returned from each reducer applies to its subsection.
Something very important to note is that now, each reducer gets passed its respective subsection of the overall state, not the whole store's worth of state like with the one-reducer example.
All of them.
Component without Redux
import React from 'react';
import axios from 'axios';
import UserList from '../views/list-user';
class UserListContainer extends React.Component {
constructor() {
this.state = {
users: []
};
}
componentDidMount() {
axios.get('/path/to/user-api').then(response => {
this.setState({users: response.data});
});
}
render() {
return <UserList users={this.state.users} />;
}
};
The react-redux module allows us to "connect" React components to Redux in a more convenient way.
import { connect } from 'react-redux';
import store from '../path/to/store';
class UserListContainer extends React.Component {
componentDidMount() {
axios.get('/path/to/user-api').then(response => {
store.dispatch({
type: 'USER_LIST_SUCCESS',
users: response.data
});
});
}
render() {
return <UserList users={this.props.users} />;
}
};
const mapStateToProps = function(store) {
return {
users: store.userState.users
};
}
export default connect(mapStateToProps)(UserListContainer);
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import router from './router';
ReactDOM.render(
<Provider store={store}>{router}</Provider>,
document.getElementById('root')
);
const initialUserState = {
users: []
}
const userReducer = function(state = initialUserState, action) {
switch(action.type) {
case 'USER_LIST_SUCCESS':
return Object.assign({}, state, { users: action.users });
}
return state;
}
Reducer for previous example
Dispatching from Events
const mapDispatchToProps = function(dispatch, ownProps) {
return {
toggleActive: function() {
dispatch({ ... });
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(UserListContainer);
When should I use Redux instead?
Attempt #1: Logging Manually
The most naïve solution is just to log the action and the next state yourself every time you call store.dispatch(action).
store.dispatch(addTodo('Use Redux'))
const action = addTodo('Use Redux')
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
This produces the desired effect, but you wouldn't want to do it every time.
Attempt #2: Wrapping Dispatch
You can extract logging into a function:
function dispatchAndLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}
You can then use it everywhere instead of store.dispatch():
dispatchAndLog(store, addTodo('Use Redux'))
Attempt #3: Monkeypatching Dispatch
const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
Problem: Crash Reporting
If logging and crash reporting are separate utilities, they might look like this:
function patchStoreToAddLogging(store) {
const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
function patchStoreToAddCrashReporting(store) {
const next = store.dispatch
store.dispatch = function dispatchAndReportErrors(action) {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, { extra: { action, state: store.getState()}})
throw err
}
}
}
patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
Attempt #4: Hiding Monkeypatching
function logger(store) {
const next = store.dispatch
// Previously:
// store.dispatch = function dispatchAndLog(action) {
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
function applyMiddlewareByMonkeypatching(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
// Transform dispatch function with each middleware.
middlewares.forEach(middleware =>
(store.dispatch = middleware(store)))
}
We could use it to apply multiple middleware like this:
applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
Attempt #5: Removing Monkeypatching
function logger(store) {
// Must point to the function returned
// by the previous middleware:
const next = store.dispatch
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
function logger(store) {
return function wrapDispatchToAddLogging(next) {
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
}
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
Attempt #6: Naïvely Applying the Middleware
// Warning: Naïve implementation!
// That's *not* Redux API.
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
let dispatch = store.dispatch
middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
return Object.assign({}, store, { dispatch })
}
The implementation of applyMiddleware() that ships with Redux is similar, but different in some important aspects.
When you call an asynchronous API, there are two crucial moments in time: the moment you start the call, and the moment when you receive an answer (or a timeout).
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
import fetch from 'cross-fetch'
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}
export function fetchPosts(subreddit) {
return function(dispatch) {
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
error => console.log('An error occurred.', error))
.then(json =>
dispatch(receivePosts(subreddit, json))
)
}
}
store.dispatch(selectSubreddit('reactjs'))