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

303

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

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

Изображение 162

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. Он отправляет действие, которое сообщает App, что оно обновляется, а затем выполняет асинхронный вызов. После завершения вызова предоставленное значение отправляется как полезная нагрузка другого действия.

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

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

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

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

  • 39
    Создатели действий никогда не должны были быть чистыми функциями. Это была ошибка в документах, а не решение, которое изменилось.
  • 1
    @DanAbramov для тестируемости это может быть хорошей практикой, однако. Redux-saga разрешает это: stackoverflow.com/a/34623840/82609
Теги:
asynchronous
redux
redux-thunk

6 ответов

360
Лучший ответ

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

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

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

Middleware, например Redux Thunk или Redux Promise, просто дает вам "синтаксический сахар" для отправки thunks или promises, но вам не нужно его использовать.

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

// 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 async
    loadOtherData(dispatch, userId) // pass dispatch first: it async
  );
}


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

С помощью создателей Action 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, заключается в том, что компоненты arent знают о том, как создатели действия реализованы, и заботятся ли они о состоянии Redux, являются ли они синхронными или асинхронными и не требуют ли они других действий создатели. Недостаток - это немного косвенное отношение, но мы считаем его достойным в реальных приложениях.

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

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

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

  • 38
    Возможно, короткая мысль Дана такова: промежуточное ПО - это централизованный подход, который позволяет вам упростить и обобщить ваши компоненты и контролировать поток данных в одном месте. Если вы поддерживаете большое приложение, оно вам понравится =)
  • 0
    Я, наверное, что-то упускаю, но почему правда, что «ваши компоненты точно знают , что конкретный вызов является асинхронным»? Разве нельзя предположить, что каждый вызов, возможно, является асинхронным и всегда проходит dispatch (и, в этом getState , getState )? Не пытаясь придираться, но если это так, то на самом деле это просто вопрос внедрения зависимостей, и нет никакой причины первого порядка, что DI должен происходить в промежуточном программном обеспечении dispatch . Это было то место, откуда я пришел с github.com/rackt/react-redux/issues/237 , в котором Дэн упомянул некоторые проблемы второго порядка.
Показать ещё 19 комментариев
286

У вас нет.

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

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

Императивный VS декларативный

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

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

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

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

Источник

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

См. также article:

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

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

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

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

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

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

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 позволяет выразить, что вы заинтересованы только в том, чтобы получить данные последнего имени пользователя (обработайте concurrency проблемы в случае, если пользователь очень быстро нажимает на многие имена пользователей). Такие вещи тяжело с грохотом. Вы могли бы использовать takeEvery, если вы не хотите этого поведения.

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

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

Вам больше не нужно запускать вызовы типа rpc типа actions.loadUser(). Ваш пользовательский интерфейс должен просто отправить то, что ПРОИСХОДИТ. Мы стреляем только события (всегда в прошедшем времени!), А не действия. Это означает, что вы можете создавать разделенные "утки" или Ограниченные контексты и что сага может выступать в качестве точки соединения между этими модульными компонентами.

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

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

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

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

Изображение 2691

Развязка

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

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

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

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

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

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

Конкретная usecase: система уведомлений

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

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

Изображение 2692

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

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

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

Сегодня термин "сага" запутан, поскольку он может описывать две разные вещи. Поскольку он используется в redux-саге, он не описывает способ обработки распределенных транзакций, а скорее способ координации действий в вашем приложении. 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-наблюдаемого), но просто нужно развязать шаблон, проверьте redux-dispatch-subscribe: it позволяет прослушивать рассылки и запускать новые рассылки в слушателе.

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});
const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});
  • 52
    Это становится лучше каждый раз, когда я возвращаюсь. Подумайте о том, чтобы превратить его в блог :).
  • 0
    спасибо @RainerAtSpirit Я собираюсь, но я хочу кое-что выяснить раньше, в том числе решить этот вопрос по архитектуре вязов ( github.com/jarvisaoieong/redux-architecture/issues/1 ) и сделать некоторый код с этими идеями в github. ком / slorber / todomvc-адаптационный
Показать ещё 15 комментариев
18

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

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

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

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

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

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

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

Обновление

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

9

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

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

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

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

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 является (или должен быть) асинхронным.

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

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

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

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

  • Переходники? Ни за что. Они должны быть чистыми функциями без побочных эффектов. Обновление магазина - серьезный, сложный бизнес. Не загрязняйте его.
  • Компоненты Dumb View? Определенно Нет. У них есть одна проблема: презентация и взаимодействие с пользователем и должны быть как можно более простыми.
  • Контейнерные компоненты? Возможно, но не оптимально. Имеет смысл, что контейнер - это место, где мы инкапсулируем некоторую связанную с представлением сложность и взаимодействуем с хранилищем, но:
    • Контейнеры должны быть более сложными, чем немые компоненты, но все равно одна ответственность: предоставление привязок между представлением и состоянием/хранилищем. Ваша асинхронная логика - это отдельная проблема.
    • Поместив его в контейнер, вы будете блокировать свою асинхронную логику в единый контекст для одного вида/маршрута. Плохая идея. В идеале это все можно использовать повторно и полностью развязать.
  • S ome другой служебный модуль? Плохая идея: вам нужно будет вводить доступ к хранилищу, что является кошмаром для обслуживания/тестирования. Лучше пойти с зерном Redux и получить доступ к магазину только с помощью предоставленных API/моделей.
  • Действия и посредники, которые их интерпретируют? Почему бы и нет?! Для начала это единственный важный вариант, который мы оставили.:-) Более логично, система действий - это развязанная логика выполнения, которую вы можете использовать из любого места. Он получил доступ к магазину и может отправлять больше действий. У него есть отдельная ответственность, которая заключается в организации потока управления и данных вокруг приложения, и большинство асинхронностей вписывается в это.
    • Как насчет создателей Action? Почему бы просто не просто асинхронно там, а не в самих действиях и в промежуточном ПО?
      • Во-первых, самое главное, у создателей нет доступа к хранилищу, как это делает промежуточное программное обеспечение. Это означает, что вы не можете отправлять новые условные действия, не можете читать из магазина, чтобы составить свой асинхронный процесс и т.д.
      • Итак, держите сложность в таком сложном месте и сохраняйте все остальное просто. Тогда создатели могут быть простыми, относительно чистыми функциями, которые легко тестировать.
  • 0
    Контейнерные компоненты - почему бы и нет? Из-за роли, которую играют компоненты в React, контейнер может выступать в качестве класса обслуживания, и он уже получает хранилище через DI (реквизиты). Поместив его в контейнер, вы бы заблокировали свою асинхронную логику в одном контексте для одного представления / маршрута - как так? Компонент может иметь несколько экземпляров. Его можно отделить от представления, например, с помощью рендера. Я полагаю, что ответ может принести еще больше пользы от коротких примеров, которые подтверждают это.
0

Хорошо, начнем видеть, как работает промежуточное программное обеспечение, что вполне отвечает на вопрос, это исходный код applyMiddleWare в 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 позволяет вам писать создателей действий, которые возвращаются вместо функции. Бранд может использоваться для задержки отправку действия или отправку только в том случае, если определенное условие встретил. Внутренняя функция получает методы отправки хранилища и 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;

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

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

Изображение 2693

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню