【问题标题】:Handle unsuccessful Ajax calls with Redux and Redux Thunk in React在 React 中使用 Redux 和 Redux Thunk 处理不成功的 Ajax 调用
【发布时间】:2018-07-11 07:09:50
【问题描述】:

对于我的 React 组件,我使用 Redux (react-redux) 和 Redux Thunk 来维护应用程序状态。

有一个ShowGroup 组件从后端检索group。如果组不存在(或任何其他错误情况),则会调用错误回调来更新状态。

class ShowGroup extends Component {
  constructor(props) {
    super(props);
    this.groupId = this.props.match.params.id;
    this.state = {
      'loaded': false,
      'error': null
    };
  }

  _fetchGroupErrorCallback = response => {
    this.setState({loaded: true, error: response});
  }

  componentDidMount() {
    this.props.fetchGroup(this.groupId, this._fetchGroupErrorCallback);
  }

  render() {
    //what is the best practice here ?
    let group = this.props.groups[this.groupId];
    if (!group) {
      if (!this.state.loaded) {
        return <div>Loading group ....</div>;
      }
      if (this.state.error) {
        return <div>{this.state.error.data.message}</div>;
      }
    }
    return (
      <div className="show-group">
        <form>
          {_.map(FIELDS, renderField.bind(this))}
        </form>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return { groups: state.groups };
}

const mapDispatchToProps = (dispatch) => {
    return {
        fetchGroup: (id, callback) => dispatch(fetchGroup(id, callback)),
        updateGroup: (id) => dispatch(updateGroup(id))
    };
};

export default reduxForm({
  validate,
  //a unique id for this form
  form:'ShowGroup',
  fields: _.keys(FIELDS),
  fields_def: FIELDS
})(
  connect(mapStateToProps, mapDispatchToProps)(ShowGroup)
);

(这是一个redux-form 组件,没关系)

export function fetchGroup(id, fetchErrorCallback) {
  return (dispatch) => {
    axios.get(URL, AUTHORIZATION_HEADER)
      .then(response => {
        dispatch(groupFetched(response))
      })
      .catch(({response}) => {
        fetchErrorCallback(response);
        dispatch(groupFetchErrored(response));
      })
  };
}

groupFetchedErrored动作创建者:

export function groupFetchErrored(response) {
  //handle the error here
  return {
     type: FETCH_GROUP_ERROR,
     response
  }
}

还有减速机:

export default function(state=INITIAL_STATE, action) {
  switch(action.type) {
    //....
    case FETCH_GROUP_ERROR:
      return _.omit(state, action.response.data.id);
    default:
      return state;
  }
}

问题:

A. 如果出现错误响应,组件会被渲染 3 次: 1.第一次加载(在ajax调用之后) 2._fetchGroupErrorCallback被调用并设置状态,导致渲染 3. groupFetchErrored 被调度,导致另一个渲染

B. 如果响应成功,组件会被渲染两次,但状态不正确,因为没有任何更新(我认为为其添加回调是不正确的)

在将 Redux 和 Redux-Thunk 与 React 结合使用时,处理 ajax 错误响应的最佳实践是什么? action creator 和 reducer 应该如何通知组件有问题?

更新 1

这是我的根减速器:

const rootReducer = combineReducers({
  form: formReducer,
  groups: groupReducer
});

export default rootReducer;

如你所见,我有groups,它是一个表单对象:

{
   groupId1: groupData1,
   groupId2, groupData2, 
   ....
}

所以,另一个问题是,我应该如何指定一个组正在加载(我不能在 groups 级别上使用 isLoadingerrorFetching,添加一个条目似乎不是一个好主意对于每个无效的组 id 用户都可以尝试。或者也许有一种方法可以将无效的组 id 放在地图中然后清理它?不过我不知道应该在哪里发生这种情况。

【问题讨论】:

  • 为什么要同时使用 state 和 props?让 props 成为唯一的真实来源,根本不使用状态。
  • 听起来不错,但是fetchGroup 应该如何告诉组件服务器响应错误?

标签: reactjs redux react-redux


【解决方案1】:

在 redux 的新模板和文档中,您可以使用 extraReducers,并检查 thunk 操作的挂起/完成状态。 它更简单,并且只会渲染你的组件一次。 例如:

export const load = createAsyncThunk(
  'worldtime/load',
  async (arg, thunkApi) => {
    const {ok, response} = await sendRequest('get', '/api/json/est/now')

    if (ok) {
      return response
    }
  },
)

然后 extraReducers 可以检查有效负载:

.addCase(load.fulfilled, (state, action) => {
  state.loading = false
  if (!action.payload) {
    // request had failed, do not update
    return
  }
  state.queries++
  state.time = action.payload.currentDateTime
})

请参阅以下帖子中的完整示例: https://runkiss.blogspot.com/2021/11/create-redux-app-with-async-ajax-calls.html

【讨论】:

    【解决方案2】:

    将所有与 fetch 相关的状态数据放入 Redux 存储中。然后商店就成了你唯一的真实来源。

    然后您可以完全省略 fetchErrorCallback

    您的 reducer 中已经有一个位置会收到错误通知:

    case FETCH_GROUP_ERROR:
      return _.omit(state, action.response.data.id);
    

    您可以将错误存储在 redux 状态中:

    case FETCH_GROUP_ERROR:
      const stateWithoutId = _.omit(state, action.response.data.id);
      return {
        ...stateWithoutId,
        errorFetching: action.response.error,
        isLoading: false
      }
    

    然后通过mapStateToPropserrorFetchingisLoading 传递给您的组件,就像您已经在使用groups 一样。

    为了正确跟踪isLoading,最好在开始获取时(就在调用axios.get 之前)调度另一个操作,也许FETCH_GROUP_STARTED。然后在你的 reducer 中将它设置为 isLoadingtrue 当该动作类型被调度时。

    case GROUP_FETCHED: 
      return { 
        ...state, 
        isLoading: false,
        group: action.response.data // or however you're doing this now.
    
      };
    case FETCH_GROUP_STARTED:
      return { ...state, isLoading: true }; // or maybe isFetchingGroups is a better name
    

    如果这是我的代码,我会进行另一项更改:我不会将 response 传递给动作创建者,而是只删除相关数据并将 that 传递给他们。所以reducer不需要知道如何处理response对象;它只是接收正确显示组件所需的数据。这样,所有处理 axios 调用并且必须知道它们返回的对象的形状的代码都在一个地方 - 动作函数。比如:

    动作创建者

    export function groupFetchErrored(errorWithId) {
      return {
         type: FETCH_GROUP_ERROR,
         payload: errorWithId
      }
    }
    
    export function groupFetchSucceeded(groupData) 
      return {
         type: GROUP_FETCHED,
         payload: groupData
      }
    }
    

    动作

    export function fetchGroup(id, fetchErrorCallback) {
      return (dispatch) => {
        axios.get(URL, AUTHORIZATION_HEADER)
          .then(response => {
            dispatch(groupFetched(response.data)) // or whatever chunk you need out of response
          })
          .catch(({response}) => {
            dispatch(groupFetchErrored({
                error: response.error, 
                id: response.data.id
              }
            );
          })
      };
    }
    

    减速器

    您可能已经注意到我在上面的动作创建器中使用了action.payload

    通常的做法是始终为已调度操作的数据属性使用相同的名称(与往常一样,在整个应用中的每个 reducer 和每个操作创建者中)。这样你的 reducer 就不需要和 action creator 紧密耦合了;它知道数据总是在payload 字段中。 (也许这就是您对 response 所做的事情,但这似乎是专门用于 http 响应的。)

    case GROUP_FETCHED: 
      return { 
        ...state, 
        isLoading: false,
        group: action.payload
      };
    case FETCH_GROUP_STARTED:
      return { ...state, isLoading: true }; // no payload needed
    case FETCH_GROUP_ERROR:
      const dataId = action.payload.id;
      const stateWithoutId = _.omit(state, dataId);
      return {
        ...stateWithoutId,
        errorFetching: action.payload.error,
        isLoading: false
      }
    

    【讨论】:

    • 组件第一次渲染,没有isLoadingerrorFetching,这些值应该如何初始化?
    • 我认为您的意思是在 Action 中发送 groupFetchSucceeded,对吧?因为不会再有 groupFetched 了。
    • 您可以在 reducer 中初始化 isLoading 和 errorFetching,但这更多是出于文档目的。由于它们从未设置过,它们都将是 undefined,其计算结果为 false。
    • 我在reducer中使用了初始状态,它工作正常。我在我的问题中添加了 Update 1,这是一个状态设计问题。如果您能看一下,我将不胜感激(这与您建议的设计有关)
    • 关于处理多个不同组的提取,这是一个很大的话题。最终,您希望您的 redux 状态包含 UI 向用户显示正在发生的事情所需的所有内容。人们已经做了很多不同的方式。您可以将数组存储在减速器中。我建议为该主题开始一个新问题 - 但首先搜索,因为这是一个常见问题。
    猜你喜欢
    • 2018-03-19
    • 2019-01-24
    • 2017-05-11
    • 2020-07-12
    • 2017-02-04
    • 1970-01-01
    • 2016-12-11
    • 2019-02-02
    • 2019-09-09
    相关资源
    最近更新 更多