如何避免Redux-Saga的重复API请求?

时间:2016-05-05 01:29:12

标签: javascript redux redux-saga

到目前为止,我比其他Flux实现更喜欢Redux,并且我用它来重写我们的前端应用程序。

我面临的主要难点:

  1. 维护API调用的状态,以避免发送重复的请求。
  2. 维护记录之间的关系。
  3. 第一个问题可以通过将状态字段保持在每种类型数据的子状态来解决。 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 },
          };
      }
    }
    

    现在我可以为帖子制作一个Saga,为评论制作另一个。每个Sagas都知道如何获取请求的状态。但这很快就会导致很多重复的代码(例如帖子,评论,喜欢,反应,作者等)。

    我想知道是否有一种避免所有重复代码的好方法。

    当我需要从redux商店获取ID评论时,第二个问题就出现了。是否有处理数据之间关系的最佳实践?

    谢谢!

3 个答案:

答案 0 :(得分:4)

首先,您可以创建一个通用的动作创建者来获取帖子。

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

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

当您调用此抓取后操作时,它会触发 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))
  }
}

答案 1 :(得分:4)

我从Redux Saga的所有者那里实现了this solution并且效果非常好 - 我从API调用中得到错误,有时会被解雇两次:

  

你可以为此创建一个更高阶的传奇,看起来像这样:

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

并像这样使用它:

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)
  

在我看来,这种方式更加优雅,并且可以根据您的需要轻松定制其行为。

答案 2 :(得分:2)

我在项目中遇到了完全相同的问题。 我尝试过redux-saga,看起来它真的是一个合理的工具来控制数据流,减少副作用。但是,处理实际问题(如重复请求和处理数据之间的关系)有点复杂。

所以我创建了一个小型图书馆' redux-dataloader'解决这个问题。

动作创作者

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
  }
}

为请求操作创建侧面加载器

然后为FETCH_POSTS'创建数据加载器。和' 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
];

将redux-dataloader应用于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)
  );
}

处理数据链

好的,然后只需使用dispatch(requestAction)来处理数据之间的关系。

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);

通知承诺的请求操作将在ttl中缓存,并防止重复请求。

顺便说一句,如果您使用async / await,您可以使用redux-dataloader处理数据提取,如下所示:

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
  }
}