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
JazzCon 2017: State, Side Effects, and Redux. O...
Search
Jeremy Fairbank
March 23, 2017
Programming
200
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
JazzCon 2017: State, Side Effects, and Redux. Oh my!
Jeremy Fairbank
March 23, 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
200
All Things Open 2018: Practical Functional Programming
jfairbank
2
290
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
マイコン向けの軽量Ruby「PicoRuby」で各種デバイスを制御するネイティブアプリの実現手法
bash0c7
0
460
AWSに止められる覚悟してますか?
morizo_1984
2
490
re:Inventに行く前に知っておきたい現地参加のノウハウ
nokomoro3
0
290
Heart of Swift Concurrency
koher
0
1.1k
[ハンズオン]AIへの指示だけで「五目並べ」を作ってみよう
satoshi256kbyte
1
310
一人だけ、Kiroが静止する日
hideg
0
140
The Rails Doctrine Decade
koic
2
440
カツオ、ご期待ください
suneo3476
0
130
標準パッケージに uuid が追加された 背景から見る Go らしい意思決定 / go_127_uuid_decision
convto
5
9.3k
そのリトライ、死んだコネクションを使い回していませんか ── GoのHTTPクライアントとHTTP/2を実プロダクト障害から学び直す
myus4a
0
350
Go × SIMDで高速化するベクトル検索 ~ルーフラインモデルでSIMDが効く境界を探れ! ~
po3rin
1
5.6k
Turning Architecture into Unit Tests in the AI Era (NSSpain XIV)
steliosf
PRO
1
120
Featured
See All Featured
Writing Fast Ruby
sferik
630
63k
Organizational Design Perspectives: An Ontology of Organizational Design Elements
kimpetersen
PRO
1
840
Chrome DevTools: State of the Union 2024 - Debugging React & Beyond
addyosmani
10
1.4k
How to Build an AI Search Optimization Roadmap - Criteria and Steps to Take #SEOIRL
aleyda
1
2.2k
Abbi's Birthday
coloredviolet
4
10k
Un-Boring Meetings
codingconduct
0
440
Impact Scores and Hybrid Strategies: The future of link building
tamaranovitovic
0
440
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
23k
It's Worth the Effort
3n
188
29k
技術選定の審美眼(2025年版) / Understanding the Spiral of Technologies 2025 edition
twada
PRO
120
120k
4 Signs Your Business is Dying
shpigford
187
23k
How to Think Like a Performance Engineer
csswizardry
28
2.8k
Transcript
State, Side Effects, and Redux Oh my! Jeremy Fairbank @elpapapollo
/ jfairbank
sigient.com
State
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
Disorganized Changes {...}
?
None
Redux Unidirectional Organized changes Read-only state
Reducer View State Actions
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 Repeat
Store Reducer State View
Store Reducer State View
Store Reducer State View
Store Reducer State View
Store Reducer State View Action
Store Reducer State View Action
Store Reducer State View
Store Reducer State View
Store Reducer State View
Store Reducer State View
let state = 0; incrementBtn.addEventListener('click', () => { state +=
1; }); decrementBtn.addEventListener('click', () => { state -= 1; });
let state = 0; incrementBtn.addEventListener('click', () => { state +=
1; }); decrementBtn.addEventListener('click', () => { state -= 1; });
let state = 0; incrementBtn.addEventListener('click', () => { state +=
1; }); decrementBtn.addEventListener('click', () => { state -= 1; });
let state = 0; state += 1; state += 1;
state -= 1; console.log(state); // 1
let state = 0; state += 1; state += 1;
state -= 1; console.log(state); // 1
let state = 0; state += 1; state += 1;
state -= 1; console.log(state); // 1 { type: 'INCREMENT' }; { type: 'INCREMENT' }; { type: 'DECREMENT' };
let state = 0; state += 1; state += 1;
state -= 1; console.log(state); // 1 { type: 'INCREMENT' }; { type: 'INCREMENT' }; { type: 'DECREMENT' }; Action
Reducer
let state = 0; state = reducer(state, { type: 'INCREMENT'
}); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
let state = 0; state = reducer(state, { type: 'INCREMENT'
}); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
let state = 0; state = reducer(state, { type: 'INCREMENT'
}); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
let state = 0; state = reducer(state, { type: 'INCREMENT'
}); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
let state = 0; state = reducer(state, { type: 'INCREMENT'
}); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1 Action
let state = 0; state = reducer(state, { type: 'INCREMENT'
}); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
let state = 0; state = reducer(state, { type: 'INCREMENT'
}); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
let state = 0; state = reducer(state, { type: 'INCREMENT'
}); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
let state = 0; state = reducer(state, { type: 'INCREMENT'
}); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
function reducer(state = 0, action) { switch (action.type) { case
'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
Store
import { createStore } from 'redux'; const store = createStore(reducer);
store.getState(); // 0
import { createStore } from 'redux'; const store = createStore(reducer);
store.getState(); // 0
import { createStore } from 'redux'; const store = createStore(reducer);
store.getState(); // 0
import { createStore } from 'redux'; const store = createStore(reducer);
store.getState(); // 0
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type:
'DECREMENT' }); store.getState(); // 1
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type:
'DECREMENT' }); store.getState(); // 1
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type:
'DECREMENT' }); store.getState(); // 1
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type:
'DECREMENT' }); store.getState(); // 1
store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch({ type: 'INCREMENT'
}); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'DECREMENT' }); // state = 1 // state = 2 // state = 1
store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch({ type: 'INCREMENT'
}); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'DECREMENT' }); // state = 1 // state = 2 // state = 1
store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch({ type: 'INCREMENT'
}); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'DECREMENT' }); // state = 1 // state = 2 // state = 1
store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch({ type: 'INCREMENT'
}); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'DECREMENT' }); // state = 1 // state = 2 // state = 1
Action Creators
const increment = () => ({ type: 'INCREMENT', }); const
decrement = () => ({ type: 'DECREMENT', });
const increment = () => ({ type: 'INCREMENT', }); const
decrement = () => ({ type: 'DECREMENT', });
const increment = () => ({ type: 'INCREMENT', }); const
decrement = () => ({ type: 'DECREMENT', });
const unsubscribe = store.subscribe(() => { console.log('state =', store.getState()); });
store.dispatch(increment()); store.dispatch(increment()); store.dispatch(decrement()); // state = 1 // state = 2 // state = 1
const unsubscribe = store.subscribe(() => { console.log('state =', store.getState()); });
store.dispatch(increment()); store.dispatch(increment()); store.dispatch(decrement()); // state = 1 // state = 2 // state = 1
import { bindActionCreators } from 'redux'; const actions = bindActionCreators(
{ increment, decrement }, store.dispatch ); actions.increment(); actions.increment(); actions.decrement(); // state = 1 // state = 2 // state = 1
import { bindActionCreators } from 'redux'; const actions = bindActionCreators(
{ increment, decrement }, store.dispatch ); actions.increment(); actions.increment(); actions.decrement(); // state = 1 // state = 2 // state = 1
import { bindActionCreators } from 'redux'; const actions = bindActionCreators(
{ increment, decrement }, store.dispatch ); actions.increment(); actions.increment(); actions.decrement(); // state = 1 // state = 2 // state = 1
import { bindActionCreators } from 'redux'; const actions = bindActionCreators(
{ increment, decrement }, store.dispatch ); actions.increment(); actions.increment(); actions.decrement(); // state = 1 // state = 2 // state = 1
import { bindActionCreators } from 'redux'; const actions = bindActionCreators(
{ increment, decrement }, store.dispatch ); actions.increment(); actions.increment(); actions.decrement(); // state = 1 // state = 2 // state = 1
incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement); store.subscribe(() => { counterElement.innerHTML = store.getState();
}); incrementBtn.click(); incrementBtn.click(); decrementBtn.click(); console.log(counterElement.innerHTML); // 1
incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement); store.subscribe(() => { counterElement.innerHTML = store.getState();
}); incrementBtn.click(); incrementBtn.click(); decrementBtn.click(); console.log(counterElement.innerHTML); // 1
incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement); store.subscribe(() => { counterElement.innerHTML = store.getState();
}); incrementBtn.click(); incrementBtn.click(); decrementBtn.click(); console.log(counterElement.innerHTML); // 1
incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement); store.subscribe(() => { counterElement.innerHTML = store.getState();
}); incrementBtn.click(); incrementBtn.click(); decrementBtn.click(); console.log(counterElement.innerHTML); // 1
incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement); store.subscribe(() => { counterElement.innerHTML = store.getState();
}); incrementBtn.click(); incrementBtn.click(); decrementBtn.click(); console.log(counterElement.innerHTML); // 1
Immutable Object State
const initialState = { counter: 0, car: { color: 'red',
}, };
const initialState = { counter: 0, car: { color: 'red',
}, };
const initialState = { counter: 0, car: { color: 'red',
}, };
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; } }
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, });
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' } }
Middleware
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' } }
Side Effects I/O Mutable State
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());
None
Reducer View State Actions Middleware Redux Saga
Reducer View State Actions Middleware Redux Saga
Reducer View State Actions Middleware Redux Saga
Reducer View State Actions Middleware Redux Saga
Effect Descriptors take('SOME_ACTION_TYPE') call(someAsyncFunction) put(someAction()) fork(someSagaFunction) Just values
Redux Saga Sagas
Redux Saga Sagas
Redux Saga Sagas
Redux Saga Sagas IO APIs Console DB call, apply
Redux Saga Sagas IO APIs Console DB call, apply call,
apply, fork, spawn, join, cancel
Redux Saga Sagas IO APIs Console DB call, apply call,
apply, fork, spawn, join, cancel Redux Store put, select, take
Generator Functions
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } const iterator = myGenerator(); iterator.next(); // { value: 'hello', done: false }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } const iterator = myGenerator(); iterator.next(); // { value: 'hello', done: false }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } const iterator = myGenerator(); iterator.next(); // { value: 'hello', done: false }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } const iterator = myGenerator(); iterator.next(); // { value: 'hello', done: false }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } const iterator = myGenerator(); iterator.next(); // { value: 'hello', done: false }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } iterator.next(); // { value: 'world', done: false }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } iterator.next(); // { value: 'world', done: false }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } iterator.next(21); // { value: 42, done: true }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } iterator.next(21); // { value: 42, done: true }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } iterator.next(21); // { value: 42, done: true }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } iterator.next(); // { value: undefined, done: true }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } iterator.next(); // { value: undefined, done: true }
function* myGenerator() { yield 'hello'; const value = yield 'world';
return value * 2; } iterator.next(); // { value: undefined, done: true }
import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()
{ yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()
{ yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()
{ yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()
{ yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()
{ yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()
{ yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()
{ yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()
{ yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()
{ yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
Saga Redux Saga yield take('REQUEST_USERS'); 1. Wait for action
Saga Redux Saga View Middleware yield take('REQUEST_USERS'); 2. Receive action
Saga Redux Saga yield call(axios.get, '/users'); 3. Call API
const response = yield call(axios.get, '/users'); Saga Redux Saga 4.
Receive response
Saga Redux Saga Reducer Redux Store yield put( receiveUsers(response.data) );
5. Dispatch (put) action
Set Up
import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware(); const store
= createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(fetchUsersSaga);
import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware(); const store
= createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(fetchUsersSaga);
import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware(); const store
= createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(fetchUsersSaga);
import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware(); const store
= createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(fetchUsersSaga);
import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware(); const store
= createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(fetchUsersSaga);
Why Sagas?
Business Logic Spread Out Component Thunk Service Service Component Reducer
Business Logic Spread Out Component Thunk Service Service Component ×
Reducer
Business Logic Consolidated Saga Saga Saga Saga Saga Saga ✓
Testing
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } const saga = fetchUsersSaga(); it('waits for REQUEST_USERS', () => { const actual = saga.next().value; const expected = take('REQUEST_USERS'); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } const saga = fetchUsersSaga(); it('waits for REQUEST_USERS', () => { const actual = saga.next().value; const expected = take('REQUEST_USERS'); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } const saga = fetchUsersSaga(); it('waits for REQUEST_USERS', () => { const actual = saga.next().value; const expected = take('REQUEST_USERS'); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } const saga = fetchUsersSaga(); it('waits for REQUEST_USERS', () => { const actual = saga.next().value; const expected = take('REQUEST_USERS'); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } const saga = fetchUsersSaga(); it('waits for REQUEST_USERS', () => { const actual = saga.next().value; const expected = take('REQUEST_USERS'); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } // ... it('fetches users', () => { const actual = saga.next().value; const expected = call(axios.get, '/users'); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } // ... it('fetches users', () => { const actual = saga.next().value; const expected = call(axios.get, '/users'); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } // ... it('fetches users', () => { const actual = saga.next().value; const expected = call(axios.get, '/users'); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } // ... it('fetches users', () => { const actual = saga.next().value; const expected = call(axios.get, '/users'); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
Error Handling
const initialState = { users: [], isFetching: false, error: null,
};
const initialState = { users: [], isFetching: false, error: null,
};
const failUsers = error => ({ type: 'FAIL_USERS', error, });
function reducer(state = initialState, action) { switch (action.type) { //
... case 'FAIL_USERS': return { ...state, error: action.error }; // ... } }
function reducer(state = initialState, action) { switch (action.type) { //
... case 'FAIL_USERS': return { ...state, error: action.error }; // ... } }
function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')
.then(({ data }) => { dispatch(receiveUsers(data)); }) .catch((e) => { dispatch(failUsers(e)); }); }; }
function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')
.then(({ data }) => { dispatch(receiveUsers(data)); }) .catch((e) => { dispatch(failUsers(e)); }); }; } Swallowed without catch
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); }
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,
'/users'); yield put(receiveUsers(response.data)); } No swallowed errors
function* fetchUsersSaga() { try { yield take('REQUEST_USERS'); const response =
yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } catch (e) { yield put(failUsers(e)); } }
function* fetchUsersSaga() { try { yield take('REQUEST_USERS'); const response =
yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } catch (e) { yield put(failUsers(e)); } }
function* fetchUsersSaga() { try { yield take('REQUEST_USERS'); const response =
yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } catch (e) { yield put(failUsers(e)); } }
function* fetchUsersSaga() { try { yield take('REQUEST_USERS'); const response =
yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } catch (e) { yield put(failUsers(e)); } }
Multiple Requests
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const responses = yield [
call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ]; const users = responses.map(resp => resp.data); yield put(receiveUsers(users)); }
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const responses = yield [
call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ]; const users = responses.map(resp => resp.data); yield put(receiveUsers(users)); }
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const responses = yield [
call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ]; const users = responses.map(resp => resp.data); yield put(receiveUsers(users)); }
function* fetchUsersSaga() { yield take('REQUEST_USERS'); const responses = yield [
call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ]; const users = responses.map(resp => resp.data); yield put(receiveUsers(users)); }
Saga yield [ call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ];
// ...
Saga yield [ call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ];
// ... 1 2 3
Saga yield [ call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ];
// ... 1 2 3
Saga yield [ call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ];
// ... 1 3
Saga yield [ call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ];
// ... 3
Saga Helpers
function* fetchUsersSaga() { const response = yield call(axios.get, '/users'); yield
put(receiveUsers(response.data)); } function* mainSaga() { while (true) { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); } }
function* fetchUsersSaga() { const response = yield call(axios.get, '/users'); yield
put(receiveUsers(response.data)); } function* mainSaga() { while (true) { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); } }
import { takeEvery } from 'redux-saga/effects'; function* fetchUsersSaga() { const
response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield takeEvery('REQUEST_USERS', fetchUsersSaga); }
import { takeEvery } from 'redux-saga/effects'; function* fetchUsersSaga() { const
response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield takeEvery('REQUEST_USERS', fetchUsersSaga); }
import { takeEvery } from 'redux-saga/effects'; function* fetchUsersSaga() { const
response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield takeEvery('REQUEST_USERS', fetchUsersSaga); }
import { takeEvery } from 'redux-saga/effects'; function* fetchUsersSaga() { const
response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield takeEvery('REQUEST_USERS', fetchUsersSaga); }
import { takeEvery } from 'redux-saga/effects'; function* fetchUsersSaga() { const
response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield takeEvery('REQUEST_USERS', fetchUsersSaga); }
mainSaga fetchUsersSaga yield takeEvery( 'REQUEST_USERS', fetchUsersSaga );
View Middleware store.dispatch( requestUsers() ); mainSaga fetchUsersSaga
mainSaga fetchUsersSaga fork
mainSaga fetchUsersSaga yield call( axios.get, '/users' );
mainSaga fetchUsersSaga const response = yield call( axios.get, '/users' );
mainSaga fetchUsersSaga yield put( receiveUsers( response.data ) ); Reducer Redux
Store
mainSaga fetchUsersSaga yield takeEvery( 'REQUEST_USERS', fetchUsersSaga ); And repeat…
Data Race Issues Use alternative takeLatest
Forking
function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield
call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout());
function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield
call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout());
function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield
call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout());
function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield
call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout());
function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield
call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout());
function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield
call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout()); Missed
function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield
call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout()); Missed Never reached
mainSaga yield take( 'REQUEST_USERS' );
View Middleware store.dispatch( requestUsers() ); mainSaga
mainSaga fetchUsersSaga call
mainSaga fetchUsersSaga call yield call( fetchUsersSaga );
mainSaga fetchUsersSaga call View Middleware store.dispatch( logout() ); ×
import { fork } from 'redux-saga/effects'; function* fetchUsersSaga() { yield
take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield fork(fetchUsersSaga); yield take('LOGOUT'); yield call(logoutSaga); }
import { fork } from 'redux-saga/effects'; function* fetchUsersSaga() { yield
take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield fork(fetchUsersSaga); yield take('LOGOUT'); yield call(logoutSaga); }
import { fork } from 'redux-saga/effects'; function* fetchUsersSaga() { yield
take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield fork(fetchUsersSaga); yield take('LOGOUT'); yield call(logoutSaga); } Nonblocking
import { fork } from 'redux-saga/effects'; function* fetchUsersSaga() { yield
take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield fork(fetchUsersSaga); yield take('LOGOUT'); yield call(logoutSaga); }
import { fork } from 'redux-saga/effects'; function* fetchUsersSaga() { yield
take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield fork(fetchUsersSaga); yield take('LOGOUT'); yield call(logoutSaga); } Won’t miss now
mainSaga
mainSaga fetchUsersSaga fork yield fork( fetchUsersSaga );
View Middleware store.dispatch( requestUsers() ); mainSaga fetchUsersSaga
mainSaga fetchUsersSaga yield call( axios.get, '/users' );
mainSaga fetchUsersSaga yield take( 'LOGOUT' );
mainSaga fetchUsersSaga View Middleware store.dispatch( logout() );
Miscellaneous Patterns
Miscellaneous Patterns • Autosaving background tasks
Miscellaneous Patterns • Autosaving background tasks • Races for timeouts
Miscellaneous Patterns • Autosaving background tasks • Races for timeouts
• Task cancellation
Miscellaneous Patterns • Autosaving background tasks • Races for timeouts
• Task cancellation • Throttling and debouncing
Miscellaneous Patterns • Autosaving background tasks • Races for timeouts
• Task cancellation • Throttling and debouncing • Hook up to other IO sources
Resources •Redux • redux.js.org • egghead.io/courses/getting-started-with-redux •React • github.com/reactjs/react-redux •Redux
Saga • redux-saga.github.io/redux-saga • Testing • github.com/jfairbank/redux-saga-test-plan
Thanks! Code: github.com/jfairbank/state-side-effects-and-redux Slides: bit.ly/jazzcon-redux Jeremy Fairbank @elpapapollo / jfairbank