Skip to content

Example redux modules

Haz edited this page May 10, 2017 · 24 revisions

There're a bunch of redux modules already on the example app. Here's an explanation of each of them.

async is responsible for handling "pseudo-asynchronous" action creators. It listens to action creators with *_REQUEST type dispatched with meta.async property. Then, it returns a promise that will be fulfilled or rejected when the equivalent *_SUCCESS or *_FAILURE actions are dispatched.

Consider the following action creators:

export const resourceCreateRequest = data => ({
  type: 'RESOURCE_CREATE_REQUEST',
  payload: { data },
  meta: {
    async: true,
  },
})

export const resourceCreateSuccess = detail => ({
  type: 'RESOURCE_CREATE_SUCCESS',
  payload: detail,
})

When dispatching resourceCreateRequest, resource saga will take it and make the http request, dispatching resourceCreateSuccess when it's done. Since the request action had a meta.async property, the async middleware will handle it and return a promise from dispatch:

// with promise chain
store.dispatch(resourceCreateRequest({ 
  title: 'Hello, World!' 
})).then((detail) => {
  console.log('Yay! Resource was created!', detail)
})

// with async/await
const detail = await store.dispatch({
  title: 'Hello, World!',
})
console.log('Yay! Resource was created!', detail)

Using callbacks instead of promises

You can also provide the done callback function. In this case, the async middleware will not return a promise from dispatch. Let's change our action creator a bit:

- export const resourceCreateRequest = data => ({
+ export const resourceCreateRequest = (data, done) => ({
    type: 'RESOURCE_CREATE_REQUEST',
    payload: { data },
    meta: {
-     async: true,
+     async: { done },
    },
  })

Now we can pass a callback to the action creator:

store.dispatch(resourceCreateRequest('posts', {
  title: 'Hello, World!',
}, (err, detail) => {
  if (err) {
    return console.error(err)
  }
  return console.log('Yay! Resource was created!', detail)
}))

Solving race conflicts

The above approaches work great for most cases. But, if you need to handle multiple simultaneous actions with the same type, the first one that resolves will fulfill all the other actions. That happens because it has only the type (e.g. RESOURCE_CREATE_REQUEST) to match with the response action (e.g. RESOURCE_CREATE_SUCCESS).

To solve it, the async middleware also adds a meta.async.key property to the request action. If you need to use that, just grab it from the request action and pass to the response action.

Response action:

- export const resourceCreateSuccess = detail => ({
+ export const resourceCreateSuccess = (detail, key) => ({
    type: 'RESOURCE_CREATE_SUCCESS',
    payload: detail,
+   meta: {
+     async: { key },
+   },
  })

Saga:

export function* createResource(api, { data }, { async }) {
  try {
    const detail = yield call([api, api.post], '/resources', data)
    yield put(actions.resourceCreateSuccess(detail, async.key))
  } catch (e) {
    yield put(actions.resourceCreateFailure(e, async.key))
  }
}

export function* watchResourceCreateRequest(api) {
  while (true) {
    // grab payload and meta properties from the action
    const { payload, meta } = yield take(actions.RESOURCE_CREATE_REQUEST)
    // pass to the worker saga
    yield call(createResource, api, payload, meta)
  }
}
Clone this wiki locally