Зачем нам промежуточное ПО для асинхронного потока в Redux?


687

Согласно документации, «без промежуточного программного обеспечения хранилище Redux поддерживает только синхронный поток данных» . Я не понимаю, почему это так. Почему компонент контейнера не может вызвать асинхронный API, а затем dispatchдействия?

Например, представьте себе простой интерфейс: поле и кнопку. Когда пользователь нажимает кнопку, поле заполняется данными с удаленного сервера.

Поле и кнопка

import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';

const ActionTypes = {
    STARTED_UPDATING: 'STARTED_UPDATING',
    UPDATED: 'UPDATED'
};

class AsyncApi {
    static getFieldValue() {
        const promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(Math.floor(Math.random() * 100));
            }, 1000);
        });
        return promise;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && <div>Waiting...</div>}
            </div>
        );
    }
}
App.propTypes = {
    dispatch: React.PropTypes.func,
    field: React.PropTypes.any,
    isWaiting: React.PropTypes.bool
};

const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
    switch (action.type) {
        case ActionTypes.STARTED_UPDATING:
            return { ...state, isWaiting: true };
        case ActionTypes.UPDATED:
            return { ...state, isWaiting: false, field: action.payload };
        default:
            return state;
    }
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
    (state) => {
        return { ...state };
    },
    (dispatch) => {
        return {
            update: () => {
                dispatch({
                    type: ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    .then(result => dispatch({
                        type: ActionTypes.UPDATED,
                        payload: result
                    }));
            }
        };
    })(App);
export default class extends React.Component {
    render() {
        return <Provider store={store}><ConnectedApp/></Provider>;
    }
}

Когда экспортированный компонент отображается, я могу нажать на кнопку, и ввод корректно обновляется.

Обратите внимание на updateфункцию в connectвызове. Он отправляет действие, которое сообщает приложению об обновлении, а затем выполняет асинхронный вызов. После завершения вызова предоставленное значение отправляется как полезная нагрузка другого действия.

Что не так с этим подходом? Почему я хотел бы использовать Redux Thunk или Redux Promise, как следует из документации?

РЕДАКТИРОВАТЬ: Я искал в репозитории Redux подсказки, и обнаружил, что создатели действий должны были быть чистыми функциями в прошлом. Например, вот пользователь, пытающийся дать лучшее объяснение асинхронного потока данных:

Сам создатель действия все еще является чистой функцией, но возвращаемая им функция thunk не обязательна, и он может выполнять наши асинхронные вызовы.

Создатели действий больше не обязаны быть чистыми. Итак, промежуточное программное обеспечение Thunk / обещание определенно требовалось в прошлом, но кажется, что это больше не так?


53
Создатели действий никогда не должны были быть чистыми функциями. Это была ошибка в документах, а не решение, которое изменилось.
Дан Абрамов

1
@DanAbramov для тестируемости это может быть хорошей практикой однако. Redux-saga разрешает это: stackoverflow.com/a/34623840/82609
Себастьян Лорбер

Ответы:


702

Что не так с этим подходом? Почему я хотел бы использовать Redux Thunk или Redux Promise, как следует из документации?

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

Вы можете прочитать мой ответ на «Как отправить действие Redux с тайм-аутом» для более подробного ознакомления.

Промежуточное программное обеспечение, такое как Redux Thunk или Redux Promise, просто дает вам «синтаксический сахар» для отправки thunks или обещаний, но вам не нужно его использовать.

Таким образом, без какого-либо промежуточного программного обеспечения ваш создатель действий может выглядеть

// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}

Но с Thunk Middleware вы можете написать это так:

// action creator
function loadData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}

Так что нет большой разницы. Что мне нравится в последнем подходе, так это то, что компоненту не важно, что создатель действий является асинхронным. Он просто вызывает dispatchнормально, его также можно использовать mapDispatchToPropsдля связывания таких создателей действий с коротким синтаксисом и т. Д. Компоненты не знают, как реализованы создатели действий, и вы можете переключаться между различными асинхронными подходами (Redux Thunk, Redux Promise, Redux Saga ) без замены компонентов. С другой стороны, при первом, явном подходе ваши компоненты точно знают , что конкретный вызов является асинхронным, и его необходимо dispatchпередать по некоторому соглашению (например, в качестве параметра синхронизации).

Также подумайте, как изменится этот код. Скажем, мы хотим иметь вторую функцию загрузки данных и объединить их в одном создателе действий.

При первом подходе мы должны помнить о том, какого типа создателя действий мы называем:

// action creators
function loadSomeData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(dispatch, userId) {
  return Promise.all(
    loadSomeData(dispatch, userId), // pass dispatch first: it's async
    loadOtherData(dispatch, userId) // pass dispatch first: it's async
  );
}


// component
componentWillMount() {
  loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}

С Redux Thunk создатели действий могут стать dispatchрезультатом действий других создателей и даже не думать, являются ли они синхронными или асинхронными:

// action creators
function loadSomeData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(userId) {
  return dispatch => Promise.all(
    dispatch(loadSomeData(userId)), // just dispatch normally!
    dispatch(loadOtherData(userId)) // just dispatch normally!
  );
}


// component
componentWillMount() {
  this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}

При таком подходе, если вы позже захотите, чтобы создатели ваших действий посмотрели на текущее состояние Redux, вы можете просто использовать второй getStateаргумент, передаваемый thunks, вообще не изменяя вызывающий код:

function loadSomeData(userId) {
  // Thanks to Redux Thunk I can use getState() here without changing callers
  return (dispatch, getState) => {
    if (getState().data[userId].isLoaded) {
      return Promise.resolve();
    }

    fetch(`http://data.com/${userId}`)
      .then(res => res.json())
      .then(
        data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
        err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
      );
  }
}

Если вам нужно изменить его на синхронный, вы также можете сделать это без изменения какого-либо вызывающего кода:

// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
  return {
    type: 'LOAD_SOME_DATA_SUCCESS',
    data: localStorage.getItem('my-data')
  }
}

Таким образом, преимущество использования промежуточного программного обеспечения, такого как Redux Thunk или Redux Promise, состоит в том, что компоненты не знают о том, как реализованы создатели действий, и заботятся ли они о состоянии Redux, являются ли они синхронными или асинхронными, а также вызывают ли они других создателей действий. , Недостатком является некоторая косвенность, но мы считаем, что это стоит в реальных приложениях.

Наконец, Redux Thunk и друзья - это только один из возможных подходов к асинхронным запросам в приложениях Redux. Другим интересным подходом является Redux Saga, который позволяет вам определять длительно работающих демонов («саг»), которые выполняют действия по мере их поступления, а также преобразуют или выполняют запросы перед выводом действий. Это перемещает логику от создателей действий в саги. Вы можете проверить это, а потом выбрать то, что подходит вам больше всего.

Я искал подсказки в репозитории Redux и обнаружил, что в прошлом создатели действий должны были быть чистыми функциями.

Это неверно Документы сказали это, но документы были не правы.
Создатели действий никогда не должны были быть чистыми функциями.
Мы исправили документы, чтобы отразить это.


57
Возможно, краткий способ выразить мысль Дэна: промежуточное ПО - это централизованный подход, который позволяет вам упростить и обобщить ваши компоненты и контролировать поток данных в одном месте. Если вы поддерживаете большое приложение, оно вам понравится =)
Сергей Лапин

3
@asdfasdfads Я не понимаю, почему это не сработает. Это будет работать точно так же; положить alertпосле dispatch()действия.
Дан Абрамов

9
Предпоследний строка в самом первом примере кода: loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch. Почему я должен пройти отправку? Если по соглашению существует только один глобальный магазин, почему бы мне просто не ссылаться на это напрямую и не делать это store.dispatchвсякий раз, когда мне нужно, например, в loadData?
Сорен Дебойс

10
@ SørenDebois Если ваше приложение работает только на стороне клиента, это будет работать. Если он отображается на сервере, вам нужно иметь разные storeэкземпляры для каждого запроса, поэтому вы не можете определить его заранее.
Дан Абрамов

3
Просто хочу отметить, что этот ответ содержит 139 строк, что в 9,92 раза больше, чем исходный код redux-thunk, который состоит из 14 строк: github.com/gaearon/redux-thunk/blob/master/src/index.js
Парень

447

Вы не

Но ... вы должны использовать Redx-сагу :)

Ответ Дана Абрамова верен, redux-thunkно я расскажу немного больше о сундук-реду, который очень похож, но более силен.

Повелительный указ декларативный

  • ДОМ : JQuery является обязательным / React является декларативным
  • Монады : IO является обязательным / Free является декларативным
  • Redux эффекты : redux-thunkобязательно / redux-sagaдекларативно

Когда у вас в руках есть Thunk, например IO-монада или обещание, вы не можете легко знать, что он будет делать после выполнения. Единственный способ проверить thunk - это выполнить его и высмеять диспетчера (или весь внешний мир, если он взаимодействует с большим количеством материала ...).

Если вы используете макеты, то вы не занимаетесь функциональным программированием.

Сквозь призму побочных эффектов макеты - это признак того, что ваш код нечист, и, на взгляд функционального программиста, доказательство того, что что-то не так. Вместо того, чтобы загружать библиотеку, чтобы помочь нам проверить, не поврежден ли айсберг, мы должны плыть вокруг него. Один хардкорный TDD / Java парень как-то спросил меня, как ты делаешь насмешки в Clojure. Ответ, мы обычно не делаем. Обычно мы видим в этом знак того, что нам нужно реорганизовать наш код.

Источник

Саги (в том виде, в redux-sagaкаком они были реализованы ) являются декларативными и, подобно компонентам Free monad или React, их гораздо проще тестировать без всяких насмешек.

Смотрите также эту статью :

в современных FP мы не должны писать программы - мы должны писать описания программ, которые мы можем затем анализировать, трансформировать и интерпретировать по желанию.

(На самом деле Redux-сага похожа на гибрид: поток обязателен, но эффекты декларативны)

Путаница: действия / события / команды ...

В мире внешнего интерфейса существует большая путаница в отношении того, как могут быть связаны некоторые базовые концепции, такие как CQRS / EventSourcing и Flux / Redux, главным образом потому, что в Flux мы используем термин «действие», который иногда может представлять как императивный код ( LOAD_USER), так и события ( USER_LOADED). Я считаю, что, как и в случае с источником событий, вы должны только отправлять события.

Использование саг на практике

Представьте себе приложение со ссылкой на профиль пользователя. Идиоматический способ справиться с этим с каждым промежуточным программным обеспечением:

redux-thunk

<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>

function loadUserProfile(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
      err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
    );
}

redux-saga

<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>


function* loadUserProfileOnNameClick() {
  yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}

function* fetchUser(action) {
  try {
    const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
    yield put({ type: 'USER_PROFILE_LOADED', userProfile })
  } 
  catch(err) {
    yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
  }
}

Эта сага переводится как:

каждый раз, когда по имени пользователя нажимают, выбирают профиль пользователя и затем отправляют событие с загруженным профилем.

Как видите, есть некоторые преимущества redux-saga.

Использование takeLatestразрешений для выражения того, что вас интересует только получение данных о последнем щелчке имени пользователя (устранение проблем параллелизма в случае, если пользователь очень быстро нажимает на большое количество имен пользователей). Такого рода вещи сложны с громилой. Вы могли бы использовать, takeEveryесли вы не хотите этого поведения.

Вы держите создателей действий в чистоте. Обратите внимание, что все еще полезно сохранять actionCreators (в сагах putи компонентах dispatch), поскольку это может помочь вам добавить проверку действия (assertions / flow / typescript) в будущем.

Ваш код становится намного более тестируемым, так как эффекты декларативны

Вам больше не нужно инициировать вызовы, похожие на rpc actions.loadUser(). Ваш пользовательский интерфейс просто должен отправить то, что произошло. Мы только запускаем события (всегда в прошедшем времени!), А не действия больше. Это означает, что вы можете создавать развязанные «утки» или ограниченные контексты и что сага может выступать в качестве связующего звена между этими модульными компонентами.

Это означает, что вашими представлениями легче управлять, потому что им больше не нужно содержать слой перевода между тем, что произошло, и тем, что должно произойти как результат.

Например, представьте бесконечный вид прокрутки. CONTAINER_SCROLLEDможет привести к тому NEXT_PAGE_LOADED, но действительно ли прокручиваемый контейнер несет ответственность за решение, следует ли нам загружать другую страницу? Затем он должен знать о более сложных вещах, таких как, была ли успешно загружена последняя страница, или уже есть страница, которая пытается загрузить, или нет больше элементов для загрузки? Я так не думаю: для максимального повторного использования прокручиваемый контейнер должен просто описывать, что он был прокручен. Загрузка страницы является «бизнес-эффектом» этого свитка

Кто-то может возразить, что генераторы могут скрывать состояние за пределами хранилища избыточных данных с помощью локальных переменных, но если вы начнете организовывать сложные вещи внутри thunks, запуская таймеры и т.д., у вас все равно будет та же проблема. И есть selectэффект, который теперь позволяет получить состояние из вашего магазина Redux.

Sagas позволяет путешествовать во времени, а также обеспечивает сложную регистрацию потоков и инструменты разработки, над которыми в настоящее время ведутся работы. Вот некоторая простая асинхронная регистрация потока, которая уже реализована:

регистрация саги

Развязка

Саги не только заменяют редуксных сундуков. Они поступают из бэкэнда / распределенных систем / источников событий.

Это очень распространенное заблуждение, что саги только здесь, чтобы заменить ваши редуксные гирлянды лучшей тестируемостью. На самом деле, это всего лишь деталь реализации Redx-саги. Использование декларативных эффектов лучше, чем thunk для тестируемости, но шаблон саги может быть реализован поверх императивного или декларативного кода.

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

Чтобы упростить это для внешнего мира, представьте, что есть widget1 и widget2. Когда нажата какая-то кнопка на widget1, это должно повлиять на widget2. Вместо того, чтобы соединить 2 виджета вместе (т.е. widget1 отправляет действие, которое нацелено на widget2), widget1 отправляет только то, что была нажата его кнопка. Затем сага прослушивает нажатие этой кнопки, а затем обновляет widget2, отправляя новое событие, о котором известно widget2.

Это добавляет уровень косвенности, который не нужен для простых приложений, но упрощает масштабирование сложных приложений. Теперь вы можете публиковать widget1 и widget2 в разных репозиториях npm, чтобы им никогда не приходилось узнавать друг о друге, не предоставляя им общий глобальный реестр действий. 2 виджета теперь являются ограниченными контекстами, которые могут жить отдельно. Они не нуждаются друг в друге, чтобы быть последовательными и могут быть повторно использованы в других приложениях. Сага является связующим звеном между двумя виджетами, которые значимым образом координируют их для вашего бизнеса.

Несколько хороших статей о том, как структурировать ваше приложение Redux, в котором вы можете использовать Redux-saga по причинам разделения:

Конкретный вариант использования: система уведомлений

Я хочу, чтобы мои компоненты могли запускать отображение уведомлений в приложении. Но я не хочу, чтобы мои компоненты были тесно связаны с системой уведомлений, которая имеет свои собственные бизнес-правила (макс. 3 уведомления, отображаемые одновременно, очередь уведомлений, время отображения 4 секунды и т. Д.).

Я не хочу, чтобы мои компоненты JSX решали, когда будет отображаться / скрываться уведомление. Я просто даю ему возможность запросить уведомление и оставить сложные правила внутри саги. Подобные вещи довольно сложно реализовать с помощью благих обещаний.

уведомления

Я описал здесь, как это можно сделать с помощью саги

Почему это называется сага?

Термин сага происходит из бэкэнд-мира. Я первоначально представил Yassine (автора Redux-saga) этому термину в длительной дискуссии .

Первоначально этот термин был введен в статье. Предполагалось, что шаблон saga будет использоваться для обработки возможной согласованности в распределенных транзакциях, но его использование было расширено до более широкого определения бэкэнд-разработчиками, так что теперь оно также охватывает «менеджер процессов». шаблон (каким-то образом оригинальный шаблон саги является специализированной формой менеджера процессов).

Сегодня термин «сага» сбивает с толку, поскольку он может описывать 2 разные вещи. Поскольку он используется в redux-saga, он описывает не способ обработки распределенных транзакций, а способ координации действий в вашем приложении. redux-sagaтакже можно было бы назвать redux-process-manager.

Смотрите также:

альтернативы

Если вам не нравится идея использования генераторов, но вас интересует шаблон саги и его свойства развязки, вы также можете добиться того же с помощью redux-observable, которое использует имя epicдля описания точно такого же шаблона, но с RxJS. Если вы уже знакомы с Rx, вы будете чувствовать себя как дома.

const loadUserProfileOnNameClickEpic = action$ =>
  action$.ofType('USER_NAME_CLICKED')
    .switchMap(action =>
      Observable.ajax(`http://data.com/${action.payload.userId}`)
        .map(userProfile => ({
          type: 'USER_PROFILE_LOADED',
          userProfile
        }))
        .catch(err => Observable.of({
          type: 'USER_PROFILE_LOAD_FAILED',
          err
        }))
    );

Некоторые полезные ресурсы редукса-саги

2017 советует

  • Не злоупотребляйте Redux-сагой только ради ее использования. Только тестируемые вызовы API не стоят того.
  • Не удаляйте thunks из вашего проекта для самых простых случаев.
  • Не стесняйтесь отправлять Thunks, yield put(someActionThunk)если это имеет смысл.

Если вы боитесь использовать Redux-saga (или Redux-observable), но вам нужен только шаблон развязки, проверьте redux-dispatch-subscribe : он позволяет прослушивать диспетчеризацию и запускать новые диспетчеры в приемнике.

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});

64
Это становится лучше каждый раз, когда я возвращаюсь. Подумайте о том, чтобы превратить его в блог :).
RainerAtSpirit

4
Спасибо за хорошую запись. Однако я не согласен с некоторыми аспектами. Каким образом LOAD_USER является обязательным? Для меня это не только декларативный - он также дает отличный читаемый код. Как например «Когда я нажимаю эту кнопку, я хочу ADD_ITEM». Я могу посмотреть на код и точно понять, что происходит. Если бы вместо этого это называлось что-то с эффектом «BUTTON_CLICK», мне пришлось бы это искать.
конфетка

4
Хороший ответ. Теперь есть другая альтернатива: github.com/blesh/redux-observable
swennemen

4
@swelet извините за поздний ответ. Когда вы отправляете ADD_ITEM, это обязательно, потому что вы отправляете действие, которое имеет целью повлиять на ваш магазин: вы ожидаете, что действие что-то сделает. Будучи декларативным, придерживайтесь философии поиска событий: вы не отправляете действия, чтобы вызвать изменения в ваших приложениях, но вы отправляете прошлые события, чтобы описать, что произошло в вашем приложении. Отправка события должна быть достаточной, чтобы считать, что состояние приложения изменилось. Тот факт, что есть магазин Redux, который реагирует на событие, является необязательной деталью реализации
Себастьен Лорбер,

3
Мне не нравится этот ответ, потому что он отвлекает от реального вопроса, чтобы продать кому-то собственную библиотеку. Этот ответ обеспечивает сравнение двух библиотек, что не было целью вопроса. Фактический вопрос - спрашивать, стоит ли вообще использовать промежуточное ПО, что объясняется принятым ответом.
Абхинав Манчанда

31

Короткий ответ : мне кажется вполне разумным подход к проблеме асинхронности. С парой предостережений.

У меня была очень похожая точка зрения, когда я работал над новым проектом, который мы только начали на моей работе. Я был большим поклонником элегантной системы Vanilla Redux для обновления хранилища и рендеринга компонентов таким образом, чтобы не выходить за пределы дерева компонентов React. Мне показалось странным подключиться к этому элегантному dispatchмеханизму для обработки асинхронности.

Я закончил тем, что пошел с очень похожим подходом к тому, что у вас есть в библиотеке, которую я учел в нашем проекте, который мы назвали response-redux-controller .

Я закончил тем, что не выбрал именно тот подход, который у вас есть выше, по нескольким причинам:

  1. Как вы пишете, эти диспетчерские функции не имеют доступа к магазину. Вы можете обойти это, передав своим компонентам пользовательского интерфейса всю информацию, необходимую для функции диспетчеризации. Но я бы сказал, что это без необходимости связывает эти компоненты пользовательского интерфейса с логикой диспетчеризации. И что еще более проблематично, у диспетчерской функции нет очевидного способа доступа к обновленному состоянию в асинхронных продолжениях.
  2. Диспетчерские функции имеют доступ к dispatchсебе через лексическую область. Это ограничивает возможности рефакторинга, как только этот connectоператор выходит из-под контроля - и это выглядит довольно громоздким только с одним этим updateметодом. Поэтому вам нужна система, позволяющая вам составлять эти функции диспетчера, если вы разбиваете их на отдельные модули.

Взятые вместе, вы должны настроить некоторую систему, чтобы позволить dispatchи хранилище быть введенным в ваши функции диспетчеризации, наряду с параметрами события. Мне известны три разумных подхода к внедрению этой зависимости:

  • redux-thunk делает это функциональным образом, передавая их в ваш thunks (делая их совсем не thunks, по определению купола). Я не работал с другими dispatchподходами промежуточного программного обеспечения, но я предполагаю, что они в основном одинаковы.
  • Контроллер реагирует на это с сопрограммой. В качестве бонуса он также дает вам доступ к «селекторам», которые являются функциями, которые вы, возможно, передали в качестве первого аргумента connect, вместо того, чтобы работать непосредственно с необработанным нормализованным хранилищем.
  • Вы также можете сделать это объектно-ориентированным способом, внедрив их в thisконтекст, используя множество возможных механизмов.

Обновить

Мне приходит в голову, что часть этой головоломки является ограничением реактивного редукса . Первый аргумент connectполучает снимок состояния, но не диспетчеризацию. Второй аргумент получает отправку, но не состояние. Ни один из аргументов не получает блокировку, закрывающуюся по текущему состоянию, для возможности просмотра обновленного состояния во время продолжения / обратного вызова.


22

Цель Абрамова - и в идеале каждого - состоит в том, чтобы просто инкапсулировать сложность (и асинхронные вызовы) в месте, где это наиболее уместно .

Где лучшее место для этого в стандартном потоке данных Redux? Как насчет:

  • Редукторы ? Ни за что. Они должны быть чистыми функциями без побочных эффектов. Обновление магазина - это серьезное, сложное дело. Не оскверняй это.
  • Тупой вид компонентов? Определенно нет. У них есть одна проблема: представление и взаимодействие с пользователем, и они должны быть максимально простыми.
  • Компоненты контейнера? Возможно, но неоптимально. Это имеет смысл в том, что контейнер - это место, где мы инкапсулируем некоторую сложность, связанную с представлением, и взаимодействуем с магазином, но:
    • Контейнеры должны быть более сложными, чем тупые компоненты, но это все еще одна обязанность: обеспечить привязки между представлением и состоянием / хранилищем. Ваша асинхронная логика - это отдельная проблема.
    • Поместив его в контейнер, вы бы заблокировали свою асинхронную логику в одном контексте для одного представления / маршрута. Плохая идея. В идеале все это можно использовать повторно и полностью отделить.
  • S Ома другого служебного модуля? Плохая идея: вам нужно внедрить доступ к магазину, что является кошмаром по ремонту и проверке. Лучше пойти по пути Redux и получить доступ к магазину только с помощью предоставленных API / моделей.
  • Действия и Middlewares, которые их интерпретируют? Почему бы нет?! Для начала, это единственный важный вариант, который мы оставили. :-) Более логично, что система действий - это разделенная логика выполнения, которую вы можете использовать из любого места. Он имеет доступ к магазину и может отправлять больше действий. Он несет единственную ответственность, которая заключается в организации потока управления и данных вокруг приложения, и большинство асинхронных операций вписывается именно в это.
    • А как насчет создателей действий? Почему бы просто не выполнить асинхронность там, а не в самих действиях и в Middleware?
      • Первое и самое важное, создатели не имеют доступа к магазину, как промежуточное ПО. Это означает, что вы не можете отправлять новые непредвиденные действия, не можете читать из магазина, чтобы составить асинхронный файл и т. Д.
      • Поэтому сохраняйте сложность в сложном месте, а все остальное - просто. Создатели могут быть простыми, относительно чистыми функциями, которые легко тестировать.

Контейнерные компоненты - почему бы и нет? Из-за роли, которую играют компоненты в React, контейнер может выступать в качестве класса обслуживания, и он уже получает хранилище через DI (реквизиты). Поместив его в контейнер, вы бы заблокировали свою асинхронную логику в одном контексте для одного представления / маршрута - как так? Компонент может иметь несколько экземпляров. Его можно отделить от представления, например, с помощью рендера. Я полагаю, что ответ может принести еще больше пользы от коротких примеров, которые подтверждают это.
Настой Эстус

Мне нравится этот ответ!
Маурисио Авенданьо

13

Чтобы ответить на вопрос, который задают в начале:

Почему компонент контейнера не может вызвать асинхронный API, а затем отправить действия?

Имейте в виду, что эти документы предназначены для Redux, а не для Redux плюс React. Хранилища Redux, подключенные к компонентам React, могут делать именно то, что вы говорите, но хранилище Plain Jane Redux без промежуточного программного обеспечения не принимает аргументы, dispatchкроме простых старых объектов.

Без промежуточного программного обеспечения вы, конечно, можете обойтись

const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));

Но подобный случай , когда асинхронность обернута вокруг Redux , а не обрабатываются с помощью Redux. Таким образом, промежуточное ПО допускает асинхронность, изменяя то, что может быть передано напрямую dispatch.


Тем не менее, дух вашего предложения, я думаю, действителен. Конечно, есть и другие способы обработки асинхронности в приложении Redux + React.

Одним из преимуществ использования промежуточного программного обеспечения является то, что вы можете продолжать использовать создателей действий как обычно, не беспокоясь о том, как они подключены. Например, используя redux-thunkкод, который вы написали, будет выглядеть так

function updateThing() {
  return dispatch => {
    dispatch({
      type: ActionTypes.STARTED_UPDATING
    });
    AsyncApi.getFieldValue()
      .then(result => dispatch({
        type: ActionTypes.UPDATED,
        payload: result
      }));
  }
}

const ConnectedApp = connect(
  (state) => { ...state },
  { update: updateThing }
)(App);

который не выглядит так уж сильно отличается от оригинала - он просто немного перемешан - и connectне знает, что updateThingэто (или должно быть) асинхронно.

Если вы также хотели поддерживать обещания , заметки , саги или сумасшедших создателей нестандартных и высоко декларативных действий, тогда Redux может сделать это, просто изменив то, что вы передаете dispatch(то есть то, что вы возвращаете от создателей действия). Нет необходимости в использовании компонентов React (или connectвызовов).


Вы советуете просто отправить еще одно событие по завершении действия. Это не сработает, когда вам нужно показать alert () после завершения действия. Обещания внутри компонентов React работают, хотя. В настоящее время я рекомендую подход Обещания.
катамфетамин

8

Хорошо, давайте начнем с того, как сначала работает промежуточное ПО, что вполне отвечает на вопрос, это исходный код функции pplyMiddleWare в Redux:

function applyMiddleware() {
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }

  return function (createStore) {
    return function (reducer, preloadedState, enhancer) {
      var store = createStore(reducer, preloadedState, enhancer);
      var _dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch(action) {
          return _dispatch(action);
        }
      };
      chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      _dispatch = compose.apply(undefined, chain)(store.dispatch);

      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}

Посмотрите на эту часть, посмотрите, как наша рассылка стала функцией .

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • Обратите внимание , что каждый из промежуточного слоя будет предоставлена dispatchи getStateфункции в качестве именованных аргументов.

Хорошо, вот как Redux-thunk как одно из наиболее часто используемых промежуточных программ для Redux представляет себя:

Промежуточное ПО Redux Thunk позволяет создавать создателей действий, которые возвращают функцию вместо действия. Thunk можно использовать для задержки отправки действия или отправки только при соблюдении определенного условия. Внутренняя функция получает методы хранения dispatch и getState в качестве параметров.

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

Так что, черт возьми, такое? Вот как это представлено в Википедии:

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

Термин возник как шутливая производная от «думать».

Thunk - это функция, которая упаковывает выражение, чтобы задержать его оценку.

//calculation of 1 + 2 is immediate 
//x === 3 
let x = 1 + 2;

//calculation of 1 + 2 is delayed 
//foo can be called later to perform the calculation 
//foo is a thunk! 
let foo = () => 1 + 2;

Посмотрите, насколько проста концепция и как она может помочь вам управлять вашими асинхронными действиями ...

Это то, что вы можете жить без этого, но помните, что в программировании всегда есть лучшие, аккуратные и правильные способы сделать что-то ...

Применить промежуточное программное обеспечение Redux


1
Первый раз на ТАК, ничего не читал. Но просто понравился пост, пристально смотрящий на картинку. Удивительно, подсказка и напоминание.
Бхойендра Раунияр

2

Использовать Redux-saga - лучшее промежуточное ПО в реализации React-redux.

Пример: store.js

  import createSagaMiddleware from 'redux-saga';
  import { createStore, applyMiddleware } from 'redux';
  import allReducer from '../reducer/allReducer';
  import rootSaga from '../saga';

  const sagaMiddleware = createSagaMiddleware();
  const store = createStore(
     allReducer,
     applyMiddleware(sagaMiddleware)
   )

   sagaMiddleware.run(rootSaga);

 export default store;

А потом saga.js

import {takeLatest,delay} from 'redux-saga';
import {call, put, take, select} from 'redux-saga/effects';
import { push } from 'react-router-redux';
import data from './data.json';

export function* updateLesson(){
   try{
       yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
       yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js 
   }
   catch(e){
      console.log("error",e)
     }
  }

export function* updateDetail(action) {
  try{
       //To write store update details
   }  
    catch(e){
       console.log("error",e)
    } 
 }

export default function* rootSaga(){
    yield [
        updateLesson()
       ]
    }

А потом action.js

 export default function updateFruit(props,fruit) {
    return (
       {
         type:"UPDATE_DETAIL",
         payload:fruit,
         props:props
       }
     )
  }

А потом Reducer.js

import {combineReducers} from 'redux';

const fetchInitialData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
 const updateDetailsData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
const allReducers =combineReducers({
   data:fetchInitialData,
   updateDetailsData
 })
export default allReducers; 

А потом main.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app/components/App.jsx';
import {Provider} from 'react-redux';
import store from './app/store';
import createRoutes from './app/routes';

const initialState = {};
const store = configureStore(initialState, browserHistory);

ReactDOM.render(
       <Provider store={store}>
          <App />  /*is your Component*/
       </Provider>, 
document.getElementById('app'));

попробуй это .. работает


3
Это серьезная вещь для человека, который просто хочет вызвать конечную точку API, чтобы вернуть объект или список объектов. Вы рекомендуете: «Просто сделай это ... тогда это, потом это, потом это другое, потом это, потом это другое, потом продолжай, потом делай ..». Но, чувак, это FRONTEND, нам просто нужно позвонить в BACKEND, чтобы предоставить нам данные, готовые для использования на внешнем интерфейсе. Если это путь, что-то не так, что-то действительно не так, и кто-то не применяет KISS в настоящее время
zameb

Привет! Используйте блок try and catch для вызовов API. Как только API даст ответ, вызовите типы действий Reducer.
С.М. Чинна

1
@zameb Возможно, вы правы, но ваша жалоба связана с самим Redux, и со всем, что он слышит, пытаясь уменьшить сложность.
Джорис

1

Существуют создатели синхронных действий, а затем - создатели асинхронных действий.

Создатель синхронного действия - это тот, который, когда мы вызываем его, немедленно возвращает объект Action со всеми соответствующими данными, прикрепленными к этому объекту, и готов к обработке нашими редукторами.

Создатели асинхронных действий - это те, в которых потребуется немного времени, прежде чем он будет готов в конечном итоге отправить действие.

По определению, каждый раз, когда у вас есть создатель действия, который отправляет сетевой запрос, он всегда будет считаться создателем асинхронного действия.

Если вы хотите, чтобы создатели асинхронных действий были внутри приложения Redux, вам нужно установить нечто, называемое промежуточным программным обеспечением, которое позволит вам иметь дело с этими создателями асинхронных действий.

Вы можете убедиться в этом в сообщении об ошибке, в котором говорится, что для асинхронных действий используется пользовательское промежуточное ПО.

Так что же такое промежуточное ПО и зачем оно нам нужно для асинхронного потока в Redux?

В контексте промежуточного программного обеспечения с избыточностью, такого как redux-thunk, промежуточное программное обеспечение помогает нам иметь дело с создателями асинхронных действий, поскольку это то, что Redux не может обработать из коробки.

С промежуточным программным обеспечением, интегрированным в цикл Redux, мы по-прежнему обращаемся к создателям действий, которые будут возвращать действие, которое будет отправлено, но теперь, когда мы отправляем действие, а не отправляем его непосредственно всем нашим редукторам, мы собираемся сказать, что действие будет отправлено через все различное промежуточное ПО внутри приложения.

Внутри одного приложения Redux у нас может быть столько промежуточного программного обеспечения, сколько мы хотим. По большей части, в проектах, над которыми мы работаем, у нас будет одно или два промежуточных ПО, подключенных к нашему магазину Redux.

Промежуточное программное обеспечение - это простая функция JavaScript, которая будет вызываться при каждом отправляемом нами действии. Внутри этой функции промежуточное ПО имеет возможность остановить отправку действия любому из редукторов, оно может изменить действие или просто поиграться с действием любым способом, который вы, например, могли бы создать, промежуточное ПО, которое регистрирует консоль каждое действие, которое вы отправляете, просто для вашего удовольствия.

Существует огромное количество промежуточного программного обеспечения с открытым исходным кодом, которое вы можете установить как зависимости в свой проект.

Вы не ограничены использованием только промежуточного программного обеспечения с открытым исходным кодом или установкой его в качестве зависимостей. Вы можете написать свое собственное промежуточное программное обеспечение и использовать его в своем магазине Redux.

Одно из наиболее популярных применений промежуточного программного обеспечения (и получение вашего ответа) предназначено для работы с создателями асинхронных действий, вероятно, наиболее популярным промежуточным программным обеспечением является redux-thunk, и оно помогает вам иметь дело с создателями асинхронных действий.

Существует много других типов промежуточного программного обеспечения, которые также помогают вам работать с создателями асинхронных действий.


1

Чтобы ответить на вопрос:

Почему компонент контейнера не может вызвать асинхронный API, а затем отправить действия?

Я бы сказал, по крайней мере, по двум причинам:

Первая причина - это разделение интересов, задача не action creatorвызывать apiи возвращать данные, вам нужно передать два аргумента вашей action creator function, action typeиpayload .

Вторая причина заключается в том, что redux storeобъект ожидает простой объект с обязательным типом действия и, возможно, a payload(но здесь вы также должны передать полезную нагрузку).

Создатель действия должен быть простым объектом, как показано ниже:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

И задание Redux-Thunk midlewareпо dispacheвашему, api callчтобы результат соответствующий action.


0

При работе в корпоративном проекте в промежуточном программном обеспечении имеется много требований, таких как (saga), недоступных в простом асинхронном потоке, ниже приведены некоторые из них:

  • Выполнение запроса параллельно
  • Вытягивание будущих действий без необходимости ждать
  • Неблокирующие вызовы Эффект гонки, пример пикапа первым
  • ответ на запуск процесса. Последовательность ваших задач (сначала в первом вызове)
  • составление
  • Отмена задачи Динамически разветвленная задача.
  • Поддержка параллельного запуска Saga за пределами промежуточного программного обеспечения.
  • Использование каналов

Список длинный, просто просмотрите расширенный раздел документации саги

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.