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
190
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
360
Connect.Tech 2019: Practical Functional Programming
jfairbank
0
430
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
torikago - Ruby::Boxで照らすモジュラモノリスの実行境界
se4weed
1
360
React本体のコードリーディング
high_g_engineer
1
140
全PRの83%がAIレビューだけでマージできるようになった開発組織はその後どうなったか
athug
1
1.6k
Android CLI
fornewid
0
220
言葉の格闘技のススメ~紙とペンと言葉から始める、キャリアの描き方~
progresscicada
2
140
使いながら育てる Claude Code — 開発フローの1コマンド化 × 繰り返し指摘の自動仕組み化
shiki_kakaku
0
1.7k
自動化したのに回らない テスト運用の壁―AI時代の品質責任と生産性
mfunaki
0
150
Google Apps Script で Ruby を動かす
kawahara
0
160
Go 1.27 における memory allocation の高速化
andpad
0
160
Laravel Boostに学ぶ、AIにPHPを書かせる技術 〜OSSの実装から蒸留するエージェント制御の王道〜
kentaroutakeda
3
680
自動化したのに回らないテスト運用の壁ーAI時代の品質責任と生産性
mfunaki
0
130
ビデオ通話が繋がる0.2秒で何が起きているのか
supurazako
2
190
Featured
See All Featured
Collaborative Software Design: How to facilitate domain modelling decisions
baasie
1
270
The Straight Up "How To Draw Better" Workshop
denniskardys
239
140k
Leading Effective Engineering Teams in the AI Era
addyosmani
9
2.3k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Navigating Algorithm Shifts & AI Overviews - #SMXNext
aleyda
1
1.5k
Rebuilding a faster, lazier Slack
samanthasiow
85
9.6k
WENDY [Excerpt]
tessaabrams
11
39k
Unlocking the hidden potential of vector embeddings in international SEO
frankvandijk
0
890
Bioeconomy Workshop: Dr. Julius Ecuru, Opportunities for a Bioeconomy in West Africa
akademiya2063
PRO
1
210
The AI Revolution Will Not Be Monopolized: How open-source beats economies of scale, even for LLMs
inesmontani
PRO
3
3.7k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
360
30k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
47
8.3k
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