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
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
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
250
CodeMash 2020: Solving the Boolean Identity Crisis
jfairbank
1
210
CodeMash 2020: Practical Functional Programming
jfairbank
1
360
Connect.Tech 2019: Practical Functional Programming
jfairbank
0
420
Connect.Tech 2019: Solving the Boolean Identity Crisis
jfairbank
0
240
Lambda Squared 2019: Solving the Boolean Identity Crisis
jfairbank
0
180
All Things Open 2018: Practical Functional Programming
jfairbank
2
280
Connect.Tech 2018: Effective React Testing
jfairbank
1
210
Fluent Conf 2018: Building web apps with Elm Tutorial
jfairbank
2
930
Other Decks in Programming
See All in Programming
Datadog LLM Observabilityで実現する 安全なLLM Usage 管理
3150
0
120
Lessons from Spec-Driven Development
simas
PRO
0
220
Observability in Practice:Grafana 與 Edge Device SRE 的那些事
blueswen
0
180
Creating Composable Callables in Contemporary C++
rollbear
0
170
LLMによるContent Moderationの本番運用の裏側と品質担保への挑戦
suikabar
3
780
技術的負債解消で開発者の未来を開く- AIの力でコード刷新
kmd2kmd
0
120
Spring Security 実践 ─ GraphQL APIで実務に役立つ 認証・認可 を学ぶ
wagyu
0
260
Vite+ Unified Toolchain for the Web
naokihaba
0
360
Hunting Vulnerabilities in Symfony with LLMs
vinceamstoutz
0
560
Skillsは効率化、Agentsは"自分の拡張"——Builder時代のエージェント編成(CC Night 2026)
wemra
1
170
エージェンティックRAGにAWSで入門しよう!
har1101
9
1.8k
Dataformのリポジトリを立ち上げるときにまずやること / dataform-day0-2026
snhryt
0
190
Featured
See All Featured
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
350
Deep Space Network (abreviated)
tonyrice
0
210
How to build a perfect <img>
jonoalderson
1
5.7k
Bioeconomy Workshop: Dr. Julius Ecuru, Opportunities for a Bioeconomy in West Africa
akademiya2063
PRO
1
150
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
Optimising Largest Contentful Paint
csswizardry
37
3.7k
From Legacy to Launchpad: Building Startup-Ready Communities
dugsong
0
240
技術選定の審美眼(2025年版) / Understanding the Spiral of Technologies 2025 edition
twada
PRO
118
120k
Navigating the moral maze — ethical principles for Al-driven product design
skipperchong
2
400
[SF Ruby Conf 2025] Rails X
palkan
2
1.1k
Build your cross-platform service in a week with App Engine
jlugia
234
18k
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
508
140k
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