Оставаться сухим с помощью асинхронных вызовов в приложении React Redux с Redux Thunk - PullRequest
0 голосов
/ 12 января 2019

Выпуск

В настоящее время у меня проблема с тем, как я хочу выполнить запрос AJAX, который может быть вызван несколькими различные элементы интерфейса на странице. AJAX-запрос всегда отправляется в одну и ту же конечную точку и всегда отправляет одни и те же свойства. из избыточного хранилища в конечную точку (хотя значения свойств могут измениться из-за взаимодействия с пользователем).

Я полностью осознаю, что моя текущая реализация ужасна.

1007 * Сводка * Чтобы нарисовать более четкую картинку, я создаю страницу поиска, где несколько элементов пользовательского интерфейса могут инициировать активацию нового поиска. Есть одна конечная точка, давайте назовем ее «/ api / search /», которая ожидает строку запроса с данными, извлеченными из Redux Store. Это будет выглядеть примерно так: term=some%20string&categories=243,2968,292&tags=11,25,99&articleType=All Я получаю путаницу, когда элемент пользовательского интерфейса должен инициировать синхронное обновление хранилища, но также должен запускать Thunk, который выполняет поиск. Вот мой компонент верхнего уровня, где я передаю Thunk-функцию executeSearch всем дочерним компонентам, которые должны инициировать поиск. Сначала я думал, что могу использовать один блок для обработки всех взаимодействий, требующих выполнения поиска, вместо того, чтобы писать отдельный блок для каждого из них. 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 принимает все значения из хранилища, но использует только те, которые я описал в начале этой проблемы. Внизу этого поста приведен пример кода всего магазина редуксов, если это поможет. Несмотря на это, вот как выглядит 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 возникает и здесь. Если щелкнуть одну из кнопок («Категория» или «Тег») в разделе «Уточнить», это раскрывающееся меню отображается с несколькими флажками, которые можно выбрать. Я фактически сохраняю, какие флажки становятся отмеченными / не отмеченными в локальном состоянии, пока пользователь не нажмет кнопку «Сохранить». Как только кнопка сохранения нажата, вновь отмеченные / непроверенные значения флажка должны быть обновлены в хранилище, а затем новый поиск должен быть инициирован через Thunk, переданный из App.js.

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. Возможно, я просто делаю что-то в корне неправильно.

Разное

Вот как выглядит мой полный магазин (в исходном состоянии), если он помогает:

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

Ответы [ 2 ]

0 голосов
/ 14 января 2019

Решение 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 в любой нужный мне компонент, связанный с избыточностью, и просто убедиться, что он выполняется. Например:

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

Это делает ваши действия создателей чистыми и очень простыми для размышления. Всякий раз, когда это действие отправляется в какой-либо части моего пользовательского интерфейса, моя сага подхватывает это и выполняет поиск с самой последней версией магазина.

Вот сага, которая заставляет его работать:

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);
}
0 голосов
/ 12 января 2019

Следующий блок в 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, и сага сможет передать обновленный store в executeSearch .

...