10

到目前为止,我比其他 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 },
      };
  }
}

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

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

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

谢谢!

4

3 回答 3

8

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

redux-saga 的 1.0+ 版本具有 takeLeading ,它在调度到与模式匹配的 Store 的每个操作上产生一个 saga。生成一次任务后,它会阻塞,直到生成的 saga 完成,然后再次开始侦听模式。

以前我从 Redux Saga 的所有者那里实现了这个解决方案,它运行得非常好——我从 API 调用中得到错误,有时会被触发两次:

您可以为此创建一个更高阶的 saga,如下所示:

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)

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

于 2017-08-03T00:43:33.447 回答
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
  }
}

为请求操作创建侧加载程序

然后为“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 存储

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
  }
}
于 2016-05-09T18:28:53.700 回答
2

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

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

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

当您调用此 fetch post 操作时,它将触发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))
  }
}
于 2016-09-07T19:46:19.597 回答