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

Mou*_*bar 6 javascript redux redux-saga

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

我面临的主要困难点:

  1. 维护API调用的状态以避免发送重复的请求.
  2. 维护记录之间的关系.

第一个问题可以通过将状态字段保持在每种类型数据的子状态来解决.例如:

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 },
      };
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

谢谢!

The*_*der 5

redux-saga现在具有takeLeading(pattern,saga,... args)

redux-saga的1.0+版具有takeLeading,可在分配给与模式匹配的Store的每个动作上产生一个传奇。一次生成任务后,它将阻塞,直到生成的传奇完成,然后再次开始侦听模式。

以前,我是从Redux Saga的所有者那里实施此解决方案的,而且效果非常好-我从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
}
Run Code Online (Sandbox Code Playgroud)

并像这样使用它:

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)
Run Code Online (Sandbox Code Playgroud)

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


小智 2

我在我的项目中遇到了完全相同的问题。我尝试过 redux-saga,看起来它确实是一个明智的工具,可以通过 redux 的副作用来控制数据流。然而,处理现实世界的问题(例如重复请求和处理数据之间的关系)有点复杂。

所以我创建了一个小型图书馆' 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
  }
}
Run Code Online (Sandbox Code Playgroud)

为请求操作创建侧加载器

然后为“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
];
Run Code Online (Sandbox Code Playgroud)

将 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)
  );
}
Run Code Online (Sandbox Code Playgroud)

处理数据链

OK,那么就用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);
Run Code Online (Sandbox Code Playgroud)

注意请求动作的承诺会缓存在 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
  }
}
Run Code Online (Sandbox Code Playgroud)