Skip to content
This repository was archived by the owner on Nov 6, 2018. It is now read-only.

Recipes

Grant Forrest edited this page Dec 11, 2017 · 1 revision

Table of Contents

Getting started with redux-saga and redux-facet

There are two scenarios where you may want to write code targeting facets in your sagas:

  1. Your saga emits some number of 'result' actions based on a trigger action, and you want those actions to have the same facet metadata as the original trigger action
  2. You want your saga to listen to only actions from a particular facet

1. "Mirroring" facet metadata to actions which are put from a saga

redux-facet includes a built-in tool for applying the same facet metadata to 'outgoing' actions from a saga as the one that triggered it. Read the docs for usage and an example.

2. Listening to a specific facet

To listen to actions which come from a specific facet, utilize redux-saga's function-as-pattern functionality.

const isProfileGetUserAction = action =>
  action.type === 'GET_USER' &&
  hasFacet('profile')(action);

const watchProfileGetUser = function*() {
  yield takeEvery(isProfileGetUserAction, getUserHandlerSaga);
}

Saga redirect after successful request

Redirecting to a new route after a successful asynchronous multi-action operation was one of the original guiding problems for redux-facet. To set up the problem, let's suppose our application enables a user to create and update Posts. When a user edits a post at /posts/:postId/edit and clicks "Save", they are redirected to /posts/:postId. With typical sagas, this is fairly simple (assuming we have react-router-redux ready):

const handlePostUpdateSaga = function*(action) {
  const response = yield call(api.updatePost, action.payload);
  yield put(postActions.update.complete(response));
  // redirect the user
  yield put(routerActions.push, `/posts/${action.payload.id}`);
}

const watchPostUpdate = function*() {
  yield takeLatest('POST_UPDATE', handlePostUpdateSaga);
}

This makes sense, since our router state is stored in Redux, and modifying our route should go through actions. The code is easy to read.

However, let's suppose we also want to add the ability for a user to quick-edit a post they've made as they're scrolling through their feed on /feed. Perhaps when they come across their own post, they can click a button and edit the post inline and save it without leaving the feed. The problem is, if we reuse our UPDATE_POST action, we will trigger our redirect logic, and the user will be pulled away from their feed and back to /posts/:postId when the update completes.

In traditional Redux, we would be forced to create a new action type, like FEED_UPDATE_POST, which does the exact same thing as UPDATE_POST, but avoids tripping the redirect logic.

With redux-saga, we can stick to DRY principles and reuse all our existing actions, sagas, and reducers. First, we apply a facet to both the edit post page, and the feed.

// containers/EditPost.js

export default compose(
  connect(mapStateToProps),
  facet('editPost', mapDispatchToProps),
)(EditPostForm);

// containers/Feed.js
export default compose(
  connect(mapStateToProps),
  facet('feed', mapDispatchToProps),
)(FeedView);

Now outgoing UPDATE_POST actions will have facet metadata for their source facet. We then apply facetSaga to our handlePostUpdateSaga and remove the redirect logic:

// only common logic for all post update operations goes here
const handlePostUpdateSaga = function*(action) {
  const response = yield call(api.updatePost, action.payload);
  yield put(postActions.update.complete(response));
}

const watchPostUpdate = function*() {
  yield takeLatest('POST_UPDATE', facetSaga(handlePostUpdateSaga));
}

Finally, we create a new saga to handle our redirect logic, listening only for the editPost facet, not the feed facet.

// sagas/updatePost/redirect.js

const handleEditPostUpdateComplete = function*(action) {
  yield put(routerActions.push, `/posts/${action.payload.id}`);
}

const isEditPostUpdateCompleteAction = action =>
  action.type === 'POST_UPDATE_COMPLETE' &&
  hasFacet('editPost')(action);

const watchEditPostUpdateComplete = function*() {
  yield takeLatest(isEditPostUpdateCompleteAction, handleEditPostUpdateComplete);
}

Our new saga will only trigger on the resulting POST_UPDATE_COMPLETE action which came from the editPost facet.

While this approach may seem to take a longer, more verbose code path than other solutions, it has some advantages:

  • Complete code reuse: actions, reducers and sagas for updating a post are reused, and an extra 'cluttering' action is not created for specific use cases.
  • Use-case-specific paths are separated from the main, generic code. This makes them easy to discover by other developers, easy to delete if no longer needed, or to update if product needs change without touching core shared logic.