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
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
220
CodeMash 2020: Practical Functional Programming
jfairbank
1
370
Connect.Tech 2019: Practical Functional Programming
jfairbank
0
440
Connect.Tech 2019: Solving the Boolean Identity Crisis
jfairbank
0
250
Lambda Squared 2019: Solving the Boolean Identity Crisis
jfairbank
0
190
All Things Open 2018: Practical Functional Programming
jfairbank
2
290
Connect.Tech 2018: Effective React Testing
jfairbank
1
220
Fluent Conf 2018: Building web apps with Elm Tutorial
jfairbank
2
930
Other Decks in Programming
See All in Programming
Deep dive into the select statement (GopherCon UK)
jespino
0
160
まだ間に合う!今年の夏こそSchemeのマクロ展開器を完全理解!
omasanori
0
590
Gmail/Google DriveをトリガーにAIエージェントを動かそう! / Run AI agents with Gmail/Google Drive as triggers!
har1101
3
460
AI時代に学ぶ 好きなルール 嫌いなルール Linter編
shorty5121
0
690
AIに既存システムを理解させる技術 ~レガシーを見捨てないハーネスエンジニアリング入門~
ochtum
0
180
まずはプロンプトガイドを読もう、話はそれからだ
kiakiraki
1
250
源内ハンズオン概要編
hideg
0
230
MySQLとPostgreSQLって何が違うの?
akagami
0
150
信頼性の目標を誰も求めてない
shubox
0
460
バグを直したら useEffect が消えた
colorful12
3
780
PHPプロジェクトの結合バランスを可視化する #php_night
kajitack
0
180
「人を評価する AI」の設計と実装
ryoyanara
0
280
Featured
See All Featured
Ethics towards AI in product and experience design
skipperchong
2
360
Bioeconomy Workshop: Dr. Julius Ecuru, Opportunities for a Bioeconomy in West Africa
akademiya2063
PRO
1
320
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
Abbi's Birthday
coloredviolet
3
9.6k
Git: the NoSQL Database
bkeepers
PRO
432
67k
How To Speak Unicorn (iThemes Webinar)
marktimemedia
1
570
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
360
30k
State of Search Keynote: SEO is Dead Long Live SEO
ryanjones
0
260
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
194
17k
Leo the Paperboy
mayatellez
8
2.2k
The SEO identity crisis: Don't let AI make you average
varn
0
550
The AI Revolution Will Not Be Monopolized: How open-source beats economies of scale, even for LLMs
inesmontani
PRO
3
3.7k
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