Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
React Native Munich Meetup
May 2017
REDUX SAGA
Nacho Martín
@nacmartin
Nacho Martin
I write code at Limenius.
We build tailor-made projects,
and provide consultancy
and formation.
We are very h...
Roadmap:
• Are Sagas indispensable?
• Do they have a strong theoretical background?
• ES6 Generators
• Sagas
Maybe you don’t need redux-saga
What we need
dispatch({type: ‘API_REQUEST’})
Reducer
What we need
dispatch({type: ‘API_REQUEST’})
Reducer
state = {…state, requesting : true }
Store
What we need
dispatch({type: ‘API_REQUEST’})
Reducer
Store
Middleware
⚙
What we need
dispatch({type: ‘API_REQUEST’})
Reducer
Store
Middleware
dispatch({type: ‘API_REQUEST_SUCCESS’, data})
⚙
What we need
dispatch({type: ‘API_REQUEST’})
Reducer
state = {…state, data : action.data }
Store
Middleware
dispatch({type...
What we need
dispatch({type: ‘API_REQUEST’})
Reducer
state = {…state, showError: true }
Store
Middleware
dispatch({type: ‘...
What we need
dispatch({type: ‘API_REQUEST’})
Reducer
state = {…state, showError: true }
Store
Middleware
dispatch({type: ‘...
Redux-thunk
function makeASandwichWithSecretSauce() {
return function (dispatch) {
dispatch({type: Constants.SANDWICH_REQU...
Redux-thunk
The lib has only 14 lines of code
If you don’t need more, stick with it
But what if you do?
If you need more
•Redux-saga
•Redux-observable
•…
•Maybe your own in the future?
Sagas: an abused word
Originally
Originally
Originally
Originally
But nowadays
A long and complicated story with many details
e.g. “my neighbor told me the saga of his divorce again”
Originally in C.S.
Héctor García-Molina
Originally in C.S.
Héctor García-Molina
( )
Originally in C.S.
Héctor García-Molina
( )
Originally in C.S.
Héctor García-Molina
( )
Multiple workflows, each providing
compensating actions for every step of
the w...
But nowadays
A process manager used to
orchestrate complex operations.
But nowadays
A process manager used to
orchestrate complex operations.
A library to manage side-effects.
And in redux-saga:
Generators
function*
First generator
function* myFirstGenerator() {
yield "one"
yield "two"
yield "three"
}
var it = myFirstGenerator()
console...
First generator
function* myFirstGenerator() {
yield "one"
yield "two"
yield "three"
}
var it = myFirstGenerator()
console...
First generator
function* myFirstGenerator() {
yield "one"
yield "two"
yield "three"
}
var it = myFirstGenerator()
console...
First generator
function* myFirstGenerator() {
yield "one"
yield "two"
yield "three"
}
var it = myFirstGenerator()
console...
First generator
function* myFirstGenerator() {
yield "one"
yield "two"
yield "three"
}
var it = myFirstGenerator()
console...
First generator
function* myFirstGenerator() {
yield "one"
yield "two"
yield "three"
}
var it = myFirstGenerator()
console...
First generator
function* myFirstGenerator() {
yield "one"
yield "two"
yield "three"
}
var it = myFirstGenerator()
console...
First generator
function* myFirstGenerator() {
yield "one"
yield "two"
yield "three"
}
var it = myFirstGenerator()
console...
First generator
function* myFirstGenerator() {
yield "one"
yield "two"
yield "three"
}
var it = myFirstGenerator()
console...
First generator
function* myFirstGenerator() {
yield "one"
yield "two"
yield "three"
}
var it = myFirstGenerator()
console...
First generator
function* myFirstGenerator() {
yield "one"
yield "two"
yield "three"
}
var it = myFirstGenerator()
console...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
x += (yield "1st " + x)
x += (yield "2nd " + x)
x += (yield "3rd " + x)
x += (yield "4th " + x...
function* sum() {
var x = 0
while(true) {
x += (yield x)
}
}
Similar, in a loop
var it = sum()
console.log(it.next(‘unused...
function* sum() {
var x = 0
while(true) {
x += (yield x)
}
}
Similar, in a loop
var it = sum()
console.log(it.next(‘unused...
function* sum() {
var x = 0
while(true) {
x += (yield x)
}
}
Similar, in a loop
var it = sum()
console.log(it.next(‘unused...
function* sum() {
var x = 0
while(true) {
x += (yield x)
}
}
Passing values to generators
var it = sum()
console.log(it.ne...
function* sum() {
var x = 0
while(true) {
x += (yield x)
}
}
Passing values to generators
var it = sum()
console.log(it.ne...
function* sum() {
var x = 0
while(true) {
x += (yield x)
}
}
Passing values to generators
var it = sum()
console.log(it.ne...
Making a iterable
for (let value of myIterable) {
console.log(value); // [1, 2, 3]
}
[...myIterable]; // [1, 2, 3]
var myI...
const fetchUser = () => new Promise(
resolve => {
setTimeout(() => resolve(
{
username: 'nacho',
hash: ‘12345'
}
), 4000)
...
const fetchUser = () => new Promise(
resolve => {
setTimeout(() => resolve(
{
username: 'nacho',
hash: ‘12345'
}
), 4000)
...
const fetchUser = () => new Promise(
resolve => {
setTimeout(() => resolve(
{
username: 'nacho',
hash: ‘12345'
}
), 4000)
...
With async code (+ promises)
const fetchUser = () => new Promise(
resolve => {
setTimeout(() => resolve(
{
username: 'nach...
With async code (+ promises)
const fetchUser = () => new Promise(
resolve => {
setTimeout(() => resolve(
{
username: 'nach...
With async code (+ promises)
function* apiCalls(username, password) {
var user = yield fetchUser(username)
var hash = yiel...
With async code (+ promises)
function* apiCalls(username, password) {
var user = yield fetchUser(username)
var hash = yiel...
With async code (+ promises)
function* apiCalls(username, password) {
var user = yield fetchUser(username)
var hash = yiel...
With async code (+ promises)
function* apiCalls(username, password) {
var user = yield fetchUser(username)
var hash = yiel...
With async code (+ promises)
function* apiCalls(username, password) {
var user = yield fetchUser(username)
var hash = yiel...
Sagas
Setup
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
// ...
import { h...
How to play a sound?
Imagine that we have a class SoundManager
that can load sounds, do a set up,
and has a method SoundMa...
Naive solution
class MoveButton extends Component {
render() {
return (
<Button
onPress={playSound(soundManager, ‘buzz’)}
...
Naive solution
class MoveButton extends Component {
render() {
return (
<Button
onPress={playSound(soundManager, ‘buzz’)}
...
Naive solution
class MoveButton extends Component {
render() {
return (
<Button
onPress={this.props.dispatch(playSound(‘bu...
Naive solution
class MoveButton extends Component {
render() {
return (
<Button
onPress={this.props.dispatch(playSound(‘bu...
Naive solution
class MoveButton extends Component {
render() {
return (
<Button
onPress={playSound(this.props.soundManager...
Naive solution
class MoveButton extends Component {
render() {
return (
<Button
onPress={playSound(this.props.soundManager...
Naive solution
class MoveButton extends Component {
render() {
return (
<Button
onPress={playSound(this.props.soundManager...
Naive solution
What if we want to play a sound when the opponent moves too
and we receive her movements from a websocket?
Naive solution
What if we want to play a sound when the opponent moves too
and we receive her movements from a websocket?
...
Naive solution
What if we want to play a sound when the opponent moves too
and we receive her movements from a websocket?
...
Using sagas
import { take } from 'redux-saga/effects'
export default function* rootSaga() {
const action = yield take(Cons...
Example: Play sound
import { take, call } from 'redux-saga/effects'
export default function* rootSaga(soundManager) {
cons...
Example: Play sound
import { take, call } from 'redux-saga/effects'
export default function* rootSaga(soundManager) {
cons...
Example: Play sound (call)
import { take, call } from 'redux-saga/effects'
export default function* rootSaga(soundManager)...
Example: Play sound (call)
import { take, call } from 'redux-saga/effects'
export default function* rootSaga(soundManager)...
Declarative effects
Effects are declarative, so they are easier to test
export function* delayAndDo() {
yield call(delay, 10...
Example: Play sound
import { take, call } from 'redux-saga/effects'
export default function* rootSaga(soundManager) {
cons...
Example: Play sound
import { take, call } from 'redux-saga/effects'
export default function* rootSaga(soundManager) {
cons...
Example: Play sound
import { take, call } from 'redux-saga/effects'
export default function* rootSaga(soundManager) {
whil...
Example: Play sound
import { take, call } from 'redux-saga/effects'
export default function* rootSaga(soundManager) {
whil...
Example: Play sound (takeEvery)
import { takeEvery, call } from 'redux-saga/effects'
function* playSound(soundManager, act...
Dispatching (put)
import { take, put } from 'redux-saga/effects'
function* watchLogin() {
while (true) {
const action = yi...
Dispatching (put)
import { take, put } from 'redux-saga/effects'
function* watchLogin() {
while (true) {
const action = yi...
Ajax example
function makeASandwichWithSecretSauce() {
return function (dispatch) {
dispatch({type: Constants.SANDWICH_REQ...
Ajax example
function makeASandwichWithSecretSauce() {
return function (dispatch) {
dispatch({type: Constants.SANDWICH_REQ...
Ajax example
import { takeEvery, put, call } from 'redux-saga/effects'
function* sandwichRequest() {
yield put({type:Const...
Ajax example
import { takeEvery, put, call } from 'redux-saga/effects'
function* sandwichRequest() {
yield put({type:Const...
takeLatest
import { takeLatest } from 'redux-saga/effects'
function* watchFetchData() {
yield takeLatest('FETCH_REQUESTED'...
takeLatest
import { takeLatest } from 'redux-saga/effects'
function* watchFetchData() {
yield takeLatest('FETCH_REQUESTED'...
Non-blocking (fork)
function* watchJoinGame(socket) {
let gameSaga = null;
while (true) {
let action = yield take(Constant...
Non-blocking (fork)
function* watchJoinGame(socket) {
let gameSaga = null;
while (true) {
let action = yield take(Constant...
Cancellation (cancel)
function* watchJoinGame(socket) {
let gameSaga = null;
while (true) {
let action = yield take(Consta...
Cancellation (cancel)
function* watchJoinGame(socket) {
let gameSaga = null;
while (true) {
let action = yield take(Consta...
Cancellation (cancel)
function* gameSaga(socket, gameId) {
try {
const result = yield call(joinChannel, socket, 'game:'+ga...
Cancellation (cancel)
function* gameSaga(socket, gameId) {
try {
const result = yield call(joinChannel, socket, 'game:'+ga...
Non-blocking detached (spawn)
A tasks waits for all its forks to terminate
Errors are bubbled up
Cancelling a tasks cancel...
Implement takeEvery from take
function* takeEvery(pattern, saga, ...args) {
const task = yield fork(function* () {
while (...
Parallel (all)
export default function* rootSaga() {
yield all([
playSounds(),
watchJoinGame(),
//…
])
}
Will block until ...
Races (race)
import { race, take } from 'redux-saga/effects'
function* aiCalculate() {
while (true) { ... }
}
function* wa...
Watch and fork pattern
https://medium.com/@pierremaoui/using-websockets-with-redux-sagas-a2bf26467cab
function* watchReque...
Sequentially, using channels
https://medium.com/@pierremaoui/using-websockets-with-redux-sagas-a2bf26467cab
import { take,...
Sequentially, using channels
https://medium.com/@pierremaoui/using-websockets-with-redux-sagas-a2bf26467cab
import { take,...
Connect + listen from socket
import { eventChannel, END } from 'redux-saga'
function websocketInitChannel() {
return event...
Obtaining the state (select)
import { select, takeEvery } from 'redux-saga/effects'
function* watchAndLog() {
yield takeEv...
Sequencing (delay)
import { put } from 'redux-saga/effects'
import { delay } from 'redux-saga'
function* scoreSequence(pay...
Summary
•Take (takeEvery, takeLast)
•Call
•Put
•Fork & Spawn
•Cancel
•Channels & EventChannels
•All & race
•Select
•Delay
Thanks! @nacmartin
nacho@limenius.com
http://limenius.com
Redux saga: managing your side effects. Also: generators in es6
Redux saga: managing your side effects. Also: generators in es6
Upcoming SlideShare
Loading in …5
×

Redux saga: managing your side effects. Also: generators in es6

2,998 views

Published on

Explanation of redux-saga for its use in React and React Native. Contains an explanation about ES6 generators, used in sagas, with emphasis in generators to manage async code.

Published in: Internet
  • Hey guys! Who wants to chat with me? More photos with me here 👉 http://www.bit.ly/katekoxx
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

Redux saga: managing your side effects. Also: generators in es6

  1. 1. React Native Munich Meetup May 2017 REDUX SAGA Nacho Martín @nacmartin
  2. 2. Nacho Martin I write code at Limenius. We build tailor-made projects, and provide consultancy and formation. We are very happy with React and React Native.
  3. 3. Roadmap: • Are Sagas indispensable? • Do they have a strong theoretical background? • ES6 Generators • Sagas
  4. 4. Maybe you don’t need redux-saga
  5. 5. What we need dispatch({type: ‘API_REQUEST’}) Reducer
  6. 6. What we need dispatch({type: ‘API_REQUEST’}) Reducer state = {…state, requesting : true } Store
  7. 7. What we need dispatch({type: ‘API_REQUEST’}) Reducer Store Middleware ⚙
  8. 8. What we need dispatch({type: ‘API_REQUEST’}) Reducer Store Middleware dispatch({type: ‘API_REQUEST_SUCCESS’, data}) ⚙
  9. 9. What we need dispatch({type: ‘API_REQUEST’}) Reducer state = {…state, data : action.data } Store Middleware dispatch({type: ‘API_REQUEST_SUCCESS’, data}) ⚙
  10. 10. What we need dispatch({type: ‘API_REQUEST’}) Reducer state = {…state, showError: true } Store Middleware dispatch({type: ‘API_REQUEST_ERROR’}) ⚙
  11. 11. What we need dispatch({type: ‘API_REQUEST’}) Reducer state = {…state, showError: true } Store Middleware dispatch({type: ‘API_REQUEST_ERROR’}) ⚙ Side effects Pure code
  12. 12. Redux-thunk function makeASandwichWithSecretSauce() { return function (dispatch) { dispatch({type: Constants.SANDWICH_REQUEST}) return fetch('https://www.google.com/search?q=secret+sauce') .then( sauce => dispatch({type: Constants.SANDWICH_SUCCESS, sauce}), error => dispatch({type: Constants.SANDWICH_ERROR, error}) ); }; } dispatch(makeASandwichWithSecretSauce())
  13. 13. Redux-thunk The lib has only 14 lines of code If you don’t need more, stick with it But what if you do?
  14. 14. If you need more •Redux-saga •Redux-observable •… •Maybe your own in the future?
  15. 15. Sagas: an abused word
  16. 16. Originally
  17. 17. Originally
  18. 18. Originally
  19. 19. Originally
  20. 20. But nowadays A long and complicated story with many details e.g. “my neighbor told me the saga of his divorce again”
  21. 21. Originally in C.S. Héctor García-Molina
  22. 22. Originally in C.S. Héctor García-Molina ( )
  23. 23. Originally in C.S. Héctor García-Molina ( )
  24. 24. Originally in C.S. Héctor García-Molina ( ) Multiple workflows, each providing compensating actions for every step of the workflow where it can fail
  25. 25. But nowadays A process manager used to orchestrate complex operations.
  26. 26. But nowadays A process manager used to orchestrate complex operations. A library to manage side-effects. And in redux-saga:
  27. 27. Generators function*
  28. 28. First generator function* myFirstGenerator() { yield "one" yield "two" yield "three" } var it = myFirstGenerator() console.log(it) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next())
  29. 29. First generator function* myFirstGenerator() { yield "one" yield "two" yield "three" } var it = myFirstGenerator() console.log(it) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next())
  30. 30. First generator function* myFirstGenerator() { yield "one" yield "two" yield "three" } var it = myFirstGenerator() console.log(it) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next()) {}
  31. 31. First generator function* myFirstGenerator() { yield "one" yield "two" yield "three" } var it = myFirstGenerator() console.log(it) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next())
  32. 32. First generator function* myFirstGenerator() { yield "one" yield "two" yield "three" } var it = myFirstGenerator() console.log(it) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next()) { value: 'one', done: false }
  33. 33. First generator function* myFirstGenerator() { yield "one" yield "two" yield "three" } var it = myFirstGenerator() console.log(it) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next())
  34. 34. First generator function* myFirstGenerator() { yield "one" yield "two" yield "three" } var it = myFirstGenerator() console.log(it) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next()) { value: 'two', done: false }
  35. 35. First generator function* myFirstGenerator() { yield "one" yield "two" yield "three" } var it = myFirstGenerator() console.log(it) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next())
  36. 36. First generator function* myFirstGenerator() { yield "one" yield "two" yield "three" } var it = myFirstGenerator() console.log(it) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next()) { value: 'three', done: false }
  37. 37. First generator function* myFirstGenerator() { yield "one" yield "two" yield "three" } var it = myFirstGenerator() console.log(it) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next())
  38. 38. First generator function* myFirstGenerator() { yield "one" yield "two" yield "three" } var it = myFirstGenerator() console.log(it) console.log(it.next()) console.log(it.next()) console.log(it.next()) console.log(it.next()) { value: undefined, done: true }
  39. 39. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  40. 40. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) heaven of data
  41. 41. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  42. 42. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  43. 43. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) { value: '1st 0’, done: false }
  44. 44. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  45. 45. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) x = 0 + 1
  46. 46. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  47. 47. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  48. 48. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) { value: '2nd 1’, done: false }
  49. 49. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  50. 50. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) x = 1 + 20
  51. 51. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  52. 52. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  53. 53. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  54. 54. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) { value: '3rd 21’, done: false }
  55. 55. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  56. 56. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) x = 21 + 300
  57. 57. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  58. 58. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  59. 59. function* sum() { var x = 0 x += (yield "1st " + x) x += (yield "2nd " + x) x += (yield "3rd " + x) x += (yield "4th " + x) } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) { value: '4th 321’, done: false }
  60. 60. function* sum() { var x = 0 while(true) { x += (yield x) } } Similar, in a loop var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  61. 61. function* sum() { var x = 0 while(true) { x += (yield x) } } Similar, in a loop var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300))
  62. 62. function* sum() { var x = 0 while(true) { x += (yield x) } } Similar, in a loop var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) { value: 0, done: false }
  63. 63. function* sum() { var x = 0 while(true) { x += (yield x) } } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) { value: 1, done: false }
  64. 64. function* sum() { var x = 0 while(true) { x += (yield x) } } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) { value:21, done: false }
  65. 65. function* sum() { var x = 0 while(true) { x += (yield x) } } Passing values to generators var it = sum() console.log(it.next(‘unused’)) console.log(it.next(1)) console.log(it.next(20)) console.log(it.next(300)) { value:321, done: false }
  66. 66. Making a iterable for (let value of myIterable) { console.log(value); // [1, 2, 3] } [...myIterable]; // [1, 2, 3] var myIterable = {}; myIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; };
  67. 67. const fetchUser = () => new Promise( resolve => { setTimeout(() => resolve( { username: 'nacho', hash: ‘12345' } ), 4000) }) With async code (+ promises) function* apiCalls(username, password) { var user = yield fetchUser(username) return user } var it = apiCalls() var promise = it.next().value console.log(promise) promise.then((result) => { console.log(result) var response = it.next(result) console.log(response) })
  68. 68. const fetchUser = () => new Promise( resolve => { setTimeout(() => resolve( { username: 'nacho', hash: ‘12345' } ), 4000) }) With async code (+ promises) function* apiCalls(username, password) { var user = yield fetchUser(username) return user } var it = apiCalls() var promise = it.next().value console.log(promise) promise.then((result) => { console.log(result) var response = it.next(result) console.log(response) })
  69. 69. const fetchUser = () => new Promise( resolve => { setTimeout(() => resolve( { username: 'nacho', hash: ‘12345' } ), 4000) }) With async code (+ promises) function* apiCalls(username, password) { var user = yield fetchUser(username) return user } var it = apiCalls() var promise = it.next().value console.log(promise) promise.then((result) => { console.log(result) var response = it.next(result) console.log(response) }) Promise { <pending> }
  70. 70. With async code (+ promises) const fetchUser = () => new Promise( resolve => { setTimeout(() => resolve( { username: 'nacho', hash: ‘12345' } ), 4000) }) function* apiCalls(username, password) { var user = yield fetchUser(username) return user } var it = apiCalls() var promise = it.next().value console.log(promise) promise.then((result) => { console.log(result) var response = it.next(result) console.log(response) }) { username: 'nacho', hash: '12345' }
  71. 71. With async code (+ promises) const fetchUser = () => new Promise( resolve => { setTimeout(() => resolve( { username: 'nacho', hash: ‘12345' } ), 4000) }) function* apiCalls(username, password) { var user = yield fetchUser(username) return user } var it = apiCalls() var promise = it.next().value console.log(promise) promise.then((result) => { console.log(result) var response = it.next(result) console.log(response) }) { value: { username: 'nacho', hash: '12345' }, done: true }
  72. 72. With async code (+ promises) function* apiCalls(username, password) { var user = yield fetchUser(username) var hash = yield someCrypto(password) if (user.hash == hash) { var hash = yield setSession(user.username) var posts = yield fetchPosts(user) } //... }
  73. 73. With async code (+ promises) function* apiCalls(username, password) { var user = yield fetchUser(username) var hash = yield someCrypto(password) if (user.hash == hash) { var hash = yield setSession(user.username) var posts = yield fetchPosts(user) } //... } We are doing async as if it was sync Easy to understand, dependent on our project
  74. 74. With async code (+ promises) function* apiCalls(username, password) { var user = yield fetchUser(username) var hash = yield someCrypto(password) if (user.hash == hash) { var hash = yield setSession(user.username) var posts = yield fetchPosts(user) } //... } We are doing async as if it was sync Easy to understand, dependent on our project ?
  75. 75. With async code (+ promises) function* apiCalls(username, password) { var user = yield fetchUser(username) var hash = yield someCrypto(password) if (user.hash == hash) { var hash = yield setSession(user.username) var posts = yield fetchPosts(user) } //... } We are doing async as if it was sync Easy to understand, dependent on our project ? More complex code but reusable between projects
  76. 76. With async code (+ promises) function* apiCalls(username, password) { var user = yield fetchUser(username) var hash = yield someCrypto(password) if (user.hash == hash) { var hash = yield setSession(user.username) var posts = yield fetchPosts(user) } //... } We are doing async as if it was sync Easy to understand, dependent on our project More complex code but reusable between projects redux-saga (or other libs)
  77. 77. Sagas
  78. 78. Setup import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' // ... import { helloSaga } from './sagas' const sagaMiddleware = createSagaMiddleware() const store = createStore( reducer, applyMiddleware(sagaMiddleware) ) sagaMiddleware.run(helloSaga)
  79. 79. How to play a sound? Imagine that we have a class SoundManager that can load sounds, do a set up, and has a method SoundManager.play(sound) How could we use it in React?
  80. 80. Naive solution class MoveButton extends Component { render() { return ( <Button onPress={playSound(soundManager, ‘buzz’)} /> ); } }
  81. 81. Naive solution class MoveButton extends Component { render() { return ( <Button onPress={playSound(soundManager, ‘buzz’)} /> ); } } But where does soundManager come from?
  82. 82. Naive solution class MoveButton extends Component { render() { return ( <Button onPress={this.props.dispatch(playSound(‘buzz’))} /> ); } }
  83. 83. Naive solution class MoveButton extends Component { render() { return ( <Button onPress={this.props.dispatch(playSound(‘buzz’))} /> ); } } Dispatch an action and we’ll see. But the action creator doesn’t have access to SoundManager :_(
  84. 84. Naive solution class MoveButton extends Component { render() { return ( <Button onPress={playSound(this.props.soundManager, ‘buzz’)} /> ); } } Passing it from its parent, and the parent of its parent with props?
  85. 85. Naive solution class MoveButton extends Component { render() { return ( <Button onPress={playSound(this.props.soundManager, ‘buzz’)} /> ); } } Passing it from its parent, and the parent of its parent with props? Hairball ahead
  86. 86. Naive solution class MoveButton extends Component { render() { return ( <Button onPress={playSound(this.props.soundManager, ‘buzz’)} /> ); } } From redux connect: • But soundManager is not serializable. • Breaks time-travel, persist and rehydrate store…
  87. 87. Naive solution What if we want to play a sound when the opponent moves too and we receive her movements from a websocket?
  88. 88. Naive solution What if we want to play a sound when the opponent moves too and we receive her movements from a websocket? class Game extends Component { componentDidMount() { this.props.dispatch(connectSocket(soundManager)) } //... }
  89. 89. Naive solution What if we want to play a sound when the opponent moves too and we receive her movements from a websocket? class Game extends Component { componentDidMount() { this.props.dispatch(connectSocket(soundManager)) } //... } What has to do connectSocket with soundManager? We are forced to do this because we don’t know anything better :_(
  90. 90. Using sagas import { take } from 'redux-saga/effects' export default function* rootSaga() { const action = yield take(Constants.PLAY_SOUND_REQUEST) console.log(action.sound) }
  91. 91. Example: Play sound import { take, call } from 'redux-saga/effects' export default function* rootSaga(soundManager) { const action = yield take(Constants.PLAY_SOUND_REQUEST) soundManager.play(action.sound) }
  92. 92. Example: Play sound import { take, call } from 'redux-saga/effects' export default function* rootSaga(soundManager) { const action = yield take(Constants.PLAY_SOUND_REQUEST) soundManager.play(action.sound) } But we will need a mock to test it
  93. 93. Example: Play sound (call) import { take, call } from 'redux-saga/effects' export default function* rootSaga(soundManager) { const action = yield take(Constants.PLAY_SOUND_REQUEST) yield call(soundManager.play, action.sound) }
  94. 94. Example: Play sound (call) import { take, call } from 'redux-saga/effects' export default function* rootSaga(soundManager) { const action = yield take(Constants.PLAY_SOUND_REQUEST) yield call(soundManager.play, action.sound) } { CALL: { fn: soundManager.play, args: ['buzz'] } } Call returns an object
  95. 95. Declarative effects Effects are declarative, so they are easier to test export function* delayAndDo() { yield call(delay, 1000) yield call(doSomething) } test('delayAndDo Saga test', (assert) => { const it = delayAndDo() assert.deepEqual( it.next().value, call(delay, 1000), 'delayAndDo Saga must call delay(1000)' ) assert.deepEqual( it.next().value, call(doSomething), 'delayAndDo Saga must call doSomething' ) { CALL: {fn: delay, args: [1000]}} { CALL: {fn: doSomething, args: []}}
  96. 96. Example: Play sound import { take, call } from 'redux-saga/effects' export default function* rootSaga(soundManager) { const action = yield take(Constants.PLAY_SOUND_REQUEST) yield call(soundManager.play, action.sound) }
  97. 97. Example: Play sound import { take, call } from 'redux-saga/effects' export default function* rootSaga(soundManager) { const action = yield take(Constants.PLAY_SOUND_REQUEST) yield call(soundManager.play, action.sound) } Will take 1 action, play a sound, and terminate
  98. 98. Example: Play sound import { take, call } from 'redux-saga/effects' export default function* rootSaga(soundManager) { while (true) { const action = yield take(Constants.PLAY_SOUND_REQUEST) yield call(soundManager.play, action.sound) } }
  99. 99. Example: Play sound import { take, call } from 'redux-saga/effects' export default function* rootSaga(soundManager) { while (true) { const action = yield take(Constants.PLAY_SOUND_REQUEST) yield call(soundManager.play, action.sound) } } Will take every action
  100. 100. Example: Play sound (takeEvery) import { takeEvery, call } from 'redux-saga/effects' function* playSound(soundManager, action) { yield call(soundManager.play, action.sound) } export default function* rootSaga(soundManager) { const action = yield takeEvery(Constants.PLAY_SOUND_REQUEST, playSound, soundManager) }
  101. 101. Dispatching (put) import { take, put } from 'redux-saga/effects' function* watchLogin() { while (true) { const action = yield take('USER_LOGIN_SUCCESS') yield put({type: 'FETCH_NEW_MESSAGES'}) } }
  102. 102. Dispatching (put) import { take, put } from 'redux-saga/effects' function* watchLogin() { while (true) { const action = yield take('USER_LOGIN_SUCCESS') yield put({type: 'FETCH_NEW_MESSAGES'}) } } put dispatches a new action
  103. 103. Ajax example function makeASandwichWithSecretSauce() { return function (dispatch) { dispatch({type: Constants.SANDWICH_REQUEST}) return fetch('https://www.google.com/search?q=secret+sauce') .then( sauce => dispatch({type: Constants.SANDWICH_SUCCESS, sauce}), error => dispatch({type: Constants.SANDWICH_ERROR, error}) ); }; } dispatch(makeASandwichWithSecretSauce())
  104. 104. Ajax example function makeASandwichWithSecretSauce() { return function (dispatch) { dispatch({type: Constants.SANDWICH_REQUEST}) return fetch('https://www.google.com/search?q=secret+sauce') .then( sauce => dispatch({type: Constants.SANDWICH_SUCCESS, sauce}), error => dispatch({type: Constants.SANDWICH_ERROR, error}) ); }; } dispatch(makeASandwichWithSecretSauce()) Thunk
  105. 105. Ajax example import { takeEvery, put, call } from 'redux-saga/effects' function* sandwichRequest() { yield put({type:Constants.SANDWICH_REQUEST_SHOW_LOADER}) try { const sauce = yield call(() => fetch('https://www.google.com/ search?q=secret+sauce')) yield put {type: Constants.SANDWICH_SUCCESS, sauce} } catch (error) { yield put {type: Constants.SANDWICH_ERROR, error} } } export default function* rootSaga() { const action = yield takeEvery(Constants.SANDWICH_REQUEST, sandwichRequest) } dispatch({type:Constants.SANDWICH_REQUEST})
  106. 106. Ajax example import { takeEvery, put, call } from 'redux-saga/effects' function* sandwichRequest() { yield put({type:Constants.SANDWICH_REQUEST_SHOW_LOADER}) try { const sauce = yield call(() => fetch('https://www.google.com/ search?q=secret+sauce')) yield put {type: Constants.SANDWICH_SUCCESS, sauce} } catch (error) { yield put {type: Constants.SANDWICH_ERROR, error} } } export default function* rootSaga() { const action = yield takeEvery(Constants.SANDWICH_REQUEST, sandwichRequest) } dispatch({type:Constants.SANDWICH_REQUEST}) Saga
  107. 107. takeLatest import { takeLatest } from 'redux-saga/effects' function* watchFetchData() { yield takeLatest('FETCH_REQUESTED', fetchData) }
  108. 108. takeLatest import { takeLatest } from 'redux-saga/effects' function* watchFetchData() { yield takeLatest('FETCH_REQUESTED', fetchData) } Ensure that only the last fetchData will be running
  109. 109. Non-blocking (fork) function* watchJoinGame(socket) { let gameSaga = null; while (true) { let action = yield take(Constants.JOIN_GAME) gameSaga = yield fork(game, socket, action.gameId) yield fork(watchLeaveGame, gameSaga) } } function* game(gameChannel, response) { yield fork(listenToSocket, gameChannel, 'game:move', processMovements) yield fork(listenToSocket, gameChannel, 'game:end', processEndGame) // More things that we want to do inside a game //... }
  110. 110. Non-blocking (fork) function* watchJoinGame(socket) { let gameSaga = null; while (true) { let action = yield take(Constants.JOIN_GAME) gameSaga = yield fork(game, socket, action.gameId) yield fork(watchLeaveGame, gameSaga) } } function* game(gameChannel, response) { yield fork(listenToSocket, gameChannel, 'game:move', processMovements) yield fork(listenToSocket, gameChannel, 'game:end', processEndGame) // More things that we want to do inside a game //... } Fork will create a new task without blocking in the caller
  111. 111. Cancellation (cancel) function* watchJoinGame(socket) { let gameSaga = null; while (true) { let action = yield take(Constants.JOIN_GAME) gameSaga = yield fork(game, socket, action.gameId) yield fork(watchLeaveGame, gameSaga) } }
  112. 112. Cancellation (cancel) function* watchJoinGame(socket) { let gameSaga = null; while (true) { let action = yield take(Constants.JOIN_GAME) gameSaga = yield fork(game, socket, action.gameId) yield fork(watchLeaveGame, gameSaga) } } function* watchLeaveGame(gameSaga) { while (true) { yield take(Constants.GAME_LEAVE) if (gameSaga) { yield cancel(gameSaga) } } }
  113. 113. Cancellation (cancel) function* gameSaga(socket, gameId) { try { const result = yield call(joinChannel, socket, 'game:'+gameId) // Call instead of fork so it blocks and we can cancel it yield call(gameSequence, result.channel, result.response) } catch (error) { console.log(error) } finally { if (yield cancelled()) { socket.channel('game:'+gameId, {}).leave() } } } function* watchJoinGame(socket) { let gameSaga = null; while (true) { let action = yield take(Constants.JOIN_GAME) gameSaga = yield fork(game, socket, action.gameId) yield fork(watchLeaveGame, gameSaga) } } function* watchLeaveGame(gameSaga) { while (true) { yield take(Constants.GAME_LEAVE) if (gameSaga) { yield cancel(gameSaga) } } }
  114. 114. Cancellation (cancel) function* gameSaga(socket, gameId) { try { const result = yield call(joinChannel, socket, 'game:'+gameId) // Call instead of fork so it blocks and we can cancel it yield call(gameSequence, result.channel, result.response) } catch (error) { console.log(error) } finally { if (yield cancelled()) { socket.channel('game:'+gameId, {}).leave() } } } function* watchJoinGame(socket) { let gameSaga = null; while (true) { let action = yield take(Constants.JOIN_GAME) gameSaga = yield fork(game, socket, action.gameId) yield fork(watchLeaveGame, gameSaga) } } function* watchLeaveGame(gameSaga) { while (true) { yield take(Constants.GAME_LEAVE) if (gameSaga) { yield cancel(gameSaga) } } }
  115. 115. Non-blocking detached (spawn) A tasks waits for all its forks to terminate Errors are bubbled up Cancelling a tasks cancels all its forks Fork Spawn New tasks are detached Errors don’t bubble up We have to cancel them manually
  116. 116. Implement takeEvery from take function* takeEvery(pattern, saga, ...args) { const task = yield fork(function* () { while (true) { const action = yield take(pattern) yield fork(saga, ...args.concat(action)) } }) return task }
  117. 117. Parallel (all) export default function* rootSaga() { yield all([ playSounds(), watchJoinGame(), //… ]) } Will block until all terminate
  118. 118. Races (race) import { race, take } from 'redux-saga/effects' function* aiCalculate() { while (true) { ... } } function* watchStartBackgroundTask() { while (true) { yield take('START_AI_COMPUTE_PLAYS') yield race({ task: call(aiCalculate), cancelAi: take('FORCE_MOVE_USER_IS_TIRED_OF_WAITING') }) } } In race, if one tasks terminates, the others are cancelled
  119. 119. Watch and fork pattern https://medium.com/@pierremaoui/using-websockets-with-redux-sagas-a2bf26467cab function* watchRequests() { while (true) { const { payload } = yield take('REQUEST') yield fork(handleRequest, payload) } }
  120. 120. Sequentially, using channels https://medium.com/@pierremaoui/using-websockets-with-redux-sagas-a2bf26467cab import { take, actionChannel, call, ... } from ‘redux-saga/effects' function* watchRequests() { const requestChan = yield actionChannel('REQUEST') while (true) { const { payload } = yield take(requestChan) yield call(handleRequest, payload) } }
  121. 121. Sequentially, using channels https://medium.com/@pierremaoui/using-websockets-with-redux-sagas-a2bf26467cab import { take, actionChannel, call, ... } from ‘redux-saga/effects' function* watchRequests() { const requestChan = yield actionChannel('REQUEST') while (true) { const { payload } = yield take(requestChan) yield call(handleRequest, payload) } } Channels act as buffers, buffering actions while we block
  122. 122. Connect + listen from socket import { eventChannel, END } from 'redux-saga' function websocketInitChannel() { return eventChannel( emitter => { const ws = new WebSocket() ws.onmessage = e => { return emitter( { type: 'ACTION_TYPE', payload } ) } // unsubscribe function return () => { ws.close() emitter(END) } }) } export default function* websocketSagas() { const channel = yield call(websocketInitChannel) while (true) { const action = yield take(channel) yield put(action) } } https://medium.com/@pierremaoui/using-websockets-with-redux-sagas-a2bf26467cab eventChannel turns the ws connection into a channel
  123. 123. Obtaining the state (select) import { select, takeEvery } from 'redux-saga/effects' function* watchAndLog() { yield takeEvery('*', function* logger(action) { const state = yield select() console.log('action', action) console.log('state after', state) }) } select() gives us the state after the reducers have applied the action It is better that sagas don’t to rely on the state, but it is still possible
  124. 124. Sequencing (delay) import { put } from 'redux-saga/effects' import { delay } from 'redux-saga' function* scoreSequence(payload) { yield put({ type: Constants.END_GAME }) yield call(delay, 2000) yield put({ type: Constants.SHOW_WINNER, rightAnswer: payload.winner }) yield call(delay, 2000) yield put({ type: Constants.GAME_UPDATE_SCORES, scores: payload.scores }) yield call(delay, 1000) yield put({ type: Constants.GAME_SHOW_PLAY_AGAIN }) }
  125. 125. Summary •Take (takeEvery, takeLast) •Call •Put •Fork & Spawn •Cancel •Channels & EventChannels •All & race •Select •Delay
  126. 126. Thanks! @nacmartin nacho@limenius.com http://limenius.com

×