Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up for free
JazzCon 2017: State, Side Effects, and Redux. Oh my!
Jeremy Fairbank
March 23, 2017
Programming
0
110
JazzCon 2017: State, Side Effects, and Redux. Oh my!
Jeremy Fairbank
March 23, 2017
Tweet
Share
More Decks by Jeremy Fairbank
See All by Jeremy Fairbank
Connect.Tech 2020: Advanced Cypress Testing
jfairbank
1
120
CodeMash 2020: Solving the Boolean Identity Crisis
jfairbank
1
110
CodeMash 2020: Practical Functional Programming
jfairbank
1
280
Connect.Tech 2019: Practical Functional Programming
jfairbank
0
220
Connect.Tech 2019: Solving the Boolean Identity Crisis
jfairbank
0
140
Lambda Squared 2019: Solving the Boolean Identity Crisis
jfairbank
0
42
All Things Open 2018: Practical Functional Programming
jfairbank
2
230
Connect.Tech 2018: Effective React Testing
jfairbank
1
110
Fluent Conf 2018: Building web apps with Elm Tutorial
jfairbank
2
710
Other Decks in Programming
See All in Programming
Showkase、Paparazziを用いたビジュアルリグレッションテストの導入にチャレンジした話 / MoT TechTalk #15
mot_techtalk
0
130
Unity+C#で学ぶ! メモリレイアウトとvtableのすゝめ 〜動的ポリモーフィズムを実現する仕組み〜
rossam
1
350
監視せなあかんし、五大紙だけにオオカミってな🐺🐺🐺🐺🐺
sadnessojisan
2
1.6k
SwiftPMのPlugin入門 / introduction_to_swiftpm_plugin
uhooi
2
110
ポケモンで学ぶiOS 16弾丸ツアー 🚅
giginet
PRO
1
620
Swift Expression Macros: a practical introduction
kishikawakatsumi
2
740
Circuit⚡
monaapk
0
200
Hatena Engineer Seminar #23「新卒研修で気軽に『ありがとう』を伝え合える Slack アプリを開発した話」
slashnephy
0
380
Hono v3 - Do Everything, Run Anywhere, But Small, And Faster
yusukebe
4
140
まだ日本国内で利用できないAppActionsにトライしてみた / MoT TechTalk #15
mot_techtalk
0
140
Enumを自動で網羅的にテストしてみた
estie
0
1.3k
CDKでValidationする本当の方法 / cdk-validation
gotok365
1
230
Featured
See All Featured
Building Flexible Design Systems
yeseniaperezcruz
314
35k
BBQ
matthewcrist
75
8.1k
The Invisible Side of Design
smashingmag
292
48k
Into the Great Unknown - MozCon
thekraken
2
300
StorybookのUI Testing Handbookを読んだ
zakiyama
8
3.2k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
152
13k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
351
21k
A Modern Web Designer's Workflow
chriscoyier
689
180k
10 Git Anti Patterns You Should be Aware of
lemiorhan
643
54k
Adopting Sorbet at Scale
ufuk
65
7.8k
Designing Experiences People Love
moore
130
22k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
10
1.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