在具有Redux Thunk的React Redux应用程序中保持异步调用的DRY

时间:2019-01-12 14:25:16

标签: javascript reactjs redux redux-thunk

问题

我目前在如何执行AJAX请求方面遇到问题,该请求可能由多个触发 页面上的不同UI元素。 AJAX请求始终转到相同的端点,并始终发送相同的属性 从redux存储到端点(尽管属性值可能会由于用户交互而更改)。

我完全意识到我目前的执行情况很糟糕。

运行失败

为了绘制更清晰的图片,我正在构建一个搜索页面,其中多个UI元素可以触发新的搜索被激活。 有一个端点称为“ / api / search /”,该端点期望查询字符串包含从Redux Store中提取的数据。看起来像这样:

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

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

P.S。如果对您不敏感,请不要过度分析以下代码。如果您浏览了以下部分,请阅读“三种情况”部分,这可能有助于您更好地理解所有工作原理。图片也包含在该部分中。

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函数内部使用所需的属性。

三种情况

我提供了一个屏幕快照(标有字母),希望在其中解释在什么地方有效,在哪些地方无效。

enter image description here

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的相同问题。当您单击“ Refine by”部分中的按钮之一(类别或标签)时,将显示此下拉列表并带有多个复选框。我实际上存储了哪些复选框在本地状态下被选中/取消选中,直到用户单击“保存”按钮。按下保存按钮后,应在商店中更新新选中/未选中的复选框值,然后应通过从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
};

2 个答案:

答案 0 :(得分:1)

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

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

executeSearch方法在渲染store时已将App引入其中。

当您这样做时:

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

executeSearch时,您的Redux存储将被同步更新,但这会导致一个新的store对象,这将导致App重新呈现,但是不会影响store正在使用的executeSearch对象。

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

答案 1 :(得分:0)

使用Ryan Cogswell解决方案非常出色。我能够删除所有使事情变得混乱的代码,并且不再需要担心将某些状态从商店传递给我的异步操作创建者,因为我现在正在使用Redux Saga。

我重构的App.js现在看起来像:

class App extends Component {
  render() {
    const {
      updateSearchTerm,
      clearAll,
      dropdownActive,
      dropdownType,
      fetchSearchResults
    } = 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 fetchSearchResults={fetchSearchResults} updateSearchTerm={updateSearchTerm} />
          <ExtraOptions clearAll={clearAll} />
          <Filters />
        </div>

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

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

我要做的就是将fetchSearchResults操作创建者导入到我需要的任何Redux连接的组件中,并确保它已执行。例如:

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

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

这是动作创建者现在的样子...

export const fetchSearchResults = () => ({ type: FETCH_SEARCH_RESULTS });

这使您的动作创作者可以纯粹而又超级简单地进行推理。每当在我的UI的任何部分中分派此操作时,我的传奇都会选择此操作,并使用商店的最新版本进行搜索。

这是使它起作用的传奇:

import { select, takeLatest, call, put } from 'redux-saga/effects';
import { getSearchCriteria } from '../selectors';
import { Api, craftSearchQueryUrl } from '../utils';
import {
  FETCH_SEARCH_RESULTS,
  FETCH_SEARCH_RESULTS_SUCCESS,
  FETCH_SEARCH_RESULTS_FAILED
} from '../actions';

function* fetchSearchResults() {
  const criteria = yield select(getSearchCriteria);

  try {
    const { data } = yield call(Api.get, craftSearchQueryUrl(criteria));
    yield put({ type: FETCH_SEARCH_RESULTS_SUCCESS, searchResults: data });
  } catch (err) {
    yield put({ type: FETCH_SEARCH_RESULTS_FAILED, error: err });
  }
}

export default function* searchResults() {
  yield takeLatest(FETCH_SEARCH_RESULTS, fetchSearchResults);
}