Search code examples
javascriptreduxredux-saga

Redux sagas Fetch only once when same id is dispatched multiple times


I’m getting a user from my API and store it in my state so I don’t have to fetch it again. Problem is that multiple components requests the user at the same time resulting in multiple concurrent fetch requests.

Is there a good pattern to avoid this?

This is my saga

function* watchUserRequests() {
    yield takeEvery(actionTypes.USER_REQUESTED, userRequested);
}

function* userRequested(action) {
    const {id} = action.payload;
    let user = yield select(state => state.users.all[id]);
    // cancel if user exists      
    if (user) return;
    user = yield call(userApi.get, id);
    yield put(userActions.userLoaded(id, banner));
}

Actions

export function userRequested(id) {
    return {type: types.USER_REQUESTED, payload: {id}};
}

export function userLoaded(id, user) {
    return {type: types.USER_LOADED, payload: {id, user}};
}

Solution

  • This is how I solved this problem in another saga. The "type" here can be ignored

    • Accumulate the requested id in an ids object:
    • Dispatch action to fetch all accumulated ids
    • Debounce fetch for 50 milliseconds
    • While waiting... Add new Ids to the ids object
    • While waiting... Cancel fetch task to give you 50 new milliseconds
    • Commit fetch
    • Clear tasks and ids

    Code:

    let ids = {};
    let tasks = {};
    
    function* watchCounterRequests() {
      yield takeEvery(actionTypes.COUNTER_REQUESTED, accumulate);
    }
    
    function* watchCounterFetchRequests() {
      yield takeEvery(actionTypes.COUNTER_FETCH_REQUESTED, counterFetchRequested);
    }
    
    function* accumulate(action) {
      const {id, type} = action.payload;
      if (!ids[type]) ids[type] = {};
    
      ids[type][id] = true;
      if (tasks[type]) {
        yield cancel(tasks[type]);
      }
      tasks[type] = yield fork(fetchCounters, type);
    }
    
    function* fetchCounters(type) {
      yield call(delay, 50);
    
      yield put({
        type: actionTypes.COUNTER_FETCH_REQUESTED,
        payload: {type: type, ids: Object.keys(ids[type])},
      });
    
      delete ids[type];
      delete tasks[type];
    }
    
    function* counterFetchRequested(action) {
      const {ids, type} = action.payload;
      let typeName = type + 'Ids';
      let req = {
        [typeName]: ids
      };
      yield put(counterActions.loadCounters(req));
    }
    
    export default [watchCounterRequests, watchCounterFetchRequests];
    

    Most of this comes from here: https://marmelab.com/blog/2016/10/18/using-redux-saga-to-deduplicate-and-group-actions.html