1 of 18

redux-sagas

A short introduction

Sophie Koonin

@type__error

@type__error

2 of 18

@type__error

3 of 18

Without side effects

Controller

View

Display this picture

@type__error

4 of 18

With side effects

Controller

View

Wait, just let me go and get this picture…

API

OK, now display this picture

@type__error

5 of 18

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

6 of 18

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

7 of 18

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

8 of 18

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

9 of 18

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

10 of 18

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

11 of 18

Triggering sagas

function* watchForFetchDogsRequested() {

yield takeLatest('FETCH_DOGS_REQUESTED', fetchDogs)

}

@type__error

12 of 18

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

13 of 18

Function call… objects?

const res = yield call(api.fetchDogs)

// return value

{

CALL: {

context: null,

fn: [Function: fetchDogs],

args: []

}

}

@type__error

14 of 18

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

15 of 18

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

16 of 18

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

17 of 18

To wrap up…

  • Easy to test
  • Good for managing complicated data fetch flows
  • Keep your actions as plain objects
  • Keep business logic out of your display components

@type__error

18 of 18

Thank you!

@type__error