gpt4 book ai didi

redux - 在基于 Action /reducer 的应用程序中处理多个异步调用的加载状态

转载 作者:行者123 更新时间:2023-12-05 07:39:37 25 4
gpt4 key购买 nike

我不认为这个问题与特定的框架或库有关,但适用于遵循 action - reducer 模式的所有基于商店的应用程序。

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

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

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

Action

  • GET_RESOURCE
  • GET_RESOURCE_SUCCESS
  • GET_RESOURCE_FAILURE

reducer

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

但是,我觉得这在某种程度上“困惑”了状态,想知道是否有人有更好的解决方案/不同的方法来解决这个问题。

问题

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

最佳答案

我遇到过几次这种情况,但解决方案因用例而异。

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

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

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

  [
{
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'],
},
]

因此根据您的数据,您的 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 },
},
}
}

使用这种结构,您的章节缩减器中将不需要 isFetchingisInvalidated,因为每一章都是一个单独的逻辑。

注意:稍后我可以为您提供额外的详细信息,说明我们如何以不同的方式利用 isFetchingisInvalidated

详细代码如下:


组件

书单

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

Chapter Reducers

特别注意setBooks,它将在您的 reducer 中初始化章节。

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

希望对您有所帮助。

关于redux - 在基于 Action /reducer 的应用程序中处理多个异步调用的加载状态,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46974980/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com