_________ ____________ ___________
| | | | | |
| Action |------------▶| Dispatcher |------------▶| callbacks |
|_________| |____________| |___________|
▲ |
| |
| |
_________ ____|_____ ____▼____
| |◀----| Action | | |
| Web API | | Creators | | Store |
|_________|----▶|__________| |_________|
▲ |
| |
____|________ ____________ ____▼____
| User | | React | | Change |
| interactions |◀--------| Views |◀-------------| events |
|______________| |___________| |_________|
What makes up a web app? In the MVC world, there are 3 major pieces - model, view, and controller (hence the name MVC)
- You need some data (model)
- You need a presentation layer (view)
- You need logic to glue views and user events, get data, handle data modifictations, etc (controller)
The concept of flux has similar concepts with different vocabulary
- Your concept of a model is the store
- Your concept of a view layer is React component (flux doesn't care)
- Your concept of user events, data modifications, and their handlers look like: 'action creators' -> action -> dispatcher -> callback
The difference between MVC and flux then come from the role of actions. Actions include the set of data fetching and user events. Instead of having handlers directly modify the model and view for those actions, flux makes actions go through a pipeline prior to hitting the store or view. Actions go through a dispatcher, then through stores, and finally all watchers of stores are notified.
Each store can only be modified by an action and nothing else. Once all stores have replied to the action, the views can finally re-render. So in the end, data always flows in one way:
action -> store -> view -> action -> store -> view -> action -> ...
What's the relationship between actions and action creators?
// The action creator is just a function that creates and returns an action!
var actionCreator = function() {
return {
type: 'AN_ACTION'
}
};
console.log(actionCreator()); // output: { type: 'AN_ACTION' }
That's it? Hell yes.
Flux has a convention of formatting an action object to contain a type property. Action objects can have other properties as well, but type allows further handling to take place. Technically, action creators can return things (like functions) other than an action which is most useful for asynchronous action handling. We'll look more into this later.
For action creators and actions to be useful, actions need to go somewhere so that anyone interested could be notified something happened and then act accordingly. This process is known as dispatching an action.
- To dispatch an action we need a dispatch function
- To let anyone interested know an action happened, we need a way to register subscribers
The application flow so far:
Action Creator -> Action
Read more about actions and action creators here: ReducingBoilerplate.
To illustrate, here's a theoretical subscriber. It is not registered anywhere yet but will be. When called, it will do what is was designed for - here, just a console.log.
var mySubscriber = function() {
console.log('Something happened.');
};
mySubscriber();
The application flow so far:
Action Creator -> Action ... Subscriber
Redux is a "predictable state container for JavaScript apps."
- Data can be stored in any way you like (JS object, array, Immutable structure, etc)
- Data of your application is called state (since it changes over time)
- We hand over state to Redux
-
By using reducers
-
A reducer is a function that receives the current app state, the action, and returns a new state modified (or reduced as we call it)
var myReducer = function(currentState, action) { /* ... */ return newState; }
- By using subscribers to state's updates
tldr; Redux provides:
- a place to put your application state
- a mechanism to subscribe to state updates
- a mechanism to dispatch actions to modifiers of your application state aka reducers
The Redux instance is called a store and can be created like this:
import { createStore } from 'redux';
// `createStore` expects a reducer that will allow it to reduce your state
let store = createStore(() => {});
Reducer functions help transform your app's state. A reducer is only called in response to an action dispatched.
A store keeps your data in it while a reducer does not. Traditionally in flux, stores hold state in them. However, in Redux each time a reducer is called, it is passed the state that needs updating. This way, redux's stores become "stateless stores" and thus renamed reducers.
/**
* A Redux instance expects a reducer so that Redux can this function on
* your application state each time an action occurs.
*/
import { createStore } from 'redux';
// A reducer is given these parameters: (state, action)
let reducer = function(...args) {
console.log('Reducer was called with args', args);
};
let store_1 = createStore(reducer);
// output: Reducer was called with args [ undefined, { type: '@@redux/INIT' } ]
Notice that the reducer is called even if we didn't dispatch any action. Why?
Redux actually dispatches an init action ({ type: '@@redux/INIT' }) to initialize the state of the app. Logically, right before initialization the state is "undefined".
To get the state that redux is holding for us use getState()
console.log('store_1 state after initialization:', store_1.getState());
// output: store_1 state after initialization: undefined
In the above example, the state is still undefined because the reducer we passed into store_1 does not return anything. This is expected. So let's give our store a proper state after initialization.
import { createStore } from 'redux';
// A reducer is given these parameters: (state, action)
let reducer = function(state = {}, action) {
console.log('reducer was called with state', state, 'and action', action);
return state;
};
let store_1 = createStore(reducer);
// output: reducer was called with state {} and action { type: '@@redux/INIT' }
console.log('store_1 state after initialization:', store_1.getState());
// output: store_1 state after initialization: {}
Note that a reducer can handle any type of data structure (an object literal {}, an array [], a boolean, a string, an immutable structure, ...).
Recall that a reducer is only called in response to a dispatched action. We will fake a state modification in response to an action of type 'SOMETHING'.
let reducer_2 = function(state = {}, action) {
console.log('reducer_2 was called with state', state, 'and action', action);
switch(action.type) {
case 'SOMETHING':
return {
...state,
message: action.value
};
default:
return state;
}
};
let store_2 = createStore(reducer_2);
// output: reducer_2 was called with state {} and action { type: '@@redux/INIT' }
console.log('store_2 state after initialization:', store_2.getState());
// output: store_2 state after initialization: {}
We see nothing new with our new state because no action dispatched. However, there are several points to note:
- We assume that action contains a type (a flux convention) and value (could be anything else) property.
- The
switchpattern is a good response to an action received in reducers. - Don't forget
default: return statewhen usingswitch, or else you end up with your reducer returning undefined and losing state. - ES7 notation of object spread
{ ...state, message: action.value }is awesome and used to merge the current state with the new returned state. - FYI ES7 object spread does a shallow copy of
{ message: action.value }over our state. This means the first level properties of state are completely overwritten by first level property of{ message: action.value }(as opposed to graceful merging). We can handle state updates differently with more complex nested data structures (using Immutable.js; using Object.assign; manual merge or anything else!
After getting a taste for how to handle actions in reducers, let's explore combining multiple reducers.
A single reducer function cannot hold all of our app's action handling in a maintainable way. Fortunately, redux doesn't care how many reducers we have - it will even help combine them if you do have multiple ones.
With the approach of having multiple reduces, we can end up with each reducer handling only a slice of app state. See below for 2 example reducers.
let userReducer = function (state = {}, action) {
console.log('userReducer was called with state', state, 'and action', action)
switch (action.type) {
// etc.
default:
return state;
}
}
let itemsReducer = function (state = [], action) {
console.log('itemsReducer was called with state', state, 'and action', action)
switch (action.type) {
// etc.
default:
return state;
}
}
However, createStore only expects one reducer function. So how do we combine reducers?
Redux has a combineReducers helper function that takes an object of all child reducers and returns a function that when invoked will:
-
call all our reducers,
-
retrieve the new state slice, and
-
reunite them in a state object ({}) that redux is holding.
import { createStore, combineReducers } from 'redux';
let reducer = combineReducers({ user: userReducer, items: itemsReducer }); // Output: // userReducer was called with state {} and action { type: '@@redux/INIT' } // userReducer was called with state {} and action { type: '@@redux/PROBE_UNKNOWN_ACTION_9.r.k.r.i.c.n.m.i' } // itemsReducer was called with state [] and action { type: '@@redux/INIT' } // itemsReducer was called with state [] and action { type: '@@redux/PROBE_UNKNOWN_ACTION_4.f.i.z.l.3.7.s.y.v.i' }
let store_0 = createStore(reducer) // Output: // userReducer was called with state {} and action { type: '@@redux/INIT' } // itemsReducer was called with state [] and action { type: '@@redux/INIT' }
Each reducer is called correctly with the init action @@redux/INIT. The other action is just a sanity check implemented in combineReducers to ensure that a reducer will always return a non-undefined state.
console.log('store_0 state after initialization:', store_0.getState())
// Output:
// store_0 state after initialization: { user: {}, items: [] }
We see that redux handles our slices of state correctly. The final state is just a simple state object made of the userReducer's slice and the itemReducer's slice.
Awesome, now that we have a pretty good idea of how reducers work, let's dive into how dispatched actions impact our redux state.
So far we focused on building reducers but haven't dispatched any actions. We will proceed with the same reducers from previous examples and handle a few actions.
let userReducer = function(state = {}, action) {
console.log('userReducer was called with state', state, 'and action', action);
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.name
};
default:
return state;
}
};
let itemsReducer = function (state = [], action) {
console.log('itemsReducer was called with state', state, 'and action', action);
switch (action.type) {
case 'ADD_ITEM':
return [
...state,
action.item
];
default:
return state;
}
};
import { createStore, combineReducers } from 'redux';
let reducer = combineReducers({
user: userReducer,
items: itemsReducer
});
let store_0 = createStore(reducer);
console.log("\n", '### It starts here');
console.log('store_0 state after initialization:', store_0.getState());
// Output:
// store_0 state after initialization: { user: {}, items: [] }
Let's dispatch our first action. To dispatch an action we need a dispatch function. Fortunately, redux provides the dispatch function that will propagate our action to all reducers.
store_0.dispatch({
type: 'AN_ACTION'
});
// Output:
// userReducer was called with state {} and action { type: 'AN_ACTION' }
// itemsReducer was called with state [] and action { type: 'AN_ACTION' }
console.log('store_0 state after action AN_ACTION:', store_0.getState())
// Output: store_0 state after action AN_ACTION: { user: {}, items: [] }
In the flux way, we should be using an action creator to send an action. That would look like is:
let setNameActionCreator = function(name) {
return {
type: 'SET_NAME',
name: name
};
};
store_0.dispatch(setNameActionCreator('Tommy'));
// Output:
// userReducer was called with state {} and action { type: 'SET_NAME', name: 'Tommy' }
// itemsReducer was called with state [] and action { type: 'SET_NAME', name: 'Tommy' }
console.log('store_0 state after action SET_NAME:', store_0.getState())
// Output:
// store_0 state after action SET_NAME: { user: { name: 'Tommy' }, items: [] }
Here we see that we dispatched our first action and changed the app's state.
But this a trivial example and not close enough to a real use-case. For example, what if we'd like do some async work in our action creator before dispatching the action?
So far here is the flow of our application
ActionCreator -> Action -> dispatcher -> reducer
So far we've only considered action creators that produce an action synchronously - when called an action is returned immediately.
What about async use-cases? Imagine this scenario:
- User clicks on button "Say Hi in 2 Seconds"
- When button "A" is clicked, we'd like to show message "Hi" after 2 seconds elapsed
- 2 seconds later, our view is updated with the message "Hi"
The way we've written our action creators before does not support this scenario. Instead, once the action creator is called, the store saves the message immediately.
We'd like to see an action creator that looks like this:
let asyncSayActionCreator_0 = function(message) {
setTimeout(function() {
return {
type: 'SAY',
message
};
}, 2000);
};
However, this action creator does not return an action. It will return undefined. The trick then is, instead of returning an action, we'll return a function. This function will dispatch the appropriate action. Since we want our function to be able to dispatch the action, it should be given redux's dispatch function. Now it will look like this:
let asyncSayActionCreator_1 = function(message) {
return function(dispatch) {
setTimeout(function() {
dispatch({
type: 'SAY',
message
});
}, 2000);
}
};
Note that this function won't even reach our reducer. The action creator itself is well-written. We just need to use redux's custom middleware for async actions.
Middleware is a new Redux concept. Generally, middleware is something that goes between parts A and B of an app to transform what A sends before passing it to B. Somehow, redux middleware helps us handle async actions.
A ----------------> B // instead of this we get
A --> middleware1 --> middleware2 --> ... --> B
In the context of Redux, we saw that the function we are returning from our async action creator cannot be handled natively by redux. But having middleware between our action creator and our reducers, we could this function into something that suits Redux
action --> dispatcher --> --> middleware1 --> middleware2 --> reducers
Our middleware will be called each time an action (or function) is dispatched and it should be able to help our action creator dispatch the real action when it wants to (or do nothing).
Essentially, middleware is functions that must conform to a very strict and specific signature:
let anyMiddleware = function({ dispatch, getState }) {
return function(next) {
return function (action) {
// your middleware-specific code goes here
}
};
};
A middleware is made up of 3 nested functions:
- The first level provides the
dispatchfunction and agetStatefunction to the 2 other levels - The second level provides the next function that will allow you to explicitly hand over your transformed input to the next middleware or to redux (so that redux can finally call all reducers)
- The third level provides the action received from the previous middleware or from your dispatch and can either trigger the next middleware (to let the action continue to flow) or process the action in any appropriate way
The middleware we will build for our async action creator is called a thunk middleware. Here is what it looks like (translated to ES5 for readability):
let thunkMiddleware = function({ dispatch, getState }) {
// console.log('Enter thunkMiddleware');
return function(next) {
// console.log('Function "next" provided: ', next);
return function(action) {
// console.log('Handling action: ', action);
return typeof action === 'function' ?
action(dispatch, getState) :
next(action);
};
};
};
To tell redux we have >1 middleware, we must use redux's applyMiddleware helper function. applyMiddleware takes all your middleware as parameters and returns a function to be called with redux's createStore. When the last function is invoked, it will produce a higher-order store that applies middleware to a store's dispatch
So here's how it could look to integrate middleware to your redux store
import { createStore, combineReducers, applyMiddleware } from 'redux';
const finalCreateStore = applyMiddleware(thunkMiddleware)(createStore);
// For multiple middlewares, write:
// `applyMiddleware(middleware1, middleware2, ...)(createStore)`
let reducer = combineReducers({
speaker: function(state = {}, action) {
console.log('`speaker` was called with state', state, 'and action', action);
switch (action.type) {
case 'SAY':
return {
...state,
message: action.message
}
default:
return state
}
}
});
const store_0 = finalCreateStore(reducer);
// Output:
// speaker was called with state {} and action { type: '@@redux/INIT' }
// speaker was called with state {} and action { type: '@@redux/PROBE_UNKNOWN_ACTION_s.b.4.z.a.x.a.j.o.r' }
// speaker was called with state {} and action { type: '@@redux/INIT' }
let asyncSayActionCreator_1 = function(message) {
return function(dispatch) {
setTimeout(function() {
console.log(new Date(), 'Dispatch action now:');
dispatch({
type: 'SAY',
message
});
}, 2000);
}
};
console.log(new Date(), 'Running our async action creator:\n');
store_0.dispatch(asyncSayActionCreator_1('Hi'));
// Output:
// Mon Aug 03 2015 00:01:20 GMT+0200 (CEST) Running our async action creator:
// Mon Aug 03 2015 00:01:22 GMT+0200 (CEST) 'Dispatch action now:'
// speaker was called with state {} and action { type: 'SAY', message: 'Hi' }
// Middleware example for logging all actions that are dispatched
function logMiddleware({ dispatch, getState }) {
return function(next) {
return function(action) {
console.log('logMiddleware action received:', action);
return next(action);
}
}
};
OK wow, that was a little long. So what we've seen now is that our action is correctly dispatched 2 seconds after our call to the async action creator! Check out http://gaearon.github.io/redux/docs/introduction/Ecosystem.html for more middleware examples.
- We know how to write actions and action creators
- We know how to dispatch our actions
- We know how to handle custom actions like async actions thanks to middleware
The missing piece of a flux app is to be notified about state updates to be able to react to them (for example by re-rendering components). How do we subscribe to our redux store updates?
_________ __________ ___________
| | | Change | | React |
| Store |----▶ events |-----▶ Views |
|_________| |__________| |___________|
Without knowing about store changes, we cannot update our views. Fortunately redux provides a way to watch over our redux store updates.
store.subscribe(function() {
// Retrieve latest store state here
// Example:
console.log('store_0 has been updated. Latest store state:', store_0.getState());
});
store_0.dispatch(addItemActionCreator({
id: 12345,
description: 'anything '
}));
// Output:
// ...
// store_0 has been updated. Latest store state: { items: [ { id: 1234, description: 'anything' } ] }
Cool, our subscribe callback is called correctly and our store now contains the new item we added.
- Our subscriber callback did not receive the state as a parameter, why?
- Since we did not receive our new state, we were bound to exploit our closured store (store_0) so this solution is not acceptable in a real multi-modules application...
- How do we actually update our views?
- How do we unsubscribe from store updates?
- More generally speaking, how should we integrate Redux with React?
To reiterate, redux is a predictable state container for Javascript apps. It has a minimalist API that is extensible. That means redux's simplicity and flexibility allows it to be used in many ways, not just React. Having said that, for using redux in React apps, we can simplify our lives tremendously by using react-redux.
We still need a better API to subscribe to store changes. That's what react-redux brings: an API that enables us to seamlessly fill the the gap between raw redux subscriptions and developer expectations. In other words, we don't have to use subscribe directly. We can instead use higher-level bindings like provide or connect which will hide us from subscribe.
It is simple to bind your React components to redux. Follow the example in: ./12_src/src. Comments are inlined into the source code.
The example contains a simple React web app. It contains
- A simple HTTP server
- Webpack for bundling the app
- Webpack Dev Server to serve Javascript files from a dedicated node server that allows for file watching
- React Hot Loader for having components live-reload in the browser as changes are made in the source code.
We focus on 2 main react-redux bindings
- the Provider component
- the connect decorator
Provider is a React Component designed to be used as a wrapper of your application's root component. Its purpose is to provide your redux instance to all of your application's components. See ./application.jsx
import React from 'react'
import Home from './home'
import { Provider } from 'react-redux'
export default class Application extends React.Component {
render () {
return (
// As explained above, the Provider must wrap your application's Root component. This way,
// this component and all of its children (even deeply nested ones) will have access to your
// Redux store. Of course, to allow Provider to do that, you must give it the store
// you built previously (via a "store" props).
<Provider store={ this.props.store }>
<Home />
</Provider>
)
}
}
How do we read from our store's state and how do we dispatch actions? The answer comes from the @connect class decorator (ES7 feature).
The "connect" decorator (written @connect) literally connects your component with your Redux's store. By doing so, it provides your store's dispatch function through a component's prop and also adds any properties you want to expose as part of your store's state.
Using @connect, you'll turn a dumb component, into a smart component with very little code overhead. See ./home.jsx.
This is the signature for @connect
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])