Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Scenic City Summit 2017: Get Started with Redux
Search
Jeremy Fairbank
July 28, 2017
Programming
140
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Scenic City Summit 2017: Get Started with Redux
Jeremy Fairbank
July 28, 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
210
CodeMash 2020: Practical Functional Programming
jfairbank
1
360
Connect.Tech 2019: Practical Functional Programming
jfairbank
0
430
Connect.Tech 2019: Solving the Boolean Identity Crisis
jfairbank
0
240
Lambda Squared 2019: Solving the Boolean Identity Crisis
jfairbank
0
180
All Things Open 2018: Practical Functional Programming
jfairbank
2
280
Connect.Tech 2018: Effective React Testing
jfairbank
1
210
Fluent Conf 2018: Building web apps with Elm Tutorial
jfairbank
2
930
Other Decks in Programming
See All in Programming
Vue × Nuxt × Oxc どこまで使える?実運用の現在地
andpad
0
370
例外の正しい扱い方 そのエラー try-catchして大丈夫?
jinwatanabe
0
350
AI がコードを書く時代における新卒エンジニアの仕事風景 (2026) / New Graduate Engineers in the Era of AI Coding (2026)
sushichan044
0
210
エンジニアと一緒にテストコードの設計と実装を改善した話
mototakatsu
0
260
ローカルLLMでどこまでコードが書けるか -縮小版 / How much code can be written on a local LLM Shortened
kishida
2
180
SREの積み重ねがAI駆動開発のガードレールになった ― 7つの実践/SRE Guardrails The 7
tomoyakitaura
8
3.6k
「なぜそう決めたのか」を残し続ける仕組み ― Notion AI カスタムエージェント × Slack連携による設計判断の自動記録 - NIKKEI Tech Talk #47
niftycorp
PRO
0
260
エンジニア向け会社紹介/Findy Company Profile
findyinc
6
360k
Signal Forms: Details & Live Coding @enterJS 2026 in Mannheim
manfredsteyer
PRO
0
220
スマートグラスで並列バイブコーディング
hyshu
0
280
AI時代の仕事技芸論〜ソフトウェア開発で「遊ぶように働く」職人的熟達のすすめ(スクフェス仙台 2026バージョン)
kuranuki
0
550
20260623_Loop Engineeringで自分の分身の問い合わせBotを作る
ryugen04
0
190
Featured
See All Featured
Jess Joyce - The Pitfalls of Following Frameworks
techseoconnect
PRO
1
190
Building Better People: How to give real-time feedback that sticks.
wjessup
370
20k
GraphQLとの向き合い方2022年版
quramy
50
15k
svc-hook: hooking system calls on ARM64 by binary rewriting
retrage
2
330
How to train your dragon (web standard)
notwaldorf
97
6.7k
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
141
35k
How to Think Like a Performance Engineer
csswizardry
28
2.7k
How People are Using Generative and Agentic AI to Supercharge Their Products, Projects, Services and Value Streams Today
helenjbeal
1
230
Public Speaking Without Barfing On Your Shoes - THAT 2023
reverentgeek
1
460
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
12
1.7k
Amusing Abliteration
ianozsvald
1
220
Color Theory Basics | Prateek | Gurzu
gurzu
0
390
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, Vue, 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 = { users: [], isFetching: false, };
function reducer(state = initialState, action) { switch (action.type) { case
'REQUEST_USERS': return { ...state, isFetching: true }; case 'RECEIVE_USERS': return { ...state, isFetching: false, users: action.payload, }; default: return state; } }
function reducer(state = initialState, action) { switch (action.type) { case
'REQUEST_USERS': return { ...state, isFetching: true }; case 'RECEIVE_USERS': return { ...state, isFetching: false, users: action.payload, }; default: return state; } }
function reducer(state = initialState, action) { switch (action.type) { case
'REQUEST_USERS': return { ...state, isFetching: true }; case 'RECEIVE_USERS': return { ...state, isFetching: false, users: action.payload, }; default: return state; } }
const requestUsers = () => ({ type: 'REQUEST_USERS', }); const
receiveUsers = users => ({ type: 'RECEIVE_USERS', payload: users, });
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 fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')
.then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers());
function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')
.then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers()); Action Creator
function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')
.then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers()); Action
function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')
.then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers());
Alternatives for async code.
• Appropriate for more complex business logic and async flows.
Alternatives for async code.
• Appropriate for more complex business logic and async flows.
• Middleware-based like thunk middleware. Alternatives for async code.
• Appropriate for more complex business logic and async flows.
• Middleware-based like thunk middleware. • redux-saga Alternatives for async code.
• Appropriate for more complex business logic and async flows.
• Middleware-based like thunk middleware. • redux-saga • redux-observable Alternatives for async code.
• Appropriate for more complex business logic and async flows.
• Middleware-based like thunk middleware. • redux-saga • redux-observable • redux-logic Alternatives for async code.
× ✓ 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.
function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')
.then(({ data }) => { dispatch(receiveUsers(data)); }); }; } However, you should test async action creators.
import td from 'testdouble'; const axios = td.replace('axios'); it('fetches users',
() => { // Arrange const dispatch = td.function(); td.when(axios.get('/users')).thenResolve({ data: 'success' }); // Act return fetchUsers()(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUsers()); expect(subject.values[1]).toEqual(receiveUsers('success')); }); }); Unit test with test doubles
Use integration tests to ensure all pieces work together. Allow
store, reducer, and actions to all interact .
it('fetches users', () => { // Arrange const store =
createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users')).thenResolve({ data: 'success' }); // Act const promise = store.dispatch(fetchUsers()); const subject = store.getState; // Assert expect(subject()).toEqual({ isFetching: true, users: [] }); return promise.then(() => { expect(subject()).toEqual({ isFetching: false, users: 'success' }); }); });
it('fetches users', () => { // Arrange const store =
createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users')).thenResolve({ data: 'success' }); // Act const promise = store.dispatch(fetchUsers()); const subject = store.getState; // Assert expect(subject()).toEqual({ isFetching: true, users: [] }); return promise.then(() => { expect(subject()).toEqual({ isFetching: false, users: 'success' }); }); });
it('fetches users', () => { // Arrange const store =
createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users')).thenResolve({ data: 'success' }); // Act const promise = store.dispatch(fetchUsers()); const subject = store.getState; // Assert expect(subject()).toEqual({ isFetching: true, users: [] }); return promise.then(() => { expect(subject()).toEqual({ isFetching: false, users: 'success' }); }); });
it('fetches users', () => { // Arrange const store =
createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users')).thenResolve({ data: 'success' }); // Act const promise = store.dispatch(fetchUsers()); const subject = store.getState; // Assert expect(subject()).toEqual({ isFetching: true, users: [] }); return promise.then(() => { expect(subject()).toEqual({ isFetching: false, users: 'success' }); }); });
Resources • Redux • redux.js.org • egghead.io/courses/getting-started-with- redux • React
• github.com/reactjs/react-redux
Thanks! Slides: bit.ly/scs-redux Jeremy Fairbank @elpapapollo / jfairbank