Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Connect.Tech 2017: Get Started with Redux
Search
Jeremy Fairbank
September 21, 2017
Programming
500
2
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Connect.Tech 2017: Get Started with Redux
Jeremy Fairbank
September 21, 2017
More Decks by Jeremy Fairbank
See All by Jeremy Fairbank
Connect.Tech 2020: Advanced Cypress Testing
jfairbank
1
260
CodeMash 2020: Solving the Boolean Identity Crisis
jfairbank
1
230
CodeMash 2020: Practical Functional Programming
jfairbank
1
370
Connect.Tech 2019: Practical Functional Programming
jfairbank
0
450
Connect.Tech 2019: Solving the Boolean Identity Crisis
jfairbank
0
270
Lambda Squared 2019: Solving the Boolean Identity Crisis
jfairbank
0
210
All Things Open 2018: Practical Functional Programming
jfairbank
2
300
Connect.Tech 2018: Effective React Testing
jfairbank
1
230
Fluent Conf 2018: Building web apps with Elm Tutorial
jfairbank
2
950
Other Decks in Programming
See All in Programming
Domain-Driven Transformation
hschwentner
2
2.3k
dotNET_vulnclicker_intro.pdf
deka_morita
0
150
学びを記事・スライド・Xへ高速展開する― AI-assisted Learning Loopで回すコンテンツ生成術
ochtum
0
150
SFCで実現する病院・医療システムの動的組み立て式UI
koukimiura
1
120
[2026-10-09]一枚の布で作らなくてもいい。 信頼性は「パッチワーク」で紡がれる~既存の運用と新しい可能性をつなぎ合わせてきたCREの話~
tosite
0
110
自作Discord音声botをLambda MicroVMsに移行した話 / Migrating My Discord Voice Bot to Lambda MicroVMs
akihisaikeda
0
150
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
430
コードで差別化できない時代の ソフトウェアエンジニアリング / Software Engineering in an Era Where Code Doesn’t Differentiate
kishida
2
200
JIS keyboard support in Omarchy
lef237
0
160
[2026-10-14](C)Re:ゼロから始める異世界生活〜始まりの終わりと終わりの始まり〜
tosite
0
160
作れる、でも消せるか?──安全に「枯らして消す」機能削除術 / Drain, Then Delete
chobishiba
2
1k
AI時代のTerminal環境を UNIX哲学で設計・運用する
yasunori0418
1
240
Featured
See All Featured
Site-Speed That Sticks
csswizardry
13
1.6k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
How to Grow Your eCommerce with AI & Automation
katarinadahlin
PRO
2
330
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
31
2.9k
How to Align SEO within the Product Triangle To Get Buy-In & Support - #RIMC
aleyda
2
1.8k
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
940
HU Berlin: Industrial-Strength Natural Language Processing with spaCy and Prodigy
inesmontani
PRO
0
760
The AI Revolution Will Not Be Monopolized: How open-source beats economies of scale, even for LLMs
inesmontani
PRO
3
3.8k
Chasing Engaging Ingredients in Design
codingconduct
0
370
The Straight Up "How To Draw Better" Workshop
denniskardys
239
140k
Have SEOs Ruined the Internet? - User Awareness of SEO in 2025
akashhashmi
0
530
Leveraging Curiosity to Care for An Aging Population
cassininazir
1
540
Transcript
Jeremy Fairbank @elpapapollo / jfairbank Get Started with Redux
Software is broken. We are here to fix it. Say
[email protected]
The Wild West of State
<div data-id="42" data-name="Tucker"> ... </div> var $el = $('[data-id="42"]'); var
currentName = $el.data('name'); $el.data('name', currentName.toUpperCase()); Data in the DOM
Controller View Model Model Model Model Model View View View
View MVC
View Model Model Model Model Model View Model View Model
View Model View Model Model Two-way Data Binding
The Wild West of State State is everywhere and anyone
can change it!
Predictable State Container
All application state in one place State Container
Predictable Old State REDUCER New State
Predictable Old State REDUCER New State • State changes in
one place
Predictable Old State REDUCER New State • State changes in
one place • State changes in well-defined ways
Predictable Old State REDUCER New State • State changes in
one place • State changes in well-defined ways • Changes are serialized
Reducer View State Actions
Reducer View State Actions
Reducer View React, Angular, Vanilla JS, etc. State Actions
Reducer View State Actions Dispatch
Reducer View State Actions
<button id="decrement">-</button> <div id="counter">0</div> <button id="increment">+</button>
incrementBtn.addEventListener('click', () => { counter.innerText = Number(counter.innerText) + 1; });
decrementBtn.addEventListener('click', () => { counter.innerText = Number(counter.innerText) - 1; });
let state = 0; function render() { counter.innerText = state;
} incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
let state = 0; function render() { counter.innerText = state;
} incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
let state = 0; function render() { counter.innerText = state;
} incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
let state = 0; function render() { counter.innerText = state;
} incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
state += 1; state -= 1; Anyone can access and
mutate state
state += 1 state -= 1 { type: 'INCREMENT' }
{ type: 'DECREMENT' } Tokens/descriptors that describe a type of change. Actions
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
Updating state is just a function call now let state
= 0; state = reducer(state, { type: 'INCREMENT' }); // 1 state = reducer(state, { type: 'INCREMENT' }); // 2 state = reducer(state, { type: 'DECREMENT' }); // 1 state = reducer(state, { type: 'ADD_2' }); // 1 console.log(state); // 1
Updating state is just a function call now let state
= 0; state = reducer(state, { type: 'INCREMENT' }); // 1 state = reducer(state, { type: 'INCREMENT' }); // 2 state = reducer(state, { type: 'DECREMENT' }); // 1 state = reducer(state, { type: 'ADD_2' }); // 1 console.log(state); // 1 Unhandled types are ignored
function dispatch(action) { state = reducer(state, action); render(); } incrementBtn.addEventListener('click',
() => { dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click', () => { dispatch({ type: 'DECREMENT' }); }); Decouple event from state change.
function dispatch(action) { state = reducer(state, action); render(); } incrementBtn.addEventListener('click',
() => { dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click', () => { dispatch({ type: 'DECREMENT' }); }); Decouple event from state change.
function dispatch(action) { state = reducer(state, action); render(); } incrementBtn.addEventListener('click',
() => { dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click', () => { dispatch({ type: 'DECREMENT' }); }); Decouple event from state change.
function dispatch(action) { state = reducer(state, action); render(); } incrementBtn.addEventListener('click',
() => { dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click', () => { dispatch({ type: 'DECREMENT' }); }); Decouple event from state change.
function dispatch(action) { state = reducer(state, action); render(); } incrementBtn.addEventListener('click',
() => { dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click', () => { dispatch({ type: 'DECREMENT' }); }); Decouple event from state change.
STORE
import { createStore } from 'redux'; const store = createStore(reducer,
0); store.getState(); // 0
import { createStore } from 'redux'; const store = createStore(reducer,
0); store.getState(); // 0
import { createStore } from 'redux'; const store = createStore(reducer,
0); store.getState(); // 0
import { createStore } from 'redux'; const store = createStore(reducer,
0); store.getState(); // 0
import { createStore } from 'redux'; const store = createStore(reducer,
0); store.getState(); // 0
incrementBtn.addEventListener('click', () => { store.dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click',
() => { store.dispatch({ type: 'DECREMENT' }); }); store.subscribe(() => { counter.innerText = store.getState(); });
incrementBtn.addEventListener('click', () => { store.dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click',
() => { store.dispatch({ type: 'DECREMENT' }); }); store.subscribe(() => { counter.innerText = store.getState(); });
incrementBtn.addEventListener('click', () => { store.dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click',
() => { store.dispatch({ type: 'DECREMENT' }); }); store.subscribe(() => { counter.innerText = store.getState(); });
Store Reducer State View 0
Store Reducer State View 0
Store Reducer State View 0 getState
Store Reducer View State 0
Store Reducer View State 0 INCREMENT dispatch
Store Reducer View State 0 INCREMENT dispatch
Store Reducer View State 0
Store Reducer View State 1
Store Reducer View State 1
Store Reducer View State 1 1 subscribe getState
incrementBtn.addEventListener('click', () => { store.dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click',
() => { store.dispatch({ type: 'DECREMENT' }); }); Problems: • Creating actions are cumbersome • Requires direct access to store
const increment = () => ({ type: 'INCREMENT', }); const
decrement = () => ({ type: 'DECREMENT', }); Reusable functions that create actions Action Creators
const increment = () => ({ type: 'INCREMENT', }); const
decrement = () => ({ type: 'DECREMENT', }); Reusable functions that create actions Action Creators
const increment = () => ({ type: 'INCREMENT', }); const
decrement = () => ({ type: 'DECREMENT', }); Reusable functions that create actions Action Creators
incrementBtn.addEventListener('click', () => { store.dispatch(increment()); }); decrementBtn.addEventListener('click', () => {
store.dispatch(decrement()); }); Problems: • Creating actions are cumbersome • Requires direct access to store
const actions = { increment: () => store.dispatch(increment()), decrement: ()
=> store.dispatch(decrement()), }; Automatically dispatch when invoked Bound Action Creators
const actions = { increment: () => store.dispatch(increment()), decrement: ()
=> store.dispatch(decrement()), }; Automatically dispatch when invoked Bound Action Creators Manually created
const actions = { increment: () => store.dispatch(increment()), decrement: ()
=> store.dispatch(decrement()), }; Automatically dispatch when invoked Bound Action Creators
import { bindActionCreators } from 'redux'; const actions = bindActionCreators(
{ increment, decrement }, store.dispatch ); Automatically dispatch when invoked Bound Action Creators
import { bindActionCreators } from 'redux'; const actions = bindActionCreators(
{ increment, decrement }, store.dispatch ); Automatically dispatch when invoked Bound Action Creators
import { bindActionCreators } from 'redux'; const actions = bindActionCreators(
{ increment, decrement }, store.dispatch ); Automatically dispatch when invoked Bound Action Creators Automatically created
Problems: • Creating actions are cumbersome • Requires direct access
to store incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement);
React +
github.com/reactjs/react-redux npm install --save react-redux Official React bindings for Redux
React Redux Library
Reducer State Actions React Redux
React Redux React Application
React Redux React Application Provider
Component React Redux React Application Provider connect State Action Creators
Component Child React Redux React Application Provider connect State Child
Action Creators
const MyApp = () => ( <div> <button>-</button> <div>0</div> <button>+</button>
</div> );
import React from 'react'; import { render } from 'react-dom';
import { Provider } from 'react-redux'; render(( <Provider store={store}> <MyAppContainer /> </Provider> ), document.getElementById('main'));
import React from 'react'; import { render } from 'react-dom';
import { Provider } from 'react-redux'; render(( <Provider store={store}> <MyAppContainer /> </Provider> ), document.getElementById('main'));
import React from 'react'; import { render } from 'react-dom';
import { Provider } from 'react-redux'; render(( <Provider store={store}> <MyAppContainer /> </Provider> ), document.getElementById('main'));
import { connect } from 'react-redux'; const mapStateToProps = counter
=> ({ counter }); function mapDispatchToProps(dispatch) { return bindActionCreators({ onIncrement: increment, onDecrement: decrement, }, dispatch); } const MyAppContainer = connect( mapStateToProps, mapDispatchToProps )(MyApp);
import { connect } from 'react-redux'; const mapStateToProps = counter
=> ({ counter }); function mapDispatchToProps(dispatch) { return bindActionCreators({ onIncrement: increment, onDecrement: decrement, }, dispatch); } const MyAppContainer = connect( mapStateToProps, mapDispatchToProps )(MyApp);
import { connect } from 'react-redux'; const mapStateToProps = counter
=> ({ counter }); function mapDispatchToProps(dispatch) { return bindActionCreators({ onIncrement: increment, onDecrement: decrement, }, dispatch); } const MyAppContainer = connect( mapStateToProps, mapDispatchToProps )(MyApp);
import { connect } from 'react-redux'; const mapStateToProps = counter
=> ({ counter }); function mapDispatchToProps(dispatch) { return bindActionCreators({ onIncrement: increment, onDecrement: decrement, }, dispatch); } const MyAppContainer = connect( mapStateToProps, mapDispatchToProps )(MyApp);
import { connect } from 'react-redux'; const mapStateToProps = counter
=> ({ counter }); function mapDispatchToProps(dispatch) { return bindActionCreators({ onIncrement: increment, onDecrement: decrement, }, dispatch); } const MyAppContainer = connect( mapStateToProps, mapDispatchToProps )(MyApp);
const MyApp = (props) => ( <div> <button onClick={props.onDecrement}> -
</button> <div>{props.counter}</div> <button onClick={props.onIncrement}> + </button> </div> );
const MyApp = (props) => ( <div> <button onClick={props.onDecrement}> -
</button> <div>{props.counter}</div> <button onClick={props.onIncrement}> + </button> </div> ); Store state mapStateToProps
const MyApp = (props) => ( <div> <button onClick={props.onDecrement}> -
</button> <div>{props.counter}</div> <button onClick={props.onIncrement}> + </button> </div> ); Bound action creators mapDispatchToProps
Immutable Object State
const initialState = { counter: 0, car: { color: 'red',
}, };
const initialState = { counter: 0, car: { color: 'red',
}, };
const initialState = { counter: 0, car: { color: 'red',
}, };
const increment = () => ({ type: 'INCREMENT', }); const
decrement = () => ({ type: 'DECREMENT', }); const changeColor = color => ({ type: 'CHANGE_COLOR', payload: color, });
const increment = () => ({ type: 'INCREMENT', }); const
decrement = () => ({ type: 'DECREMENT', }); const changeColor = color => ({ type: 'CHANGE_COLOR', payload: color, });
const increment = () => ({ type: 'INCREMENT', }); const
decrement = () => ({ type: 'DECREMENT', }); const changeColor = color => ({ type: 'CHANGE_COLOR', payload: color, });
function reducer(state = initialState, action) { switch (action.type) { case
'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
function reducer(state = initialState, action) { switch (action.type) { case
'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
function reducer(state = initialState, action) { switch (action.type) { case
'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
function reducer(state = initialState, action) { switch (action.type) { case
'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
function reducer(state = initialState, action) { switch (action.type) { case
'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
function reducer(state = initialState, action) { switch (action.type) { case
'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch(increment()); store.dispatch(changeColor('green')); //
state = { counter: 1, car: { color: 'red' } } // state = { counter: 1, car: { color: 'green' } }
store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch(increment()); store.dispatch(changeColor('green')); //
state = { counter: 1, car: { color: 'red' } } // state = { counter: 1, car: { color: 'green' } }
store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch(increment()); store.dispatch(changeColor('green')); //
state = { counter: 1, car: { color: 'red' } } // state = { counter: 1, car: { color: 'green' } }
function reducer(state = initialState, action) { }
Reducer Composition Create modular reducers for better organization and readability
Root Reducer Counter Reducer Car Reducer
function counterReducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
const initialState = { color: 'red' }; function carReducer(state =
initialState, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, color: action.payload }; default: return state; } }
const initialState = { color: 'red' }; function carReducer(state =
initialState, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, color: action.payload }; default: return state; } }
const initialState = { color: 'red' }; function carReducer(state =
initialState, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, color: action.payload }; default: return state; } }
function reducer(state = {}, action) { return { counter: counterReducer(state.counter,
action), car: carReducer(state.car, action), }; }
function reducer(state = {}, action) { return { counter: counterReducer(state.counter,
action), car: carReducer(state.car, action), }; }
function reducer(state = {}, action) { return { counter: counterReducer(state.counter,
action), car: carReducer(state.car, action), }; }
function reducer(state = {}, action) { return { counter: counterReducer(state.counter,
action), car: carReducer(state.car, action), }; }
import { combineReducers } from 'redux'; const reducer = combineReducers({
counter: counterReducer, car: carReducer, });
import { combineReducers } from 'redux'; const reducer = combineReducers({
counter: counterReducer, car: carReducer, });
import { combineReducers } from 'redux'; const reducer = combineReducers({
counter: counterReducer, car: carReducer, });
Root Reducer Counter Reducer Car Reducer Engine Reducer Tire Reducer
… … …
Root Reducer Counter Reducer Car Reducer Engine Reducer Tire Reducer
… … …
Root Reducer Counter Reducer Car Reducer Engine Reducer Tire Reducer
… … …
Interact with APIs?
Middleware Enhance Redux applications
• Logging Middleware Enhance Redux applications
• Logging • Debugging Middleware Enhance Redux applications
• Logging • Debugging • API interaction Middleware Enhance Redux
applications
• Logging • Debugging • API interaction • Custom actions
Middleware Enhance Redux applications
Reducer View State Actions Middleware
Reducer View State Actions Middleware Intercept
const logMiddleware = api => next => action => {
console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
const logMiddleware = api => next => action => {
console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
const logMiddleware = api => next => action => {
console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
const logMiddleware = api => next => action => {
console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
const logMiddleware = api => next => action => {
console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
const logMiddleware = api => next => action => {
console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
const logMiddleware = api => next => action => {
console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
const logMiddleware = api => next => action => {
console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
const logMiddleware = api => next => action => {
console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
import { applyMiddleware } from 'redux'; const store = createStore(
reducer, applyMiddleware(logMiddleware) ); store.dispatch(increment()); store.dispatch(changeColor('green')); // dispatch { type: 'INCREMENT' } // state = { counter: 1, car: { color: 'red' } } // dispatch { type: 'CHANGE_COLOR', payload: 'green' } // state = { counter: 1, car: { color: 'green' } }
import { applyMiddleware } from 'redux'; const store = createStore(
reducer, applyMiddleware(logMiddleware) ); store.dispatch(increment()); store.dispatch(changeColor('green')); // dispatch { type: 'INCREMENT' } // state = { counter: 1, car: { color: 'red' } } // dispatch { type: 'CHANGE_COLOR', payload: 'green' } // state = { counter: 1, car: { color: 'green' } }
import { applyMiddleware } from 'redux'; const store = createStore(
reducer, applyMiddleware(logMiddleware) ); store.dispatch(increment()); store.dispatch(changeColor('green')); // dispatch { type: 'INCREMENT' } // state = { counter: 1, car: { color: 'red' } } // dispatch { type: 'CHANGE_COLOR', payload: 'green' } // state = { counter: 1, car: { color: 'green' } }
import { applyMiddleware } from 'redux'; const store = createStore(
reducer, applyMiddleware(logMiddleware) ); store.dispatch(increment()); store.dispatch(changeColor('green')); // dispatch { type: 'INCREMENT' } // state = { counter: 1, car: { color: 'red' } } // dispatch { type: 'CHANGE_COLOR', payload: 'green' } // state = { counter: 1, car: { color: 'green' } }
import { applyMiddleware } from 'redux'; const store = createStore(
reducer, applyMiddleware(logMiddleware) ); store.dispatch(increment()); store.dispatch(changeColor('green')); // dispatch { type: 'INCREMENT' } // state = { counter: 1, car: { color: 'red' } } // dispatch { type: 'CHANGE_COLOR', payload: 'green' } // state = { counter: 1, car: { color: 'green' } }
const initialState = { status: 'READY', user: null, };
function reducer(state = initialState, action) { switch (action.type) { case
'REQUEST_USER': return { ...state, status: 'FETCHING' }; case 'RECEIVE_USER': return { ...state, status: 'SUCCESS', user: action.payload, }; default: return state; } }
function reducer(state = initialState, action) { switch (action.type) { case
'REQUEST_USER': return { ...state, status: 'FETCHING' }; case 'RECEIVE_USER': return { ...state, status: 'SUCCESS', user: action.payload, }; default: return state; } }
function reducer(state = initialState, action) { switch (action.type) { case
'REQUEST_USER': return { ...state, status: 'FETCHING' }; case 'RECEIVE_USER': return { ...state, status: 'SUCCESS', user: action.payload, }; default: return state; } }
const requestUser = id => ({ type: 'REQUEST_USER', payload: id,
}); const receiveUser = user => ({ type: 'RECEIVE_USER', payload: user, });
const store = createStore(reducer); function fetchUser(id) { store.dispatch(requestUser(id)); axios.get(`/users/${id}`) .then(({
data: user }) => { store.dispatch(receiveUser(user)); }); } fetchUser(1);
const store = createStore(reducer); function fetchUser(id) { store.dispatch(requestUser(id)); axios.get(`/users/${id}`) .then(({
data: user }) => { store.dispatch(receiveUser(user)); }); } fetchUser(1);
const store = createStore(reducer); function fetchUser(id) { store.dispatch(requestUser(id)); axios.get(`/users/${id}`) .then(({
data: user }) => { store.dispatch(receiveUser(user)); }); } fetchUser(1);
const thunkMiddleware = api => next => action => {
if (typeof action === 'function') { return action(api.dispatch); } return next(action); }; const store = createStore( reducer, applyMiddleware(thunkMiddleware) );
const thunkMiddleware = api => next => action => {
if (typeof action === 'function') { return action(api.dispatch); } return next(action); }; const store = createStore( reducer, applyMiddleware(thunkMiddleware) );
const thunkMiddleware = api => next => action => {
if (typeof action === 'function') { return action(api.dispatch); } return next(action); }; const store = createStore( reducer, applyMiddleware(thunkMiddleware) );
const thunkMiddleware = api => next => action => {
if (typeof action === 'function') { return action(api.dispatch); } return next(action); }; const store = createStore( reducer, applyMiddleware(thunkMiddleware) );
function fetchUser(id) { return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`)
.then(({ data: user }) => { dispatch(receiveUser(user)); }); }; } store.dispatch(fetchUser(1));
function fetchUser(id) { return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`)
.then(({ data: user }) => { dispatch(receiveUser(user)); }); }; } store.dispatch(fetchUser(1)); Action Creator
function fetchUser(id) { return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`)
.then(({ data: user }) => { dispatch(receiveUser(user)); }); }; } store.dispatch(fetchUser(1)); Action
function fetchUser(id) { return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`)
.then(({ data: user }) => { dispatch(receiveUser(user)); }); }; } store.dispatch(fetchUser(1));
Alternative Async Middleware
redux-saga.js.org
• Uses ES6 generator functions redux-saga.js.org
• Uses ES6 generator functions • Write asynchronous code in
synchronous manner redux-saga.js.org
• Uses ES6 generator functions • Write asynchronous code in
synchronous manner • Great for coordinating multiple API calls redux-saga.js.org
• Uses ES6 generator functions • Write asynchronous code in
synchronous manner • Great for coordinating multiple API calls • Great for forking background tasks redux-saga.js.org
redux-observable.js.org
redux-observable.js.org • Uses RxJS observables
redux-observable.js.org • Uses RxJS observables • Write asynchronous code with
declarative observable chains
redux-observable.js.org • Uses RxJS observables • Write asynchronous code with
declarative observable chains • Great for composing asynchronous operations
redux-observable.js.org • Uses RxJS observables • Write asynchronous code with
declarative observable chains • Great for composing asynchronous operations • Very concise code
And plenty more options…
• redux-logic And plenty more options…
• redux-logic • redux-ship And plenty more options…
• redux-logic • redux-ship • redux-promise And plenty more options…
• redux-logic • redux-ship • redux-promise • redux-api-middleware And plenty
more options…
× ✓ Testing
const state = { counter: 0, car: { color: 'red'
}, }; it('returns initial state', () => { expect(reducer(undefined, {})).toEqual(state); }); it('increments the number', () => { const subject = reducer(state, increment()).counter; expect(subject).toBe(1); }); it('changes the car color', () => { const subject = reducer(state, changeColor('green')).car.color; expect(subject).toBe('green'); }); Easy reducer unit tests!
it('creates an INCREMENT action', () => { expect(increment()).toEqual({ type: 'INCREMENT'
}); }); it('creates a CHANGE_COLOR action', () => { expect(changeColor('blue')).toEqual({ type: 'CHANGE_COLOR', payload: 'blue', }); }); You can test action creators, but not really necessary.
However, you should test async action creators. function fetchUser(id) {
return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`) .then(({ data: user }) => { dispatch(receiveUser(user)); }); }; }
import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a
user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); }); Unit test with test doubles
import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a
user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a
user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a
user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a
user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a
user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a
user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a
user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
Use integration tests to ensure all pieces work together. Allow
store, reducer, and actions to all interact .
it('fetches a user', () => { // Arrange const store
= createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act const promise = store.dispatch(fetchUser(1)); const subject = store.getState; // Assert expect(subject()).toEqual({ status: 'FETCHING', user: null }); return promise.then(() => { expect(subject()).toEqual({ status: 'SUCCESS', user: 'fake user' }); }); });
it('fetches a user', () => { // Arrange const store
= createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act const promise = store.dispatch(fetchUser(1)); const subject = store.getState; // Assert expect(subject()).toEqual({ status: 'FETCHING', user: null }); return promise.then(() => { expect(subject()).toEqual({ status: 'SUCCESS', user: 'fake user' }); }); });
it('fetches a user', () => { // Arrange const store
= createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act const promise = store.dispatch(fetchUser(1)); const subject = store.getState; // Assert expect(subject()).toEqual({ status: 'FETCHING', user: null }); return promise.then(() => { expect(subject()).toEqual({ status: 'SUCCESS', user: 'fake user' }); }); });
it('fetches a user', () => { // Arrange const store
= createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act const promise = store.dispatch(fetchUser(1)); const subject = store.getState; // Assert expect(subject()).toEqual({ status: 'FETCHING', user: null }); return promise.then(() => { expect(subject()).toEqual({ status: 'SUCCESS', user: 'fake user' }); }); });
it('fetches a user', () => { // Arrange const store
= createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act const promise = store.dispatch(fetchUser(1)); const subject = store.getState; // Assert expect(subject()).toEqual({ status: 'FETCHING', user: null }); return promise.then(() => { expect(subject()).toEqual({ status: 'SUCCESS', user: 'fake user' }); }); });
Resources • Redux • redux.js.org • egghead.io/courses/getting-started-with- redux • React
• github.com/reactjs/react-redux
Thanks! Slides: bit.ly/redux-connect Jeremy Fairbank @elpapapollo / jfairbank