-
A Redux middleware for handling side effects (asynchronous and impure things)
-
Being a middleware means it has access to the full application state and can dispatch redux actions
-
Uses generator functions as opposed to using
async/await
since testing generators is easier -
Similar to
redux-thunk
, but eliminates getting stuck in callback hell as well as being simpler to test
-
Very similar to the Python term generator
-
Halt execution of a function for an indefinite period of time when the
yield
keyword is used inside of afunction*
-
Every time execution stops you can return, "yield", a value back to the function to continue execution with
-
Provides essentially the same functionality as ES7
async/await
keywords (which are built on top of generators), but slightly more complex
Take code we are used to writing like:
User.findbyId(id)
.then(user => {
user.getFavoriteIceCreams()
.then(iceCreams => {
console.log(iceCreams);
});
});
In a generator function it would look like:
let user = yield User.findbyId(id);
let iceCreams = yield user.getFavoriteIceCreams();
console.log(iceCreams);
As you can see this makes asynchronous code look synchronous and is much easier to read
function* generatorExample() {
console.log('start here');
let a = yield 'first yield';
console.log(a);
let b = yield 'second yield';
console.log(b);
return 'all finished';
}
let iterator = generatorExample();
let next = iterator.next();
console.log(next);
next = iterator.next('I am now the a variable');
console.log(next);
next = iterator.next('I am now the b variable');
console.log(next);
@[1,8-9]
@[1,8-10]
@[1-2,8-10](start here
)
@[1-3,8-10]
@[1-3,8-11]({value: "first yield", done: false}
)
@[1-3,8-12]
@[1-4,8-12](I am now the a variable
)
@[1-5,8-12]
@[1-5,8-13]({value: "second yield", done: false}
)
@[1-5,8-14]
@[1-6,8-14](I am now the b variable
)
@[1-14]
@[1-15]({value: "all finished", done: true}
)
Saga Helpers / Effects Example (fetchSaga.js)
import { call, put, takeEvery } from 'redux-saga/effects'
export function* fetchData(action) {
try {
const data = yield call(Api.fetchUser, action.url)
yield put({type: 'SUCCEEDED', data})
} catch (error) {
yield put({type: 'FAILED', error})
}
}
export function* watchFetchData() {
yield takeEvery('REQUESTED', fetchData)
}
@[10-12](function to listen for every 'FETCH_REQUESTED' action dispatched)
@[11](console.log of the effect:
{ '@@redux-saga/IO': true,
FORK: { context: null, fn: [Function: takeEvery], args: [ 'REQUESTED', [Function: fetchData] ] }})
@[2-9]
@[4](console.log of the effect:
{ '@@redux-saga/IO': true,
CALL: { context: null, fn: [Function: Api.fetchUser], args: [action.url] }})
@[5](console.log of the effect:
{ '@@redux-saga/IO': true,
PUT: { channel: null, action: { type: 'SUCCEEDED', data: [1] }}})
@[6-8](If an error occurred perform a different action)
@[7](console.log of the effect:
{ '@@redux-saga/IO': true,
PUT: { channel: null, action: { type: 'FAILED', error: 'error occurred' }}})
Unit Testing Sagas
import { call, put, takeEvery } from 'redux-saga/effects';
import { fetchData, watchFetchData } from '../fetchSaga';
const data = [1];
it('fetchData Saga test', () => {
const generator = fetchData();
let next = generator.next();
expect(next.value).toEqual(call(Api.fetchUser, action.url));
next = generator.next(data);
expect(next.value).toEqual(put({type: 'SUCCEEDED', data}));
});
it('watchFetchData Saga test', () => {
const generator = watchFetchData();
let next = generator.next();
expect(next.value).toEqual(takeEvery('REQUESTED', fetchData));
});
@[4-10]
@[11-15]
You've seen something like the following in a react component to get data before your app loads:
function fetchData(url){
let { dispatch } = this.props;
return fetch(url)
.then(response => response.json())
.then(items => dispatch(actions.receiveItems(items)))
.catch(() => dispatch(actions.fetchFailure()));
}
This is bad!
- Components shouldn't include logic to fetch data
- Not easy to test without significant mocking
-
Install redux-saga:
npm install redux-saga
-
Create a
sagas
dir with a new js file within it -
Move the fetch function into the new saga file (or a separate location then import it in)
-
Create a new generator function that will call
fetchData
then dispatch an action w/ the response -
Wire up the redux store with the saga middleware
-
Remove the initial fetch call from the component
src/sagas/items.js
import { put, call } from 'redux-saga/effects';
import * as actions from '../actions/items';
function fetchData(url){
return fetch(url).then(response => response.json());
}
export function* getItems() {
try {
const items = yield call(fetchData, "http://item.fetch/api");
yield put(actions.receiveItems(items));
} catch {
yield put(actions.fetchFailure());
}
}
src/sagas/index.js
import { fork } from 'redux-saga/effects';
import { getItems } from './items'
export default function* root(){
yield fork(getItems);
}
Where your react-redux Provider
is, add the following:
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);