处理基于动作/减速器的应用程序中多个异步调用的加载状态

时间:2017-10-27 12:26:05

标签: redux ngrx

我不认为此问题绑定到特定的框架或库,但适用于遵循操作的所有基于商店的应用程序 - reducer模式。

为清楚起见,我使用的是Angular和@ngrx。

在我正在处理的应用程序中,我们需要跟踪各个资源的加载状态。

我们处理其他异步请求的方式是希望熟悉的模式:

操作

  • GET_RESOURCE
  • GET_RESOURCE_SUCCESS
  • GET_RESOURCE_FAILURE

减速

switch(action.type)
  case GET_RESOURCE:
    return {
      ...state,
      isLoading = true
    };
  case GET_RESOURCE_SUCCESS:
  case GET_RESOURCE_FAILURE:
    return {
      ...state,
      isLoading = false
    };
  ...

这适用于异步调用,我们希望在应用程序中全局指示加载状态。

在我们的应用程序中,我们获取一些数据,例如 BOOKS ,其中包含对其他资源的引用列表,例如 CHAPTERS 。 如果用户想要查看 CHAPTER ,则他/她单击触发异步调用的 CHAPTER 引用。为了向用户表明这个特定的 CHAPTER 正在加载,我们需要的不仅仅是我们州的全局isLoading标志。

我们解决这个问题的方法是创建一个这样的包装对象:

interface AsyncObject<T> {
  id: string;
  status: AsyncStatus;
  payload: T;
}

其中AsyncStatus是这样的枚举:

enum AsyncStatus {
  InFlight,
  Success,
  Error
}

在我们的州,我们像这样存储章节:

{
  chapters: {[id: string]: AsyncObject<Chapter> }
}

然而,我觉得这种状态“混乱”,并且想知道某人是否有更好的解决方案/解决这个问题的方法。

问题

  • 是否有关于如何处理此方案的最佳做法?
  • 有更好的方法来解决这个问题吗?

1 个答案:

答案 0 :(得分:0)

我曾多次面对这种情况,但解决方案根据使用情况而有所不同。

其中一个解决方案是使用嵌套的reducer。它不是反模式,但不建议,因为它很难维护,但它取决于用例。

另一个是我在下面详述的那个。

根据您的描述,您获取的数据应如下所示:

  {
    books: {
      isFetching: false,
      isInvalidated: false,
      selectedBook: null,
      data: {
        1: { id: 1, title: 'Robinson Crusoe', author: 'Daniel Defoe' },
        2: { id: 2, title: 'Gullivers Travels', author: 'Jonathan Swift' },
      }
    },

    chapters: {
      isFetching: false,
      isInvalidated: true,
      selectedChapter: null,
      data: {
        'chp1_robincrusoe': { isFetching: false, isInvalidated: true, id: 'chp1_robincrusoe', bookId: 1, data: null },
        'chp2_robincrusoe': { isFetching: false, isInvalidated: true, id: 'chp2_robincrusoe', bookId: 1, data: null },
        'chp1_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp1_gulliverstravels', bookId: 2, data: null },
        'chp2_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp2_gulliverstravels', bookId: 2, data: null },
        'chp3_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp3_gulliverstravels', bookId: 2, data: null },
      },
    }
  }

因此,根据您的数据,您的减速机应如下所示:

isFetching

使用此结构,您的章节缩减器中不需要isInvalidatedisFetching,因为每个章节都是一个独立的逻辑。

注意:我稍后会就如何以不同的方式利用isInvalidated import React from 'react'; import map from 'lodash/map'; class BookList extends React.Component { componentDidMount() { if (this.props.isInvalidated && !this.props.isFetching) { this.props.actions.readBooks(); } } render() { const { isFetching, isInvalidated, data, } = this.props; if (isFetching || (isInvalidated && !isFetching)) return <Loading />; return <div>{map(data, entry => <Book id={entry.id} />)}</div>; } } 向您提供奖励详情。

详细代码下方:

<强>零件

<强>书目

import React from 'react';
import filter from 'lodash/filter';
import { createSelector } from 'reselect';
import map from 'lodash/map';
import find from 'lodash/find';

class Book extends React.Component {
  render() {
    const {
      dispatch,
      book,
      chapters,
    } = this.props;

    return (
      <div>
        <h3>{book.title} by {book.author}</h3>
        <ChapterList bookId={book.id} />
      </div>
    );
  }
}

const foundBook = createSelector(
  state => state.books,
  (books, { id }) => find(books, { id }),
);

const mapStateToProps = (reducers, props) => {
  return {
    book: foundBook(reducers, props),
  };
};

export default connect(mapStateToProps)(Book);

图书

  import React from 'react';
  import { connect } from 'react-redux';
  import { createSelector } from 'reselect';
  import map from 'lodash/map';
  import find from 'lodash/find';

  class ChapterList extends React.Component {
    render() {
      const { dispatch, chapters } = this.props;
      return (
        <div>
          {map(chapters, entry => (
            <Chapter
              id={entry.id}
              onClick={() => dispatch(actions.readChapter(entry.id))} />
          ))}
        </div>
      );
    }
  }

  const bookChapters = createSelector(
    state => state.chapters,
    (chapters, bookId) => find(chapters, { bookId }),
  );

  const mapStateToProps = (reducers, props) => {
    return {
      chapters: bookChapters(reducers, props),
    };
  };

  export default connect(mapStateToProps)(ChapterList);

<强> ChapterList

  import React from 'react';
  import { connect } from 'react-redux';
  import { createSelector } from 'reselect';
  import map from 'lodash/map';
  import find from 'lodash/find';

  class Chapter extends React.Component {
    render() {
      const { chapter, onClick } = this.props;

      if (chapter.isFetching || (chapter.isInvalidated && !chapter.isFetching)) return <div>{chapter.id}</div>;

      return (
        <div>
          <h4>{chapter.id}<h4>
          <div>{chapter.data.details}</div>  
        </div>
      );
    }
  }

  const foundChapter = createSelector(
    state => state.chapters,
    (chapters, { id }) => find(chapters, { id }),
  );

  const mapStateToProps = (reducers, props) => {
    return {
      chapter: foundChapter(reducers, props),
    };
  };

  export default connect(mapStateToProps)(Chapter);

<强>章

  export function readBooks() {
    return (dispatch, getState, api) => {
      dispatch({ type: 'readBooks' });
      return fetch({}) // Your fetch here
        .then(result => dispatch(setBooks(result)))
        .catch(error => dispatch(addBookError(error)));
    };
  }

  export function setBooks(data) {
    return {
      type: 'setBooks',
      data,
    };
  }

  export function addBookError(error) {
    return {
      type: 'addBookError',
      error,
    };
  }

图书操作

  export function readChapter(id) {
    return (dispatch, getState, api) => {
      dispatch({ type: 'readChapter' });
      return fetch({}) // Your fetch here - place the chapter id
        .then(result => dispatch(setChapter(result)))
        .catch(error => dispatch(addChapterError(error)));
    };
  }

  export function setChapter(data) {
    return {
      type: 'setChapter',
      data,
    };
  }

  export function addChapterError(error) {
    return {
      type: 'addChapterError',
      error,
    };
  }

章节动作

  import reduce from 'lodash/reduce';
  import { combineReducers } from 'redux';

  export default combineReducers({
    isInvalidated,
    isFetching,
    items,
    errors,
  });

  function isInvalidated(state = true, action) {
    switch (action.type) {
      case 'invalidateBooks':
        return true;
      case 'setBooks':
        return false;
      default:
        return state;
    }
  }

  function isFetching(state = false, action) {
    switch (action.type) {
      case 'readBooks':
        return true;
      case 'setBooks':
        return false;
      default:
        return state;
    }
  }

  function items(state = {}, action) {
    switch (action.type) {
      case 'readBook': {
        if (action.id && !state[action.id]) {
          return {
            ...state,
            [action.id]: book(undefined, action),
          };
        }

        return state;
      }
      case 'setBooks':
        return {
          ...state,
          ...reduce(action.data, (result, value, key) => ({
            ...result,
            [key]: books(value, action),
          }), {});
        },
      default:
        return state;
    }
  }

  function book(state = {
    isFetching: false,
    isInvalidated: true,

    id: null,
    errors: [],
  }, action) {
    switch (action.type) {
      case 'readBooks':
        return { ...state, isFetching: true };
      case 'setBooks':
        return {
          ...state,
          isInvalidated: false,
          isFetching: false,
          errors: [],
        };
      default:
        return state;
    }
  }

  function errors(state = [], action) {
    switch (action.type) {
      case 'addBooksError':
        return [
          ...state,
          action.error,
        ];
      case 'setBooks':
      case 'setBooks':
        return state.length > 0 ? [] : state;
      default:
        return state;
    }
  }

Book Reducers

setBooks

章节减少者

加注 import reduce from 'lodash/reduce'; import { combineReducers } from 'redux'; const defaultState = { isFetching: false, isInvalidated: true, id: null, errors: [], }; export default combineReducers({ isInvalidated, isFetching, items, errors, }); function isInvalidated(state = true, action) { switch (action.type) { case 'invalidateChapters': return true; case 'setChapters': return false; default: return state; } } function isFetching(state = false, action) { switch (action.type) { case 'readChapters': return true; case 'setChapters': return false; default: return state; } } function items(state = {}, action) { switch (action.type) { case 'setBooks': return { ...state, ...reduce(action.data, (result, value, key) => ({ ...result, ...reduce(value.references, (res, chapterKey) => ({ ...res, [chapterKey]: chapter({ ...defaultState, id: chapterKey, bookId: value.id }, action), }), {}), }), {}); }; case 'readChapter': { if (action.id && !state[action.id]) { return { ...state, [action.id]: book(undefined, action), }; } return state; } case 'setChapters': return { ...state, ...reduce(action.data, (result, value, key) => ({ ...result, [key]: chapter(value, action), }), {}); }, default: return state; } } function chapter(state = { ...defaultState }, action) { switch (action.type) { case 'readChapters': return { ...state, isFetching: true }; case 'setChapters': return { ...state, isInvalidated: false, isFetching: false, errors: [], }; default: return state; } } function errors(state = [], action) { switch (action.type) { case 'addChaptersError': return [ ...state, action.error, ]; case 'setChapters': case 'setChapters': return state.length > 0 ? [] : state; default: return state; } } -,这将启动您的Reducer中的章节。

df.columns=[x.replace('-','') for x in list(df)]

希望它有所帮助。