首页 > 解决方案 > 使用 Redux Thunk 在 React Redux 应用程序中使用异步调用保持 DRY

问题描述

问题

我目前遇到一个问题,我想如何执行可由页面上的几个不同 UI 元素触发的 AJAX 请求。AJAX 请求总是发送到同一个端点,并且总是将相同的属性从 redux 存储发送到端点(尽管属性值可能会由于用户交互而改变)。

我完全意识到我目前的实施很糟糕。

撞倒

为了画得更清楚,我正在构建一个搜索页面,其中几个 UI 元素可以触发一个新的搜索被激活。有一个端点,我们称之为“/api/search/”,它需要一个查询字符串,其中包含从 Redux 存储中提取的数据。它看起来像这样:

term=some%20string&categories=243,2968,292&tags=11,25,99&articleType=All

我被绊倒的地方是当 UI 元素需要触发对商店的同步更新,但还需要触发执行搜索的 Thunk 时。这是我的顶级组件,我将“executeSearch”Thunk 函数传递给所有需要触发搜索的子组件。我最初的想法是,我可以使用一个 thunk 来处理所有需要执行搜索的交互,而不是为每个交互编写一个单独的 thunk。

PS如果对您没有意义,请不要过度分析下面的代码。如果您浏览以下部分,然后阅读“三个场景”部分,这可能有助于您更好地理解一切是如何工作的。图片也包含在该部分中。

class App extends Component {
  executeSearch = () => {
    this.props.executeSearch(this.props.store); // This is my Thunk
  };

  render() {
    const { updateSearchTerm, clearAll, dropdownActive, dropdownType } = this.props;

    return (
      <section className="standard-content">
        <div className="inner-container narrow">
          <div className="centered">
            <h1>Search</h1>
            <h2 className="alt">Search our extensive database of research articles.</h2>
          </div>

          <SearchTerm initSearch={this.executeSearch} updateSearchTerm={updateSearchTerm} />
          <ExtraOptions clearAll={clearAll} />
          <Filters executeSearch={this.executeSearch} />
        </div>

        {dropdownActive ? (
          dropdownType === 'categories' ? (
            <CategoryDropdown executeSearch={this.executeSearch} />
          ) : (
            <TagDropdown executeSearch={this.executeSearch} />
          )
        ) : null}

        <SearchResults />
      </section>
    );
  }
}

const mapStateToProps = state => {
  return {
    store: state,
    dropdownActive: state.dropdownActive,
    dropdownType: state.dropdownType
  };
};

executeSearch 函数从存储中获取所有值,但仅使用我在本期开头概述的值。如果有帮助,本文底部有整个 redux 存储的代码示例。无论如何,这就是 Thunk 的样子:

export const executeSearch = criteria => {
  const searchQueryUrl = craftSearchQueryUrl(criteria);

  // if (term === '' && !selectedCategories && !selectedTags && articleType === 'All') {
  //   return { type: ABORT_SEARCH };
  // }

  return async dispatch => {
    dispatch({ type: FETCH_SEARCH_RESULTS });

    try {
      const res = await axios.post(`${window.siteUrl}api/search`, searchQueryUrl);
      dispatch({ type: FETCH_SEARCH_RESULTS_SUCCESS, searchResults: res.data });
    } catch (err) {
      dispatch({ type: FETCH_SEARCH_RESULTS_FAILED });
    }
  };
};

// Helper function to craft a proper search query string
const craftSearchQueryUrl = criteria => {
  const { term, articleType, selectedCategories, selectedTags } = criteria;
  let categoriesString = selectedCategories.join(',');
  let tagsString = selectedTags.join(',');

  return `term=${term}&articleType=${articleType}&categories=${categoriesString}&tags=${tagsString}&offset=${offset}`;
};

请记住,这里的“条件”参数是我在 App.js 中作为参数传递的整个商店对象。您会看到我只使用了 craftSearchQueryUrl 函数内部需要的属性。

三种场景

我已经附上了一个截图(标有字母),我希望在其中解释哪些事情有效,哪些事情不做。

在此处输入图像描述

A.) 用户应该能够填写此文本字段,并且当他们按下放大镜时,它应该会触发 Thunk。这很好用,因为文本字段中的值在每次击键时都会在存储中更新,这意味着存储中的值在用户甚至有机会按下放大镜之前始终是最新的。

B.) 默认情况下,在初始页面加载时选中“全部”复选框。如果用户单击旁边列出的其他复选框之一,则应立即启动搜索。这是我的问题开始出现的地方。这是我目前拥有的代码:

export default ({ handleCheckboxChange, articleType, executeSearch }) => (
  <div style={{ marginTop: '15px', marginBottom: '20px' }}>
    <span className="search-title">Type: </span>

    {articleTypes.map(type => (
      <Checkbox
        key={type}
        type={type}
        handleCheckboxChange={() => {
          handleCheckboxChange('articleType', { type });
          executeSearch();
        }}
        isChecked={type === articleType}
      />
    ))}
  </div>
);

当复选框更改时,它会更新商店中的 articleType 值(通过 handleCheckboxChange),然后说执行从 App.js 传递下来的搜索功能。但是,更新后的 articleValue 类型不是更新的类型,因为我相信在商店有机会更新此值之前调用了搜索功能。

C.) 来自 B 的同样问题也发生在这里。当您单击“优化依据”部分中的一个按钮(类别或标签)时,此下拉菜单会出现多个可供选择的复选框。我实际上存储了哪些复选框在本地状态下被选中/取消选中,直到用户单击保存按钮。按下保存按钮后,应在商店中更新新选中/未选中的复选框值,然后应通过从 App.js 传递的 Thunk 启动新的搜索。

export default ({ children, toggleDropdown, handleCheckboxChange, refineBy, executeSearch }) => {
  const handleSave = () => {
    handleCheckboxChange(refineBy.classification, refineBy);
    toggleDropdown('close');
    executeSearch(); // None of the checkbox values that were changed are reflected when the search executes
  };

  return (
    <div className="faux-dropdown">
      <button className="close-dropdown" onClick={() => toggleDropdown('close')}>
        <span>X</span>
      </button>
      <div className="checks">
        <div className="row">{children}</div>
      </div>

      <div className="filter-selection">
        <button className="refine-save" onClick={handleSave}>
          Save
        </button>
      </div>
    </div>
  );
};

需要注意的是,执行搜索时使用的值不是 B 和 C 的更新值,但它们实际上在存储中正确更新。

其他解决方案

我的另一个想法是创建一个 redux 中间件,但老实说,在尝试其他任何事情之前,我真的可以在这方面使用一些专家帮助。理想情况下,被接受的解决方案的解释应该是彻底的,并且包括一个在处理 Redux 应用程序时考虑到最佳架构实践的解决方案。也许我只是在这里做一些根本错误的事情。

杂项

如果有帮助,这就是我的完整商店(处于初始状态)的样子:

const initialState = {
  term: '',
  articleType: 'All',
  totalJournalArticles: 0,
  categories: [],
  selectedCategories: [],
  tags: [],
  selectedTags: [],
  searchResults: [],
  offset: 0,
  dropdownActive: false,
  dropdownType: '',
  isFetching: false
};

标签: javascriptreactjsreduxredux-thunk

解决方案


以下块App是问题的症结所在:

  executeSearch = () => {
    this.props.executeSearch(this.props.store); // This is my Thunk
  };

这种方法在渲染时executeSearch已经融入其中。storeApp

当你这样做时:

  handleCheckboxChange('articleType', { type });
  executeSearch();

当你到达executeSearch你的 Redux 存储时,将同步更新,但这会产生一个新store对象,这将导致App重新渲染,但不会影响正在使用的store对象executeSearch

正如其他人在评论中指出的那样,我认为最直接的处理方法是使用中间件,该中间件提供了一种机制来执行副作用以响应商店更新后的 redux 操作。为此,我个人会推荐 Redux Saga,但我知道还有其他选择。在这种情况下,您将有一个 saga 监视应该触发搜索的任何操作,然后 saga 是唯一调用的东西,executeSearch并且 saga 将能够将更新的内容传递storeexecuteSearch.


推荐阅读