redux-sagas
A short introduction
Sophie Koonin
@type__error
@type__error
@type__error
Without side effects
Controller
View
Display this picture
@type__error
With side effects
Controller
View
Wait, just let me go and get this picture…
API
OK, now display this picture
@type__error
Fetching data from inside a component
export default class DogList extends React.Component {
constructor(props) {
super(props)
this.state = {
dogs: []
}
}
componentDidMount() {
api.fetchDogs()
.then(response => this.setState({ dogs: response.body.dogs }))
}
...
}
@type__error
Fetching data with redux-thunk
Thunk: a function returned by another function in order to delay execution until it is needed
const fetchDogs = dispatch => api.fetchDogs()
.then(({dogs}) => dispatch(fetchDogsSucceeded(dogs)))
@type__error
Fetching data with redux-thunk
const fetchDogs = dispatch => {
dispatch(fetchDogsRequested())
api.fetchDogs()
.then(({dogs}) => Promise.all(dogs.map(dog => fetchFavouriteToy(dog)(dispatch))))
.then(dogs => dispatch(fetchDogsSucceeded(dogs)))
}
const fetchFavouriteToy = dog => dispatch => {
dispatch(fetchToysRequested(dog))
return api.fetchToy(dog).then(({toy}) => ({...dog, toy}))
}
@type__error
Testing thunks can get complicated…
jest.mock('../fetch-toys');
jest.mock('../fetch-dogs-succeeded')
jest.mock('../fetch-dogs', () => jest.fn(() => ({
ok: true,
body: {
dogs: [ 'Gandalf', 'Pepsi']
}
})));
test('calls fetchDogs API', () => {
const dispatch = jest.fn()
fetchDogs(dispatch)
expect(dispatch).toHaveBeenCalledWith(fetchDogs)
})
test('dispatches fetchFavouriteToysRequested with the fetched dogs if response is ok', () => {
const dispatch = jest.fn()
fetchDogs(dispatch)
expect(dispatch).toHaveBeenCalledWith(fetchFavouriteToy)
})
@type__error
Introducing redux-saga
“redux-saga is a library that aims to make application side effects ... easier to manage, more efficient to execute, simple to test, and better at handling failures.”
@type__error
Fetching data with sagas
function* getDog = () => {
try {
const res = yield call(api.fetchDogs)
const dogsWithToys = yield all(res.dogs.map(dog => fetchFavouriteToy(dog)))
yield put(fetchDogsSuccessful(dogsWithToys))
} catch (error) {
yield put(fetchDogsFailed(error))
}
}
function* fetchFavouriteToy({ dog }) {
try {
const response = yield call(api.fetchFavouriteToy, dog)
return {...dog, favouriteToy: response.toy}
} catch (error) {
yield put(fetchFavouriteToyFailed(error))
}
}
@type__error
Triggering sagas
function* watchForFetchDogsRequested() {
yield takeLatest('FETCH_DOGS_REQUESTED', fetchDogs)
}
@type__error
How sagas work
function* getDog = () => {
try {
const res = yield call(api.fetchDogs)
const dogsWithToys = yield all(res.dogs.map(dog => fetchFavouriteToy(dog)))
yield put(fetchDogsSuccessful(dogsWithToys))
} catch (error) {
yield put(fetchDogsFailed(error))
}
}
ES6 Generators
@type__error
Function call… objects?
const res = yield call(api.fetchDogs)
// return value
{
CALL: {
context: null,
fn: [Function: fetchDogs],
args: []
}
}
@type__error
Effects
const [
favouriteFood,
favouriteToy
] = yield all([
select(getFavouriteFood),
select(getFavouriteToy)
])
const { response, timeout } = yield race({
response: call(api.patDog, dogId),
timeout: delay(1000, ‘oh no’)
})
@type__error
Testing is easy with sagas!
// Sagas
import { call } from ‘redux-saga/effects’
test('calls fetchDogs API', () => {
const iterator = fetchDogs()
expect(iterator.next().value).toEqual(call(api.fetchDogs))
})
// Thunks
jest.mock('../fetch-dogs');
jest.mock('../fetch-toys');
test('calls fetchDogs API', () => {
const dispatch = jest.fn()
fetchDogs(dispatch)
expect(dispatch).toHaveBeenCalledWith(fetchDogs)
})
@type__error
Testing branched logic
test('dispatches fetchDogsFailed if the call fails', () => {
const iterator = fetchDogs()
iterator.next()
expect(iterator.throw(‘oh no’).value)
.toEqual(call(fetchDogsFailed(‘oh no’)))
})
@type__error
To wrap up…
@type__error
Thank you!
@type__error