A middleware is some code you can put between dispatching an action, and the moment it reaches the reducer
Asynchronous things like data fetching and impure functions like accessing the browser cache.
firing tracking events, setting up a subscription, and manually changing the DOM in React components are all examples of side effects.
class App extends Component {
constructor() {
super();
this.state = { data: [] };
}
componentDidMount() {
fetch(`https://api.coinmarketcap.com/v1/ticker/?limit=10`)
.then(res => res.json())
.then(json => this.props.requestSuccess(json));
.catch(e => {
switch (e.response.status) {
case 409: {}
default: {}
}
this.props.requestFailed(e)
})
}
render() {
return (<p>{this.props.data}</p>);
}
}
Based on some readings and my experience...
Use Thunk instead of Saga for simple and trivial tasks like:
Use Saga for
an iterator object for the function is returned instead. When the iterator's next() method is called, the generator function's body is executed until the first yield expression
function* anotherGenerator() {
const string = "some code"
const who = "world"
yield who
string.concat(" more code")
}
const gen = anotherGenerator().next()
const gretee = gen.next().value
`hello, ${gretee}`
=> hello, world
const gen = translationGenerator().next()
const translation = gen.next("lapiz")
translation.value
=> "stift"
function* translatorGenerator() {
const spanishWord = yield;
return dictionary[spanishWord];
}
export function* fetchData() {
try {
const data = yield axios.get('/user?ID=12345')
yield dispatch({type: "FETCH_SUCCEEDED", data})
} catch (error) {
yield dispatch({type: "FETCH_FAILED", error})
}
}
function thunkActionCreator(forPerson) {
return function(dispatch) {
return axios.get('/user?ID=12345').then(
(data) => dispatch({type: "FETCH_SUCCEEDED", data}),
(error) => dispatch({type: "FETCH_FAILED", error}),
);
};
}
function middleware(store) {
return next => action => {
const result = next(action)
const iter = saga(action)
process(iter)
return result
}
}
function process(iterator) {
function iterate(arg) {
const yielded = iterator.next(arg)
if (!yielded.done) {
digestEffect(result.value, iterate)
}
}
iterate()
}
function digestEffect(
effect,
nextCallback /* iterate() */
) {
if (is.simpleValue(effect)) {
nextCallback(effect)
}
else if (is.promise(effect)) {
effect.then(nextCallback)
}
}
takeLatest("EXAMPLE", ()=>{})
{
// ...
type: 'FORK',
payload: {
fn: ƒ takeLatest(patternOrChannel, worker),
args: ['EXAMPLE', () => {}]
}
};
call(getLoggedInDetails)
{
// ...
type: 'CALL',
payload: {
context: null,
fn: f getloginDetails(),
args: []
}
};
put(getLoggedInDetails())
{
// ...
type: 'PUT',
payload: {
action: {
type: 'app/GET_LOGGED_IN_DETAILS_IN_PROGRESS'
}
}
};
function digestEffect(
effect,
nextCallback /* iterate() */
) {
if (is.simpleValue(effect)) {
nextCallback(effect)
}
else if (is.promise(effect)) {
effect.then(nextCallback)
}
else if (is.effectObj(effect)) {
const effectRunner = effectRunnerMap[effect.type]
effectRunner(effect.payload, nextCallback)
} else if (is.iterator(effect)) {
// The process function from before
process(effect)
}
}
export function* fetchData() {
try {
const data = yield call(axios.get, '/user?ID=12345')
yield put({type: "FETCH_SUCCEEDED", data})
} catch (error) {
yield put({type: "FETCH_FAILED", error})
}
}
export function* fetchData(action) {
const data = yield call(callApi, action.payload.url)
}
const gen = fetchData()
assert.deepEqual(
gen.next().value,
{
CALL: {
fn: Api.fetch,
args: ['./products']
}
},
'it should call the fetch api'
);
export default function* rootSaga() {
yield fork(watchProductSaga)
yield fork(watchProductContentSaga)
yield fork(watchProductReviewsSaga)
yield fork(watchProductStockSaga)
yield fork(watchUserIdentityIdSaga)
yield fork(watchUserAddToBasketSaga)
yield fork(watchFactFinderSaga)
yield fork(watchLocationChange)
yield fork(watchBrandPageDetails)
yield fork(watchCampaignPageDetails)
yield fork(watchCategoryPageDetails)
yield fork(watchTvShowSaga)
yield fork(watchStockReminderSaga)
}
refers to a way of organizing the control flow using two separate Sagas
The watcher: will watch for dispatched actions and fork a worker on every action
The worker: will handle the action and terminate
function* watchFetchData() {
yield takeEvery(
'FETCH_REQUESTED',
fetchData
)
}
function* fetchData(action) {
try {
const data = yield axios.get('/user?ID=' + action.payload)
yield dispatch(requestSuccessfull(data))
} catch (error) {
yield dispatch(requestFailed(error))
}
}
A channel is an object used to send and receive messages between tasks. Messages from senders are queued until an interested receiver request a message, and registered receiver is queued until a message is available.
function sagaMiddleware() {
return function middleware(store) {
return next => action => {
const result = next(action)
channel.put(action)
const iter = saga(action)
process(iter)
return result
}
}
}
function process(iterator) {
function iterate(arg) {
const yielded = iterator.next(arg)
if (!yielded.done) {
digestEffect(result.value, iterate)
}
}
iterate()
}
fork, like call, can be used to invoke both normal and Generator functions. But, the calls are non-blocking, the middleware doesn't suspend the Generator while waiting for the result of fn. Instead as soon as fn is invoked, the Generator resumes immediately.
function runForkEffect(fn, ...args, cb) {
const taskIterator = createTaskIterator(fn, args)
const child = process(taskIterator)
cb(child)
}
Creates an Effect description that instructs the middleware to wait for a specified action on the Store. The Generator is suspended until an action that matches pattern is dispatched.
The result of yield take(pattern) is an action object being dispatched.
function runTakeEffect(channel, pattern, cb) {
try {
channel.take(cb, pattern)
} catch (err) {
cb(err, true)
return
}
}
We extract api calls into their own self-contained sagas
Handle Logic by composing a lot of different smaller sagas.
export function* loginAndRedirectUser(action) {
const {
username, password, url
} = action.payload;
yield put(
loginUser(username, password)
);
yield take(
actionTypes.REQUEST_SUCCESS
);
yield put(getLoggedInDetails());
updateLoginState();
yield url.redirect()
}
describe(sagaWatcher, () => {
it('listens the EXAMPLE ActionType', () => {
const iterator = sagaWatcher();
const actualYield = iterator.next().value;
expect(actualYield.type).toEqual('FORK');
expect(actualYield.payload.args).toEqual(
expect.arrayContaining(['EXAMPLE_ACTION_TYPE', saga])
);
});
});
describe(saga, () => {
const iterator = loginAndRedirectUser({type: "EXAMPLE"});
const effects = effectsFrom(iterator);
it('logs in the user', async () => {
expect(effects).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'PUT',
payload: expect.objectContaining({
action: expect.objectContaining({
type: "ANOTHER_ACTION_TYPE"
})
})
})
])
);
});
});
Let's wait for the first thing to happen of the following list
export function* trackLoginResult(loginMethod) {
const winner = yield race({
success: take(LOGIN_USER_REQUEST_SUCCESS),
error: take(LOGIN_USER_REQUEST_FAILED)
});
if (winner.success) {
const loginSuccess = {
loginMethod,
...winner.success.payload
};
yield put(trackLoginSuccess(loginSuccess));
} else {
const loginFailedReason = winner.error.reason;
const loginFailedDetailed = {
loginFailedReason,
loginMethod
};
yield put(trackLoginFailure(loginFailedDetailed));
}
}
Basically, Queues tracking events to be dispatched only after a pageImpression is tracked.
export function* trackingQueue() {
yield take(LOCATION_CHANGE);
while (true) {
const chn = yield actionChannel(
isTrackingAction
);
const impression = yield take(
TRACK_PAGE_IMPRESSION
);
yield put(writeToDL(impression));
while (true) {
const winner = yield race({
locationChanged: take(
LOCATION_CHANGE
),
trackingAction: take(chn)
});
if (winner.trackingAction) {
yield put(writeToDL(
winner.trackingAction
));
} else {
break;
}
}
}
}
Should our classes know about saga effects?
export class RedirectUrl {
//...
redirect(): CallEffect | PutEffect {
if (this.isAbsolute()) {
return call(redirectTo, this.value);
} else {
return put(push(this.value));
}
}