Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid duplicate API requests with Redux-Saga?

So far I like Redux better than other Flux implementations, and I'm using it to re-write our front end application.

The main struggling points that I'm facing:

  1. Maintaining the status of API calls to avoid sending duplicate requests.
  2. Maintaining relationships between records.

The first issue could be solved by keeping a status field in the sub-state of each type of data. E.g.:

function postsReducer(state, action) {
  switch(action.type) {
    case "FETCH_POSTS":
      return {
        ...state,
        status: "loading",
      };
    case "LOADED_POSTS":
      return {
        status: "complete",
        posts: action.posts,
      };
  }
}

function commentsReducer(state, action) {
  const { type, postId } = action;
  switch(type) {
    case "FETCH_COMMENTS_OF_POST":
      return {
        ...state,
        status: { ...state.status, [postId]: "loading" },
      };
    case "LOADED_COMMENTS_OF_POST":
      return {
        status: { ...state.status, [postId]: "complete" },
        posts: { ...state.posts, [postId]: action.posts },
      };
  }
}

Now I can make a Saga for Posts and another one for Comments. Each of the Sagas knows how to get the status of requests. But that would lead to a lot of duplicate code soon (e.g. Posts, Comments, Likes, Reactions, Authors, etc).

I'm wondering if there is a good way to avoid all that duplicate code.

The 2nd issue comes to existence when I need to get a comment by ID from the redux store. Are there best practices for handling relationships between data?

Thanks!

like image 568
Mouad Debbar Avatar asked May 05 '16 01:05

Mouad Debbar


People also ask

Is redux saga multithreaded?

As I mentioned above, you can run tasks parallel in JavaScript using web workers, through it's multi-threading from technical point of view.

What is the benefit of using redux saga?

redux-saga is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, easy to test, and better at handling failures.

What is takeLatest in redux saga?

takeLatest(pattern, saga, ... And automatically cancels any previous saga task started previously if it's still running. Each time an action is dispatched to the store. And if this action matches pattern , takeLatest starts a new saga task in the background.

Is redux saga synchronous?

Redux Saga utilizes ES6 generator functions for this. Generators allow for synchronously written asynchronous code. A generator will automatically pause — or yield — at each asynchronous call until it completes before continuing.


3 Answers

redux-saga now has takeLeading(pattern, saga, ...args)

Version 1.0+ of redux-saga has takeLeading that spawns a saga on each action dispatched to the Store that matches pattern. After spawning a task once, it blocks until the spawned saga completes and then starts to listen for a pattern again.

Previously I implemented this solution from the owner of Redux Saga and it worked really well - I was getting errors from API calls sometimes being fired twice:

You could create a higher order saga for this, which would look something like this:

function* takeOneAndBlock(pattern, worker, ...args) {
  const task = yield fork(function* () {
    while (true) {
      const action = yield take(pattern)
      yield call(worker, ...args, action)
    }
  })
  return task
}

and use it like this:

function* fetchRequest() {
  try {
    yield put({type: 'FETCH_START'});
    const res = yield call(api.fetch);
    yield put({type: 'FETCH_SUCCESS'});
  } catch (err) {
    yield put({type: 'FETCH_FAILURE'});
  }
}

yield takeOneAndBlock('FETCH_REQUEST', fetchRequest)

In my opinion this way is far way more elegant and also its behaviour can be easily customized depending on your needs.

like image 184
The Coder Avatar answered Sep 22 '22 17:09

The Coder


I had the exact same issue in my project. I have tried redux-saga, it seems that it's really a sensible tool to control the data flow with redux on side effects. However, it's a little complex to deal with the real world problem such as duplicate requests and handling relationships between data.

So I created a small library 'redux-dataloader' to solve this problem.

Action Creators

import { load } from 'redux-dataloader'
function fetchPostsRequest() {
  // Wrap the original action with load(), it returns a Promise of this action. 
  return load({
    type: 'FETCH_POSTS'
  });
}

function fetchPostsSuccess(posts) {
  return {
    type: 'LOADED_POSTS',
    posts: posts
  };
}

function fetchCommentsRequest(postId) {
  return load({
    type: 'FETCH_COMMENTS',
    postId: postId
  });
}

function fetchCommentsSuccess(postId, comments) {
  return {
    type: 'LOADED_COMMENTS_OF_POST',
    postId: postId,
    comments: comments
  }
}

Create side loaders for request actions

Then create data loaders for 'FETCH_POSTS' and 'FETCH_COMMENTS':

import { createLoader, fixedWait } from 'redux-dataloader';

const postsLoader = createLoader('FETCH_POSTS', {
  success: (ctx, data) => {
    // You can get dispatch(), getState() and request action from ctx basically.
    const { postId } = ctx.action;
    return fetchPostsSuccess(data);
  },
  error: (ctx, errData) => {
    // return an error action
  },
  shouldFetch: (ctx) => {
    // (optional) this method prevent fetch() 
  },
  fetch: async (ctx) => {
    // Start fetching posts, use async/await or return a Promise
    // ...
  }
});

const commentsLoader = createLoader('FETCH_COMMENTS', {
  success: (ctx, data) => {
    const { postId } = ctx.action;
    return fetchCommentsSuccess(postId, data);
  },
  error: (ctx, errData) => {
    // return an error action
  },
  shouldFetch: (ctx) => {
    const { postId } = ctx.action;
    return !!ctx.getState().comments.comments[postId];
  },
  fetch: async (ctx) => {
    const { postId } = ctx.action;
    // Start fetching comments by postId, use async/await or return a Promise
    // ...
  },
}, {
  // You can also customize ttl, and retry strategies
  ttl: 10000, // Don't fetch data with same request action within 10s
  retryTimes: 3, // Try 3 times in total when error occurs
  retryWait: fixedWait(1000), // sleeps 1s before retrying
});

export default [
  postsLoader,
  commentsLoader
];

Apply redux-dataloader to redux store

import { createDataLoaderMiddleware } from 'redux-dataloader';
import loaders from './dataloaders';
import rootReducer from './reducers/index';
import { createStore, applyMiddleware } from 'redux';

function configureStore() {
  const dataLoaderMiddleware = createDataLoaderMiddleware(loaders, {
    // (optional) add some helpers to ctx that can be used in loader
  });

  return createStore(
    rootReducer,
    applyMiddleware(dataLoaderMiddleware)
  );
}

Handle data chain

OK, then just use dispatch(requestAction) to handle relationships between data.

class PostContainer extends React.Component {
  componentDidMount() {
    const dispatch = this.props.dispatch;
    const getState = this.props.getState;
    dispatch(fetchPostsRequest()).then(() => {
      // Always get data from store!
      const postPromises = getState().posts.posts.map(post => {
        return dispatch(fetchCommentsRequest(post.id));
      });
      return Promise.all(postPromises);
    }).then() => {
      // ...
    });
  }

  render() {
    // ...
  }
}

export default connect(
  state => ()
)(PostContainer);

NOTICE The promised of request action with be cached within ttl, and prevent duplicated requests.

BTW, if you are using async/await, you can handle data fetching with redux-dataloader like this:

async function fetchData(props, store) {
  try {
    const { dispatch, getState } = store;
    await dispatch(fetchUserRequest(props.userId));
    const userId = getState().users.user.id;
    await dispatch(fetchPostsRequest(userId));
    const posts = getState().posts.userPosts[userId];
    const commentRequests = posts.map(post => fetchCommentsRequest(post.id))
    await Promise.all(commentRequests);
  } catch (err) {
    // error handler
  }
}
like image 24
Bin HOU Avatar answered Sep 23 '22 17:09

Bin HOU


First, you can create a generic action creator for fetching post.

function fetchPost(id) {
  return {
   type: 'FETCH_POST_REQUEST',
   payload: id,
  };
}

function fetchPostSuccess(post, likes, comments) {
  return {
    type: 'FETCH_POST_SUCCESS',
    payload: {
      post,
      likes,
      comments,
    },
  };
}

When you call this fetch post action, it'll trigger onFetchPost saga.

function* watchFetchPost() {
  yield* takeLatest('FETCH_POST_REQUEST', onFetchPost);
}

function* onFetchPost(action) {
  const id = action.payload;

  try {
    // This will do the trick for you.
    const [ post, likes, comments ] = yield [
      call(Api.getPost, id),
      call(Api.getLikesOfPost, id),
      call(Api.getCommentsOfPost, id),
    ];

    // Instead of dispatching three different actions, heres just one!
    yield put(fetchPostSuccess(post, likes, comments));
  } catch(error) {
    yield put(fetchPostFailure(error))
  }
}
like image 32
altay Avatar answered Sep 23 '22 17:09

altay