Skip to content

How to wait for another action in redux-saga

I have a few sagas that may finish and then put another action into the store.

Some sagas should only execute after others are executed: they must block, or wait until another one is finished.

Summarized as follows:

export function* authorize(action) {
  const { clientId } = action.data;

  const response = yield call(apiAuthorize, clientId);
  // Redux reducer picks this up and sets a token in storage. 
  yield put({ type: AUTHORIZE_SUCCEEDED, data: response.data.data });
}

export function* fetchMessages(action) {
  console.log(action);
  const { timelineId } = action.data;

  // how can we block this until either `token` is set (getToken returns non-null)
  //  or until AUTHORIZE_SUCCEEDED is sent?

  // The token set by AUTHORIZED_SUCCEEDED is read from the storage.
  // This will be null untill the AUTHORIZE_SUCCEEDED is handled by redux.
  // When null, the api-call will return a 401 so we want to block  untill we
  // have the token.
  const token = yield select(getToken);
  const response = yield call(apiFetchMessages, token);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
}

export default function* appSaga() {
  yield takeEvery(AUTHORIZE_REQUESTED, authorize);
  yield takeEvery(MESSAGES_REQUESTED, fetchMessages);
}

I’m trying to maintain as little coupling between sagas, so bonus points for showing me a way to achieve this outside of the functions .

Please note that is a simplified version. In reality there are several such fetchMessages that may be triggered all of which should wait until the AUTHORIZE_SUCCEEDED comes in.

I can add a loop in the fetchMessage() function, but that feels icky. I’m not very familiar with Javascript, Redux, Saga or generator functions, so maybe this feeling is entirely wrong. I’m also not confident how to run a loop with a timeout with sagas’ yield/select etc.

while (true) {
  const token = yield setTimeout(() => select(getToken), 1000);
  if (!!token) { 
    break;
  }
});

Another trick which works, but is cludgy, is to retry the fetchMessages api call on a 401.

try {
  const response = yield call(apiFetchMessages, token);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
} catch (error) {
  if (error.request.status === 401) {
    yield put({ type: MESSAGES_REQUESTED, data: { blockId } });
  } else {
    throw error;
  }
}

Is there an API or function for this in saga? Is this a proper pattern, or is my idea of blocking one action till another finishes wrong to begin with?

Answer

Starting with the more coupled, but simpler solution – instead of using delay to wait in the cycle, you can use the take effect to wait for the AUTHORIZE_SUCCEEDED action:

export function* fetchMessages(action) {
  const { timelineId } = action.data;

  // the cycle might not be needed if you are sure the 
  // AUTHORIZE_SUCCEEDED action is always dispatched with a valid token
  let token;
  while (true) {
     token = yield select(getToken);
     if (token) break;
     yield take(AUTHORIZE_SUCCEEDED);
  }

  const response = yield call(apiFetchMessages, token);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
}

To make this less clunky, you can abstract this away into its own saga:

export function* getTokenSaga() {
  let token;
  while (true) {
     token = yield select(getToken);
     if (token) break;
     yield take(AUTHORIZE_SUCCEEDED);
  }
  return token;
}

export function* fetchMessages(action) {
  const { timelineId } = action.data;

  const token = yield call(getTokenSaga);
  const response = yield call(apiFetchMessages, token);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
}

Another way to approach this is to wrap the fetching method:

export function* fetchWithToken(fetchFn, ...params) {
  let token;
  while (true) {
     token = yield select(getToken);
     if (token) break;
     yield take(AUTHORIZE_SUCCEEDED);
  }
  return yield call(fetchFn, token, ...params);
}

export function* fetchMessages(action) {
  const { timelineId } = action.data;

  const response = yield call(fetchWithToken, apiFetchMessages);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
}

Completely different way to possibly solve this would be to change the architecture of your app to make sure no fetching action like MESSAGES_REQUESTED can be dispatched, until you have the token – for example to show loading until you get the token and only then allow the rest of the application to request additional data.

In such a case you could then modify the fetch method itself to get the token, since it would be always available:

const loadData = (endpoint, payload) => {
  const token = getTokenSelector(store.getState())
  return fetch(endpoint, payload).then(...);
}

const apiFetchMessages = () => {
  return loadData('/messages');
}

export function* fetchMessages(action) {
  const { timelineId } = action.data;

  const response = yield call(apiFetchMessages);
  yield put({ type: MESSAGES_REQUEST_SUCCEEDED, data: response.data.data });
}

If such a change is not possible in the place where you dispatch the actions, there is one more way I can think of how to make sure the token is always available without modifying the fetchMessages saga itself and that is to instead buffer the other actions using actionChannel effect, until you have the token – this can get bit more complicated since you need to think about what to buffer when:

export default function* appSaga() {
  // we buffer all fetching actions
  const channel = yield actionChannel([MESSAGES_REQUESTED, FOO_REQUESTED]);

  // then we block the saga until AUTHORIZE_REQUESTED is dispatched and processed
  const action = yield take(AUTHORIZE_REQUESTED);
  yield call(authorize, action);

  // There is multiple ways to process the buffer, for example
  // we can simply redispatch the actions once we started
  // listening for them using the `takeEvery` effect
  yield takeEvery(MESSAGES_REQUESTED, fetchMessages);
  yield takeEvery(FOO_REQUESTED, fetchFoo);
  while (const action = yield take(channel)) {
    yield put(action);
  }
}