The document summarizes a presentation on Redux Sagas given at a React Native meetup in Munich in May 2017. It introduces generators and how they enable asynchronous logic to be written in a synchronous style. It provides examples of using generators to increment a value each time a new value is passed in and shows how generators can be used in a loop. The purpose of Redux Sagas is to manage asynchronous side effects in Redux applications using generator functions.
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. Roadmap:
• Are Sagas indispensable?
• Do they have a strong theoretical background?
• ES6 Generators
• Sagas
10. What we need
dispatch({type: ‘API_REQUEST’})
Reducer
Store
Middleware
dispatch({type: ‘API_REQUEST_SUCCESS’, data})
⚙
11. What we need
dispatch({type: ‘API_REQUEST’})
Reducer
state = {…state, data : action.data }
Store
Middleware
dispatch({type: ‘API_REQUEST_SUCCESS’, data})
⚙
12. What we need
dispatch({type: ‘API_REQUEST’})
Reducer
state = {…state, showError: true }
Store
Middleware
dispatch({type: ‘API_REQUEST_ERROR’})
⚙
13. What we need
dispatch({type: ‘API_REQUEST’})
Reducer
state = {…state, showError: true }
Store
Middleware
dispatch({type: ‘API_REQUEST_ERROR’})
⚙
Side effects
Pure code
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. 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. 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())
{}
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. 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 }
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. 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 }
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. 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 }
39. 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())
40. 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 }
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. 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
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))
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. 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 }
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. 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
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))
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. 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 }
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. 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
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. 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))
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. 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 }
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. 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
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))
60. 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))
61. 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 }
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))
63. 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))
64. 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 }
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: 1, done: false }
66. 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 }
67. 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 }
68. 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;
};
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)
})
70. 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)
})
71. 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> }
72. 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' }
73. 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 }
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)
}
//...
}
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
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
?
77. 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
78. 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)
80. 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)
81. 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?
83. Naive solution
class MoveButton extends Component {
render() {
return (
<Button
onPress={playSound(soundManager, ‘buzz’)}
/>
);
}
}
But where does soundManager come from?
85. 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 :_(
86. 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?
87. 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
88. 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…
89. Naive solution
What if we want to play a sound when the opponent moves too
and we receive her movements from a websocket?
90. 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))
}
//...
}
91. 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 :_(
92. Using sagas
import { take } from 'redux-saga/effects'
export default function* rootSaga() {
const action = yield take(Constants.PLAY_SOUND_REQUEST)
console.log(action.sound)
}
94. 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
99. 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
100. 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)
}
}
101. 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
110. takeLatest
import { takeLatest } from 'redux-saga/effects'
function* watchFetchData() {
yield takeLatest('FETCH_REQUESTED', fetchData)
}
Ensure that only the last fetchData will be running
111. 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
//...
}
112. 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
113. 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)
}
}
114. 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) }
}
}
115. 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) }
}
}
116. 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) }
}
}
117. 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
124. 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
125. 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