在基于动作/减少器的应用程序中处理多个异步调用的加载状态 [英] Handling loading state of multiple async calls in an action/reducer based application

查看:60
本文介绍了在基于动作/减少器的应用程序中处理多个异步调用的加载状态的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I don´t think this issue is bound to a specific framework or library, but applies to all store based application following the action - reducer pattern.

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

For clarity, I am using Angular and @ngrx.

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

In the application I am working on we need to track the loading state of individual resources.

我们希望通过其他方式来处理其他异步请求:

The way we handle other async requests is by this, hopefully familiar, pattern:

操作

  • 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
    };
  ...

这对于我们要在应用程序中全局指示加载状态的异步调用非常有用.

This works well for async calls where we want to indicate the loading state globally in our application.

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

In our application we fetch some data, say BOOKS, that contains a list of references to other resources, say CHAPTERS. If the user wants to view a CHAPTER he/she clicks the CHAPTER reference that trigger an async call. To indicate to the user that this specific CHAPTER is loading, we need something more than just a global isLoading flag in our state.

我们解决此问题的方法是创建一个包装对象,如下所示:

The way we have solved this is by creating a wrapping object like this:

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

其中AsyncStatus是这样的枚举:

where AsyncStatus is an enum like this:

enum AsyncStatus {
  InFlight,
  Success,
  Error
}

在我们的状态下,我们像这样存储章节":

In our state we store the CHAPTERS like so:

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

但是,我觉得这种状态有些混乱,想知道是否有人对这个问题有更好的解决方案/不同的方法.

However, I feel like this 'clutter' the state in a way and wonder if someone has a better solution / different approach to this problem.

问题

  • 是否有最佳实践来应对这种情况?
  • 有没有更好的方法来处理这个问题?

推荐答案

我曾多次遇到这种情况,但解决方案因使用案例而异.

I have faced several times this kind of situation but the solution differs according to the use case.

解决方案之一是嵌套嵌套化径器.它不是反模式,但不建议使用,因为它很难维护,但取决于用例.

One of the solution would be to have nested reducers. It is not an antipattern but not advised because it is hard to maintain but it depends on the usecase.

另一个是我在下面详细介绍的一个.

The other one would be the one I detail below.

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

Based on what you described, your fetched data should look like this:

  [
    {
      id: 1,
      title: 'Robinson Crusoe',
      author: 'Daniel Defoe',
      references: ['chp1_robincrusoe', 'chp2_robincrusoe'],
    },
    {
      id: 2,
      title: 'Gullivers Travels',
      author: 'Jonathan Swift',
      references: ['chp1_gulliverstravels', 'chp2_gulliverstravels', 'chp3_gulliverstravels'],
    },
  ]

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

So according to your data, your reducers should look like this:

  {
    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 },
      },
    }
  }

采用这种结构,由于每一章都是独立的逻辑,因此您无需在章缩减器中使用isFetchingisInvalidated.

With this structure you won't need isFetching and isInvalidated in your chapter reducers as every chapter is a separated logic.

注意:我可以在以后为您提供有关如何以不同方式利用isFetchingisInvalidated的详细信息.

Note: I could give you a bonus details later on on how we can leverage the isFetching and isInvalidated in a different way.

下面的详细代码:

组件

BookList

  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);

章节

  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;
    }
  }

减速章

setBooks上要格外注意,这将使减速器中的各章生效.

Pay extra attention on setBooks which will init the chapters in your reducers.

  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;
    }
  }

希望有帮助.

这篇关于在基于动作/减少器的应用程序中处理多个异步调用的加载状态的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆