- https://code-cartoons.com/a-cartoon-guide-to-flux-6157355ab207
- https://medium.com/swlh/the-case-for-flux-379b7d1982c6
- https://egghead.io/courses/getting-started-with-redux
- https://github.com/happypoulp/redux-tutorial
-
Redux defines itself as a predictable state container for JS applications.
-
It attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen.
-
It makes you think of your app as an initial state being modified by a sequential list of actions.
-
In summary Redux provides the following:
- A place to put your application state.
- A mechanism to dispatch actions to modifiers of your application state, A.K.A reducers.
- A mechanism to subscribe to state updates.
-
The Redux instance is called a
store
and can be created like this:
import { createStore } from 'redux';
const store = createStore(() => {});
-
createStore
expects a function that will allow it to reduce your state (reducer). -
That instance will hold the state of the application and the state will be mutated by reducer function(s).
-
By passing reducer function(s) into
createStore()
Redux will call that function each time an action occurs.
- Actions represent everything a user can do in an app.
- They are plain JS objects with a
type
property;type
is one ofActionTypes
(string constants defined by your app) E.g
{
type: 'ADD_TODO',
text: 'Use Redux'
}
{
type: 'REMOVE_TODO',
stampId: 42
}
{
type: 'LOAD_ARTICLE',
response: {...}
}
- Action creators are functions used to create and return actions E.g:
// Boilerplate
const actionCreator = () => {
return {
type: 'AN_ACTION'
}
}
function actionCreator() {
return {
type: 'AN_ACTION'
}
}
// actionCreators.js
export function actionCreator() {
return {
type: 'AN_ACTION'
}
}
export function addTodo(text) {
return {
type: 'ADD_TOO',
text
}
}
// AddTodo.js
import { addTodo } from './actionCreators';
// somewhere in an event handler
dispatch(addTodo('Use Redux'))
-
In order for an action to be useful it needs to be dispatched (using a
dispatch
function), the store will then be aware of its occurrence and act accordingly. -
actionCreator -> Action
(the steps so far)
-
In JS a reducer is a function that accepts an accumulation and a value and returns a new accumulation (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)
-
They are used to reduce a collection of values down to a single value E.g
const total = [0, 1, 2, 3].reduce((accumulator, currentValue) => {
return accumulator + currentValue;
});
console.log(total) // 6
-
In Redux, the accumulated value is the
state
object and the values being accumulated are actions. Reducers calculate a new state given the previous state and an action. -
Reducers must be pure functions (functions that
return
the exact same output for given inputs). -
A Reducer is a subscriber to actions.
-
When creating a Redux instance you must give it a reducer function so that Redux can call this function(s) on the application state each time an action occurs.
-
By passing reducers to
createStore
Redux registers action handlers.
import { createStore } from 'redux';
// Boilerplate structure for reducers
const store = createStore(() => {});
const reducer_0 = (...args) => {
console.log('Reducer was called with args', args);
};
const store_0 = createStore(reducer); // Reducer was called with args [ undefined, { type: '@@redux/INIT' } ]
-
[ undefined, { type: '@@redux/INIT' } ]
- To initialize the state of the application Redux dispatches an
INIT
action ({ type: '@@redux/INIT' }
) - When this initialization takes place the state is yet to be defined and so we get
undefined
. - A reducer receives both the current state and an action as parameters:
(state, action)
- Since none of these parameters were declared in the example above it returns the defaults:
[ undefined, { type: '@@redux/INIT' } ]
- To initialize the state of the application Redux dispatches an
-
A store is an object that holds the application's state tree.
-
There should only be a single store.
-
The base dispatch function always synchronously sends an action to the store's reducer, along with the previous state returned by the store, to calculate a new state. Actions are expected to be plain objects.
store = {
dispatch: Dispatch // The base dispatch function
getState: () => State // Returns the current state of the store
subscribe: (listener: () => void) => () => void // Registers a function to be called on state changes
replaceReducer: (reducer: Reducer) => void // Can be used to implement hot reloading and code splitting.
}
import { createStore } from 'redux';
const reducer_0 = (state, action) => {
console.log(`reducer_0 was called with state, ${state}, and action, ${action}`);
}
const store_0 = createStore(reducer_0); // reducer_0 was called with state, undefined, and action, { type: '@@redux/INIT' }
- To get the state you call
getState
E.g:
console.log('store_0 state after initialization:', store_0.getState()); // store_0 state after initialization: undefined
- After initialization state returns
undefined
because the reducer in this case (reducer_0
) isn't modifying the state object of the application
const reducer_1 = (state, action) => {
console.log(`reducer_1 was called with state, ${state}, and action, ${action}`);
if (typeof state === 'undefined') {
return {};
}
return state;
}
const store_1 = createStore(reducer_1); // reducer_1 was called with state, undefined, and action, { type: '@@redux/INIT' }
console.log('store_1 state after initialization:', store_1.getState()); // store_1 state after initialization: {}
- You can use ES6 default parameters to clean up the pattern above :
const reducer_2 = (state = {}, action) => {
console.log(`reducer_2 was called with state, ${state}, and action, ${action}`);
return state;
}
const store_2 = createStore(reducer_2); // reducer_2 was called with state, {}, and action, { type: '@@redux/INIT' }
console.log('store_2 state after initialization:', store_2.getState()); // store_2 state after initialization: {}
-
The
state
parameter has been initialized in the reducer and no longer returnsundefined
-
However a reducer is only called in response to an action that has been dispatched. E.g
const reducer_3 = (state= {}, action) => {
console.log(`reducer_3 was called with state, ${state}, and action, ${action}`);
switch (action.type) { // `action.type` assumes that the action contains a type property
case 'SAY_SOMETHING':
return {
...state, // [ES7 object spread](https://goo.gl/ST86RO) is used to merge the current state with the new returned state {message: action.value}
message: action.value // `action.value` assumes that the action contains a value property
}
default: // Always use `default` to `return state` to avoid getting `undefined` and losing the current state
return state;
}
}
const store_3 = createStore(reducer_3); // reducer_3 was called with state {}, and action, { type: '@@redux/INIT' }
console.log('store_3 state after initialization:', store_3.getState()); // store_3 state after initialization {}
- Boilerplate reducer function:
const reducer_0 = (state = {}, action) => {
console.log(`reducer_0 was called with state, ${state}, and action, ${action}`);
switch (action.type) {
case 'SAY_SOMETHING':
return {
...state,
message: action.value
}
default:
return state;
}
};
- Boilerplate reducer function with multiple actions:
const reducer_1 = (state = {}, action) => {
console.log(`reducer_1 was called with state, ${state}, and action, ${action}`);
switch (action.type) {
case 'SAY_SOMETHING':
return {
...state,
message: action.value
}
case 'DO_SOMETHING':
// ...
case 'LEARN_SOMETHING':
// ...
case 'HEAR_SOMETHING':
// ...
case 'GO_SOMEWHERE':
// ...
// etc...
default:
return state;
}
};
- The boilerplate with multiple actions will get hard to maintain so it makes sense to split these actions into multiple reducers E.g:
const userReducer = (state = {}, action) => {
console.log(`userReducer was called with state, ${state}, and action, ${action}`);
switch (action.type) {
// ...etc
default:
return state;
}
};
const itemsReducer = (state= [], action) => {
console.log(`itemsReducer was called with state, ${state}, and action, ${action}`);
switch (action.type) {
// ...etc
default:
return state;
}
};
-
A reducer can handle any type of data structure; in the code above
userReducer
state has been initialized to an object literal ({}
) whileitemsReducer
state has been initialized to an array ([]
). -
By splitting the actions into multiple reducers each reducer will handle only a slice of the application state
-
Though the
createStore()
method only accepts one reducer function, so we will have to combine them -
We can do this by using the
combineReducers()
method; it takes an object and returns a function that, when invoked, will call all the reducers, retrieve the new slice of state and combine them into a state object E.g:
import { createStore, combineReducers } from 'redux';
const reducer = combineReducers({
user: userReducer,
items: itemsReducer
});
const store_0 = createStore(reducer);
console.log('store_0 state after initialization:', store_0.getState());
/*
* Output:
* userReducer was called with state {} and action { type: '@@redux/INIT' }
* userReducer was called with state {} and action { type: * * '@@redux/PROBE_UNKNOWN_ACTION_l.5.a.9.z.q.f.f.l.x.r' }
* itemsReducer was called with state [] and action { type: '@@redux/INIT' }
* itemsReducer was called with state [] and action { type: * '@@redux/PROBE_UNKNOWN_ACTION_v.d.0.f.i.b.g.w.r.k.9' }
* userReducer was called with state {} and action { type: '@@redux/INIT' }
* itemsReducer was called with state [] and action { type: '@@redux/INIT' }
* store_0 state after initialization: { user: {}, items: [] }
*/
-
The
dispatch
method is used to handle actions and it is provided by Redux on the store object -
It propagates our action(s) to all the available reducers E.g:
import { createStore, combineReducers } from 'redux';
// Create reducers:
const userReducer = (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;
}
}
const itemsReducer = (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;
}
}
const reducer = combineReducers({
user: userReducer,
items: itemsReducer
});
// Pass reducers into store:
const store = createStore(reducer);
console.log('store state after initialization:', store.getState()); // { user: {}, items: [] }
// Dispatch an action
store.dispatch({
type: 'AN_ACTION'
});
/* Output:
* userReducer was called with state {} and action { type: '@@redux/INIT' }
* userReducer was called with state {} and action { type: '@@redux/PROBE_UNKNOWN_ACTION_d.i.h.9.w.g.9.1.9.k.9' }
* itemsReducer was called with state [] and action { type: '@@redux/INIT' }
* itemsReducer was called with state [] and action { type: '@@redux/PROBE_UNKNOWN_ACTION_z.u.l.y.x.6.i.g.g.b.9' }
* userReducer was called with state {} and action { type: '@@redux/INIT' }
* itemsReducer was called with state [] and action { type: '@@redux/INIT' }
* store state after initialization: { user: {}, items: [] }
* userReducer was called with state {} and action { type: 'AN_ACTION' }
* itemsReducer was called with state [] and action { type: 'AN_ACTION' }
*/
-
In the code above an action is passed directly into the
dispatch
method and we can see that the action (AN_ACTION
) has been called on each reducer based on the last 2 lines in the output -
But this action (
AN_ACTION
) doesn't modify the state and we can confirm this by calling thegetState
method after it was dispatched E.g
console.log('store state after action AN_ACTION :', store.getState()); // store state after action AN_ACTION: { user: {}, items: [] }
- Using an action creator:
const setNameActionCreator = (name) => {
type: 'SET_NAME',
name
}
store.dispatch(setNameActionCreator('bob'));
console.log('store state after action SET_NAME :', store.getState());
/* Output:
* userReducer was called with state {} and action { type: 'SET_NAME', name: 'bob' }
* itemsReducer was called with state [] and action { type: 'SET_NAME', name: 'bob' }
* store state after action SET_NAME: { user: { name: 'bob' }, items: [] }
*/
-
In the code above an action creator is used to pass the the action into the
dispatch
method instead and it also modifies state using thename
parameter -
The
name
property is initially returned inuserReducer
(name: action.name
) and alsosetNameActionCreator()
returns a type property that matches thatcase
(SET_NAME
). Therefore thename
property returned insetNameActionCreator
will update that which is returned inuserReducer
. -
In the output above you can see that the application state has been updated to reflect this update:
{ user: { name: 'bob' }, items: [] }