Redux: 如何链接异步操作?

创建于 2016-04-28  ·  24评论  ·  资料来源: reduxjs/redux

您好,我一直在研究 Redux,遇到了一个有趣的问题? 需要从其他动作发出一连串的异步请求
1-getUser()
2-getPost()

登录用户后执行 2 个解决方案 .then(dispatch({type:GET_POST_REQUEST}))
或者在middleWare中编写函数。

如何正确做?

最有用的评论

你好! 这是一个问题跟踪器,而不是一个支持论坛。 感谢您下次在 StackOverflow 上提问,因为这里的答案会丢失,这与 SO 不同。

也就是说,如果您使用Redux Thunk中间件创建存储,您可以像这样编写异步操作创建器:

// If you use Redux Thunk...
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(reducer, applyMiddleware(thunk))

// You can define asynchronous action creators that return functions.
// We call such action creators "thunks":

export function getUser(id) {
  // Redux Thunk will inject dispatch here:
  return dispatch => {
    // Reducers may handle this to set a flag like isFetching
    dispatch({ type: 'GET_USER_REQUEST', id })

    // Perform the actual API call
    return fetchUser().then(
      response => {
        // Reducers may handle this to show the data and reset isFetching
        dispatch({ type: 'GET_USER_SUCCESS', id,  response })
      },
      error => {
        // Reducers may handle this to reset isFetching
        dispatch({ type: 'GET_USER_FAILURE', id,  error })
        // Rethrow so returned Promise is rejected
        throw error
      }
    )
  }
}

// Thunks can be dispatched, if Redux Thunk is applied,
// just like normal action creators:
store.dispatch(getUser(42));

// The return value of dispatch() when you dispatch a thunk *is*
// the return value of the inner function. This is why it's useful
// to return a Promise (even though it is not strictly necessary):
store.dispatch(getUser(42)).then(() =>
  console.log('Fetched user and updated UI!')
)

// Here is another thunk action creator.
// It works exactly the same way.
export function getPost(id) {
  return dispatch => {
    dispatch({ type: 'GET_POST_REQUEST', id })
    return fetchPost().then(
      response => dispatch({ type: 'GET_POST_SUCCESS', id,  response }),
      error => {
        dispatch({ type: 'GET_POST_FAILURE', id,  error })
        throw error
      }
    )
  }
}

// Now we can combine them
export function getUserAndTheirFirstPost(userId) {
  // Again, Redux Thunk will inject dispatch here.
  // It also injects a second argument called getState() that lets us read the current state.
  return (dispatch, getState) => {
    // Remember I told you dispatch() can now handle thunks?
    return dispatch(getUser(userId)).then(() => {
      // Assuming this is where the fetched user got stored
      const fetchedUser = getState().usersById[userId]
      // Assuming it has a "postIDs" field:
      const firstPostID = fetchedUser.postIDs[0]
      // And we can dispatch() another thunk now!
      return dispatch(getPost(firstPostID))
    })
  }
}

// And we can now wait for the combined thunk:
store.dispatch(getUserAndTheirFirstPost(43)).then(() => {
  console.log('fetched a user and their first post')
})

// We can do this anywhere we have access to dispatch().
// For example, we can use this.props.dispatch, or put action
// creators right into the props by passing them to connect, like this:
// export default connect(mapStateToProps, { getUserAndTheirFirstPost })

我们应该把它放到常见问题解答中。

所有24条评论

你好! 这是一个问题跟踪器,而不是一个支持论坛。 感谢您下次在 StackOverflow 上提问,因为这里的答案会丢失,这与 SO 不同。

也就是说,如果您使用Redux Thunk中间件创建存储,您可以像这样编写异步操作创建器:

// If you use Redux Thunk...
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(reducer, applyMiddleware(thunk))

// You can define asynchronous action creators that return functions.
// We call such action creators "thunks":

export function getUser(id) {
  // Redux Thunk will inject dispatch here:
  return dispatch => {
    // Reducers may handle this to set a flag like isFetching
    dispatch({ type: 'GET_USER_REQUEST', id })

    // Perform the actual API call
    return fetchUser().then(
      response => {
        // Reducers may handle this to show the data and reset isFetching
        dispatch({ type: 'GET_USER_SUCCESS', id,  response })
      },
      error => {
        // Reducers may handle this to reset isFetching
        dispatch({ type: 'GET_USER_FAILURE', id,  error })
        // Rethrow so returned Promise is rejected
        throw error
      }
    )
  }
}

// Thunks can be dispatched, if Redux Thunk is applied,
// just like normal action creators:
store.dispatch(getUser(42));

// The return value of dispatch() when you dispatch a thunk *is*
// the return value of the inner function. This is why it's useful
// to return a Promise (even though it is not strictly necessary):
store.dispatch(getUser(42)).then(() =>
  console.log('Fetched user and updated UI!')
)

// Here is another thunk action creator.
// It works exactly the same way.
export function getPost(id) {
  return dispatch => {
    dispatch({ type: 'GET_POST_REQUEST', id })
    return fetchPost().then(
      response => dispatch({ type: 'GET_POST_SUCCESS', id,  response }),
      error => {
        dispatch({ type: 'GET_POST_FAILURE', id,  error })
        throw error
      }
    )
  }
}

// Now we can combine them
export function getUserAndTheirFirstPost(userId) {
  // Again, Redux Thunk will inject dispatch here.
  // It also injects a second argument called getState() that lets us read the current state.
  return (dispatch, getState) => {
    // Remember I told you dispatch() can now handle thunks?
    return dispatch(getUser(userId)).then(() => {
      // Assuming this is where the fetched user got stored
      const fetchedUser = getState().usersById[userId]
      // Assuming it has a "postIDs" field:
      const firstPostID = fetchedUser.postIDs[0]
      // And we can dispatch() another thunk now!
      return dispatch(getPost(firstPostID))
    })
  }
}

// And we can now wait for the combined thunk:
store.dispatch(getUserAndTheirFirstPost(43)).then(() => {
  console.log('fetched a user and their first post')
})

// We can do this anywhere we have access to dispatch().
// For example, we can use this.props.dispatch, or put action
// creators right into the props by passing them to connect, like this:
// export default connect(mapStateToProps, { getUserAndTheirFirstPost })

我们应该把它放到常见问题解答中。

我解决了这个问题如下。 无需修改操作。 我放入了组件 Promise。

  clickShowUserEvent(data) {
   Promise.resolve(data.userAuth(data.login, data.password)) // dispatch
    .then(function (response) {
      data.showEvents(); //dispatch
      return response;
    })
    .then(function(response){console.log("@RESPONSE",response);data.show(data)})
  }

这是正确的决定吗?

这也有效,使用哪种模式取决于您。

@ar53n在 React 组件中带有 promise 的模式有几个缺陷:

  • 没有错误处理(在上面的示例中),即catch部分。 您可能会收到未经处理的拒绝。
  • 它是不间断的,例如,当组件卸载或发生某些改变应用程序状态的操作时。
  • 它的状态是隐含的。 尽管副作用是一个单独的讨论,但至少在应用程序状态下跟踪该正在运行的进程会很有用。

@sompylasar谢谢约翰谢谢你的评论。 我只是不想修改简单的动作。 我们有 2 个简单的操作AuthenticationGetEvents这是 2 个异步请求,甚至包含catch 。 单击组件时仅调用此操作
例如

export function userAuth(login, password) {
  return (dispatch, getState) => {
    console.log('STATE', getState())
    let newState = dispatch(requestUserAuth(login, password))
    return fetch(AUTH_URL + newState.queryParams, MY_INIT)
      .then(response => response.json())
      .then(function (json) { dispatch(receiveUserAuth(json)); return json})
      .catch(error => dispatch(errorUserAuth(error)))
  }
}

我们有这个
image

如果我错了,请纠正我,谢谢

@ar53n那你很好,错误得到处理,并且在商店中跟踪过程。 组件中的不间断进程的问题仍然存在,但如果您在组件或操作重击中开始,这可能并不那么重要。

@ar53n你也可以看看redux-dataloader
它专为复杂的链异步操作而设计。
希望能帮助到你!

关闭它,因为这里发布了一些可能的解决方案。 您现在可能还想研究 redux-saga!

@gaearon你的例子是否打破了时间旅行,不是吗?

我的意思是,例如你的 AJAX 调用第一次失败,然后你去修复服务器端并想要重做它

@gaearon我尝试了您的解决方案,但是当我尝试从任何组件调用store.dispatch(...)时(在我的情况下,从 LoginComponent 执行授权请求)我收到了这个错误:

undefined is not an object (evaluating '_AppNavigator.AppNavigator.router')

似乎有什么不对劲。 我已经像这样设置了动作创建者:

// actions.tsx
export const ActionCreators = {
    authenticate: (username: string, password: string) => {
        return (dispatch) => {
            return auth.login(username, password).then(
                response => {
                    dispatch(navActionCreators.login(res))
                    return response
                },
                error => {
                    throw error
                }
            )
        }
    }
}

// LoginScreen.tsx (login method)
store.dispatch(authActions.authenticate(this.state.username, this.state.password))
  .then((res) => {
     this.setState({isLoading: false})
   })
   .catch((error: Error) => {
     this.setState({
       isLoading: false,
       error: error ? error.message : 'Si è verificato un\' errore.'
     })
   })

我错过了什么?

@bm-software:这个问题应该在 Stack Overflow 上提出。

@ar53n@sompylasar我知道已经有一段时间了,但我现在正在努力解决这种模式。 @ar53n在您的示例中,如果userAuth fetch ,那么在调用userAuth的链中会发生什么?

看起来.catch(error => dispatch(errorUserAuth(error)))会调度 errerUserAuth 操作,这很棒。 在 Redux 中,这通常是我们“处理”错误的方式。 但是在你之前提到的链条中:

clickShowUserEvent(data) {
   Promise.resolve(data.userAuth(data.login, data.password)) // dispatch
    .then(function (response) {
      data.showEvents(); //dispatch
      return response;
    })
    .then(function(response){console.log("@RESPONSE",response);data.show(data)})
  }

data.showEvents() _总是_会被调用,即使用户身份验证失败。 我不认为这是大多数人所期望或想要的,但由于 Redux 错误处理通常是通过调度而不是重新抛出来完成的,它会吞下错误,因此承诺链无法按预期工作。

此外,如果您确实重新投掷,您将不得不在_应用程序中的每一个动作创建者调用,无处不在_上进行.catch() 。 上面@gaearon的示例,他重新抛出所有错误,最后这样做:

// And we can now wait for the combined thunk:
store.dispatch(getUserAndTheirFirstPost(43)).then(() => {
  console.log('fetched a user and their first post')
})

如果getUserAndTheirFirstPost内的大组合链中的 _anything_ 失败,则会出现“未处理的承诺拒绝”错误。

我认为唯一的答案是重新抛出然后.catch()到处,或者可能使用 React 16 错误边界。

@jasonrhodes

clickShowUserEvent(data) {
   Promise.resolve(data.userAuth(data.login, data.password)) // dispatch
    .then(function (response) {
      data.showEvents(); //dispatch

data.showEvents() _always_ 将被调用,即使用户身份验证失败。

不,如果data.userAuth(data.login, data.password)返回一个最终被拒绝的承诺,它将不会被调用。 .then的第一个参数在 promise 完成时调用(称为onFulfilled ),第二个参数在 promise 被拒绝时调用(称为onRejected )——参见规范.

另一方面,React 16 错误边界对 Promise 没有帮助,它们只捕获同步抛出的异常以确保 React 内部状态不被破坏。

@jasonrhodes另外,成为 Promise 世界的好公民,要么将 Promise 返回给调用者(然后它必须处理错误),要么将.catch附加到它(并在创建 Promise 的地方处理错误)。

你的例子没有:

clickShowUserEvent(data) {
   Promise.resolve(data.userAuth(data.login, data.password)) // dispatch
    .then(function (response) {
      data.showEvents(); //dispatch
      return response;
    })
    .then(function(response){console.log("@RESPONSE",response);data.show(data)})
  }

@sompylasar这不是我的示例,它是您在此线程中早些时候一直在回复的示例。 我知道已经有一段时间了,但我在这里引用了之前的对话,因为我不止一次通过 Google 搜索遇到过这个帖子。

再看一遍,即使userAuth fetch调用失败, data.showEvents()也会_总是_被调用。 为什么? 因为userAuth函数内部有一个.catch()以 Redux 方式处理错误:通过调度错误操作。

这就是我的帖子的重点:当您从异步操作中捕获错误以便可以分派错误操作时,您是否会吞下并阻止进一步的承诺链正常工作? 或者您是否重新抛出并强制该动作创建者的每个调用者.catch()无论如何以避免“未处理的承诺拒绝”?

此外,根据我的经验,我发现在将某人链接到规范之前了解他们所知道的内容是个好主意。 感谢您的回复,我知道这是我挖掘的旧对话,但它很重要,因为您可以看到它是多么容易错过!

@jasonrhodes

@sompylasar这不是我的示例,它是您在此线程中早些时候一直在回复的示例。 我知道已经有一段时间了,但我在这里引用了之前的对话,因为我不止一次通过 Google 搜索遇到过这个帖子。

明白了,我很抱歉,没有回忆起整个上下文。

因为在userAuth函数内部有一个.catch()以 Redux 方式处理错误:通过调度错误操作。

明白了,那么要么按预期工作,要么如果您返回该承诺(并在调用站点处理错误),则不应将.catch放在那里,或者您应该在.catch中重新抛出错误

无论如何,thunk 本身并不适合链接。 您应该在 thunk 中进行链接,或者将 sagas 用于更复杂的异步工作流。

我一直被打破承诺链的问题困扰了一段时间。 通常,我的动作创建者会在 .then() 中发出 SUCCESS 动作,或者在 .catch() 中发出 FAILURE 动作,该请求是从 thunk 返回的同一个 http 请求。

每当我的动作创建者进入 catch 块并且我执行了this.props.myActionCreator().then(() => )时,即使请求中有问题,then 中的代码也会执行。

考虑到这一点,我总是确保检查应用程序状态中的错误变量,该变量在 FAILURE 情况下为该操作创建者设置。 但是当您调用多个动作创建者时,事情会变得一团糟,尤其是那些依赖于另一个的动作创建者。 我必须有一个 if 语句来检查许多错误变量。

我喜欢不通过在 catch 块中为动作创建者返回值重新抛出错误来破坏承诺链的事实。 但是,这需要我们使用 React 组件中的 .catch() ,其中调用了动作创建者。 我不会在那个 catch 块中写入任何内容,因为错误变量已经由减速器中的 FAILURE 操作的处理设置。

那么你们, @jasonrhodes@sompylasar ,会推荐我使用重新抛出方法并在 React 组件中的动作创建者调用的承诺链上放置一个空的 .catch() 块吗?

@nbkhope老实说,这是我使用 Redux 时遇到的最大问题,直到今天我还没有找到一个好的答案。 很抱歉没有提供更多帮助!

伙计们,您可以在本文中找到一些 thunk 替代方案https://decembersoft.com/posts/what-is-the-right-way-to-do-asynchronous-operations-in-redux/

@gaearon

关于您的第一个答案,如果我使用 async & await 而不是 Promise,我应该如何转换它? 类似于以下内容:

export const funcA = () => {
    return async (dispatch) => {
        const data = await doSomething(...)
        dispatch({ action: DID_SOMETHING, payload: data })
    }
}

export const funcB = () => {
    return async (dispatch) => {
        const data = await doSomethingElse(...)
        dispatch({ action: DID_SOMETHING_ELSE, payload: data })
    }
}

export const funcC = () => {
    return async (dispatch) => {
        const data = await doSomethingMore(...)
        dispatch({ action: DID_SOMETHING_MORE, payload: data })
    }
}

// how to chain funcA, funcB and funcC
const myFunc = () => {
    // execute funcA
    // when complete execute funcB
    // when complete execute funcC
}

@yingdongzhang ,您可以将它们链接如下:

const myFunc = () => {
  return async (dispatch) => {
    try {
      await dispatch(funcA())
      await dispatch(funcB())
      await dispatch(funcC())
    } catch (error) {
      //error handling
    }
  }
}

@Boomxx谢谢你按预期工作。

我想知道我将如何做fetch all posts of the user

@km16 :这是一个错误跟踪器,而不是一个支持系统。 对于使用问题,请使用 Stack Overflow 或 Reactiflux,那里有更多的人准备帮助您 - 您可能会更快地得到更好的答案。 谢谢!

此页面是否有帮助?
0 / 5 - 0 等级