Навіщо нам потрібне проміжне програмне забезпечення для асинхронного потоку в Redux?


686

Згідно з документами, "Без проміжного програмного забезпечення магазин Redux підтримує лише синхронний потік даних" . Я не розумію, чому це так. Чому компонент контейнера не може викликати API async, а потім 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, як підказує документація?

EDIT: Я шукав репо в Redux, щоб знайти підказки, і виявив, що в минулому для творців дій були чисті функції. Наприклад, ось користувач намагається надати кращі пояснення для асинхронного потоку даних:

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

Творцям дій більше не потрібно бути чистими. Так, проміжне програмне забезпечення / обіцянки, безумовно, вимагалося в минулому, але здається, що це вже не так?


53
Від творців дій ніколи не вимагали бути чистими функціями. Це була помилка в документах, а не рішення, яке змінилося.
Дан Абрамов

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

Відповіді:


701

Що не так у цьому підході? Чому я б хотів використовувати Redux Thunk або Redux Promise, як підказує документація?

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

Ви можете прочитати мою відповідь на тему "Як відправити Redux дії з таймаутом" для більш детального ознайомлення.

Посереднє програмне забезпечення, наприклад Redux Thunk або Redux Promise, просто надає вам «синтаксичний цукор» для відправки громовідводів або обіцянок, але вам не потрібно його використовувати.

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

// 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аргумент, переданий громовідводум, не змінюючи код виклику взагалі:

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?
Søren Debois

10
@ SørenDebois Якщо ваш додаток є лише клієнтом, це працювало б. Якщо він відображається на сервері, вам потрібно мати інший storeпримірник для кожного запиту, тому ви не можете його заздалегідь визначити.
Дан Абрамов

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

447

Ви цього не робите.

Але ... ви повинні використовувати редукс-сагу :)

Відповідь Дана Абрамова слушна, redux-thunkале я розповім трохи більше про редукс-сагу, яка є досить подібною, але потужнішою.

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

  • DOM : jQuery є імперативним / React є декларативним
  • Монади : IO є імперативним / Free є декларативним
  • Ефекти Redux : redux-thunkє імперативними / redux-sagaє декларативними

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

Якщо ви використовуєте макети, то ви не займаєтесь функціональним програмуванням.

Через об’єктив побічних ефектів знущання - це прапор, що ваш код нечистий, і в оці функціонального програміста - доказ того, що щось не так. Замість завантаження бібліотеки, яка допоможе нам перевірити, чи є айсберг недоторканим, ми повинні плисти навколо нього. Хардкор TDD / Java хлопець одного разу запитав мене, як ти знущаєшся в Clojure. Відповідь, ми зазвичай цього не робимо. Зазвичай ми бачимо це як знак, що нам потрібно переробити код.

Джерело

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

Дивіться також цю статтю :

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

(Насправді Редукс-сага - це як гібрид: потік необхідний, але ефекти декларативні)

Плутанина: дії / події / команди ...

У фронтовому світі існує велика плутанина щодо того, як можуть бути пов’язані такі поняття, як бекенд 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), оскільки це може допомогти вам додати перевірку дій (твердження / потік / typecript) у майбутньому.

Ваш код стає набагато більш перевіреним, оскільки ефекти декларативні

Вам більше не потрібно запускати подібні дзвінки, такі як rpc actions.loadUser(). Ваш інтерфейс повинен просто відправити те, що сталося. Ми займаємося лише пожежними подіями (завжди в минулому часі!) І більше не робимо дій. Це означає, що ви можете створювати зв'язані "качки" або " Обмежені контексти" і що сага може виступати в якості точки з'єднання між цими модульними компонентами.

Це означає, що вашими переглядами легше керувати, оскільки їм більше не потрібно містити цей шар перекладу між тим, що сталося, і тим, що має відбутися як ефект

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

Дехто може стверджувати, що генератори можуть приховати стан за межами магазину redux з локальними змінними, але якщо ви почнете упорядковувати складні речі всередині грому, запускаючи таймери тощо, у вас все одно виникне така ж проблема. І є selectефект, який зараз дозволяє отримати певний стан у вашому магазині Redux.

Саги можуть подорожувати часом, а також дозволяє складати складний журнал потоку та інструменти розробки, над якими зараз працює. Ось декілька простих журналів потоку асинхронізації, які вже реалізовані:

сага протоколювання

Розв'язка

Саги не тільки замінюють скоромовки. Вони надходять із бекенду / розподілених систем / пошуку подій.

Дуже поширене оману, що саги якраз тут, щоб замінити ваші скоромовки на кращу перевірку. Насправді це лише деталізація редукс-саги. Використання декларативних ефектів краще, ніж загроза для перевірки, але модель саги може бути реалізована поверх імперативного чи декларативного коду.

По-перше, сага - це програмне забезпечення, яке дозволяє координувати тривалі операції (можлива послідовність) та транзакції в різних обмежених контекстах (жаргон дизайну, керований доменом).

Щоб спростити це для фронтового світу, уявіть, що є віджет1 та віджет2. Якщо натиснути якусь кнопку віджета1, це має вплинути на віджет2. Замість того, щоб з'єднати два віджети разом (тобто віджет1 відправляє дію, націлену на віджет2), віджет1 розсилає лише те, що його кнопку було натиснуто. Тоді сага прослухає цей натискання кнопки, а потім оновить віджет2, розпочавши нову подію, про яку знає widget2.

Це додає рівня непрямості, який не є необхідним для простих додатків, але полегшує масштабування складних програм. Тепер ви можете публікувати widget1 та widget2 у різних сховищах npm, щоб вони ніколи не знали один про одного, не маючи їх спільного використання глобального реєстру дій. Два віджети тепер обмежені контекстами, які можуть жити окремо. Їм не потрібно, щоб один одного були послідовними, і їх можна повторно використовувати в інших додатках. Сага - це сполучна точка між двома віджетами, які координують їх у значущий спосіб для вашого бізнесу.

Кілька приємних статей про те, як структурувати додаток Redux, за допомогою якого ви можете використовувати Redux-saga з причини роз’єднання:

Конкретна справа використання: система повідомлень

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

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

сповіщення

Я тут описав, як це можна зробити за допомогою саги

Чому його називають сагою?

Термін «сага» походить із світу заходу. Я спочатку представив Яссіна (автора «Редукс-саги») в цей термін під час тривалої дискусії .

Спочатку цей термін був введений з документом , модель саги повинна була використовуватися для обробки можливої ​​послідовності розподілених транзакцій, але його використання було розширено на більш широке визначення розробниками бекенда, так що тепер воно також охоплює "менеджера процесів" шаблон (якось оригінальний шаблон саги - це спеціалізована форма менеджера процесів).

Сьогодні термін "сага" заплутаний, оскільки може описувати дві різні речі. Оскільки він використовується в редукс-сазі, він не описує спосіб обробки розподілених транзакцій, а скоріше спосіб координації дій у вашому додатку. redux-sagaможна було також назвати redux-process-manager.

Дивись також:

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

Якщо вам не подобається ідея використання генераторів, але вас цікавить шаблон саги та його властивості роз'єднання, ви також можете досягти того ж за допомогою редукційного спостереження, яке використовує ім'я 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-saga лише заради використання. Тільки виклики API для тестування не варто.
  • Не видаляйте грози з вашого проекту для більшості простих випадків.
  • Не соромтеся відправляти громи, yield put(someActionThunk)якщо це має сенс.

Якщо вас лякає використання Redux-saga (або Redux-спостерігаемо), але вам просто потрібна схема роз'єднання, перевірте 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
NIce відповідь. Зараз є ще одна альтернатива: github.com/blesh/redux-observable
swennemen

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

3
Мені ця відповідь не подобається, оскільки вона відволікає від актуального питання, щоб продати комусь власну бібліотеку. Ця відповідь дає порівняння двох бібліотек, що не було метою запитання. Актуальне питання - це чи взагалі використовувати проміжне програмне забезпечення, що пояснюється прийнятою відповіддю.
Абхінав Манчанда

31

Коротка відповідь : мені здається цілком розумним підходом до проблеми асинхронності. З парою застережень.

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

Я закінчився дійсно подібним підходом до того, що у вас є в бібліотеці, і я взяв участь в нашому проекті, який ми назвали react-redux-controller .

Зрештою, я не пішов з точним підходом, який ви маєте вище, з кількох причин:

  1. Як ви це написали, ці диспетчерські функції не мають доступу до магазину. Ви можете дещо подолати це, передавши компоненти інтерфейсу користувача всю інформацію, необхідну диспетчерській функції. Але я б заперечував, що це з’єднує ці компоненти інтерфейсу з логікою відправки. І що більш проблематично, немає очевидного способу функції диспетчеризації доступу до оновленого стану в асинхронних продовженнях.
  2. Диспетчерські функції мають доступ до dispatchсебе через лексичну область. Це обмежує варіанти рефакторингу, як тільки ця connectзаява вийде з-під руки - і вона виглядає досить непростою лише одним updateметодом. Тож вам потрібна якась система, яка дозволяє складати ці функції диспетчера, якщо ви розбиваєте їх на окремі модулі.

Візьміть разом, ви повинні встановити деяку систему, щоб дозволити dispatchмагазину вводити ваші диспетчерські функції разом з параметрами події. Я знаю три розумні підходи до цієї ін'єкції залежності:

  • redux-thunk робить це функціонально, передаючи їх у ваші гроні (роблячи їх зовсім не товстими, за визначенням купола). Я не працював з іншими dispatchпідходами проміжного програмного забезпечення, але я припускаю, що вони в основному однакові.
  • Контролер react-redux робить це за допомогою програми. Як бонус, він також надає вам доступ до "селекторів", які є функціями, які ви, можливо, передали як перший аргумент connect, а не працювати безпосередньо з необробленим, нормалізованим магазином.
  • Ви також можете зробити це об'єктно-орієнтованим способом, ввівши їх у thisконтекст, за допомогою різних можливих механізмів.

Оновлення

Мені здається, що частина цієї головоломки є обмеженням реакції-відновлення . Перший аргумент, щоб connectотримати знімок стану, але не відправити. Другий аргумент отримує відправлення, але не стан. Жоден аргумент не отримує сповіщення, яке закривається над поточним станом, оскільки він може бачити оновлений стан під час продовження / зворотного виклику.


22

Мета Абрамова - і в ідеалі кожного - просто укласти складність (і асинхронізувати дзвінки) в тому місці, де це найбільш доречно .

Де найкраще це зробити в стандартному потоці даних Redux? Як щодо:

  • Редуктори ? У жодному разі. Вони повинні бути чистими функціями без побічних ефектів. Оновлення магазину - серйозний, складний бізнес. Не забруднюйте його.
  • Тупі компоненти перегляду? Безумовно, ні. Вони мають одну проблему: презентація та взаємодія з користувачем, і вони повинні бути максимально простими.
  • Компоненти контейнерів? Можливо, але недооптимальне. Це має сенс у тому, що контейнер - це місце, де ми вкладаємо певну складність перегляду та взаємодіємо з магазином, але:
    • Контейнери повинні бути складнішими, ніж німі компоненти, але це все одно є одна відповідальність: забезпечення зв'язків між переглядом та станом / магазином. Ваша логіка асинхронізації - це зовсім окрема проблема.
    • Помістивши його в контейнер, ви б заблокували свою логіку асинхронізації в одному контексті для одного перегляду / маршруту. Погана ідея. В ідеалі це все для багаторазового використання та повністю відокремлено.
  • З іншими модулями обслуговування? Погана ідея: вам потрібно буде ввести доступ до магазину, що є кошмаром ремонту / тестування. Краще поїхати з зерном Redux і зайти в магазин лише за допомогою наданих API / моделей.
  • Дії та середнє виробництво, що їх інтерпретують? Чому ні?! Для початку це єдиний головний варіант, який нам залишився. :-) Більш логічно, що система дій - це відокремлена логіка виконання, яку ви можете використовувати з будь-якого місця. Він має доступ до магазину і може відправити більше дій. Він несе єдину відповідальність, яка полягає в організації потоку контролю та даних навколо програми, і більшість асинхронних робіт вписується прямо в це.
    • Що з Творцями дій? Чому б просто не виконати асинхронізацію там, а не в самих діях, а не в Middleware?
      • По-перше, і найголовніше, що творці не мають доступу до магазину, як це робить проміжне програмне забезпечення. Це означає, що ви не можете відправляти нові дії в умовних ситуаціях, не можете читати з магазину, щоб скласти свою асинхронізацію тощо.
      • Отже, зберігайте складність у необхідному місці, а все інше просто. Тоді творці можуть бути простими, відносно чистими функціями, які легко перевірити.

Компоненти контейнерів - чому б і ні? Через рольові компоненти, що грають у React, контейнер може виконувати роль сервісного класу, і він вже отримує магазин через DI (реквізити). Помістивши його в контейнер, ви б заблокували свою логіку асинхронізації в одному контексті, для одного виду / маршруту - як це? Компонент може мати кілька примірників. Він може бути від'єднаний від презентації, наприклад, з рендером. Я думаю, що відповідь могла б отримати більше користі від коротких прикладів, які підтверджують суть.
колба Естуса

Я люблю цю відповідь!
Маурісіо Авенданьо

13

Щоб відповісти на запитання, яке задають на початку:

Чому компонент контейнера не може викликати API асинхронізації, а потім відправляти дії?

Майте на увазі, що ці документи призначені для Redux, а не Redux плюс React. Магазини Redux, підключені до компонентів React, можуть робити саме те, що ви говорите, але звичайний магазин Red Red без проміжного програмного забезпечення не приймає аргументи, 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дзвінками).


Ви радите відправити ще одну подію щодо завершення дій. Це не спрацює, коли після завершення дії потрібно показати сповіщення (). Обіцянки всередині компонентів 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 дозволяє писати творцям дій, які повертають функцію замість дії. Громовідвід може бути використаний для затримки відправки дії або для відправки лише у випадку дотримання певної умови. Внутрішня функція отримує методи зберігання відправки та getState в якості параметрів.

Отже, як бачите, він поверне функцію замість дії, означає, що ви можете зачекати і викликати її в будь-який час, як це є функцією ...

Так що, чорт забирає? Ось як це введено у Вікіпедії:

У комп’ютерному програмуванні грона - це підпрограма, яка використовується для введення додаткового обчислення в іншу підпрограму. 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
Перший раз на SO, нічого не читав. Але просто сподобався пост, який дивиться на картину. Дивовижні, натяки та нагадування.
Бходжендра Рауніяр

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, щоб повернути об'єкт чи список об'єктів. Ви рекомендуєте: "просто зробіть це ... то це, то це, то це інше, то те, то це інше, то продовжуйте, потім робіть ..". Але людино, це ФРОНТЕНД, нам просто потрібно зателефонувати НАЗАД, щоб дати нам дані, готові використовуватись на фронті. Якщо це шлях, щось не так, щось справді не так, і хтось зараз не застосовує KISS
zameb

Привіт, використовуй блок спробу і лову для дзвінків API. Після того, як API дав відповідь, викличте типи дій "Редуктор".
SM Chinna

1
@zameb Ви можете мати рацію, але ваша скарга - це саме на Redux, і все це підслуховує, намагаючись зменшити складність.
jorisw

1

Є творці синхронних дій, а потім - асинхронні творці дій.

Творець синхронних дій - це той, що коли ми його називаємо, він негайно повертає об'єкт Action з усіма відповідними даними, приєднаними до цього об'єкта, і його готові обробляти наші редуктори.

Творці асинхронних дій - це той, в якому знадобиться трохи часу, перш ніж він буде готовий в кінцевому підсумку відправити дію.

За визначенням, щоразу, коли у вас є автор дії, який робить мережевий запит, він завжди буде кваліфікований як творець асинхронних дій.

Якщо ви хочете створити асинхронних творців дій всередині програми Redux, вам слід встановити щось, що називається проміжним програмним забезпеченням, яке дозволить вам мати справу з тими асинхронними творцями дій.

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

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

У контексті середнього програмного забезпечення redux, такого як redux-thunk, середнє програмне забезпечення допомагає нам мати справу з творцями асинхронних дій, тому що Redux не може впоратися з коробки.

З інтегрованим програмним забезпеченням, що інтегрується в цикл Redux, ми все ще викликаємо творців дій, тобто повернемо дію, яка буде відправлена, але тепер, коли ми відправляємо дію, а не надсилаємо її безпосередньо всім нашим редукторам, ми збираємось сказати, що дія буде надіслана через усі різні проміжні програми всередині програми.

Всередині однієї програми Redux у нас може бути стільки або мало програмного забезпечення, скільки ми хочемо. Здебільшого у проектах, над якими ми працюємо, ми матимемо один-два проміжних програм, підключених до нашого магазину Redux.

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

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

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

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

Існує багато інших типів проміжного програмного забезпечення, які також допомагають вам мати справу з творцями асинхронних дій.


1

Щоб відповісти на запитання:

Чому компонент контейнера не може викликати API асинхронізації, а потім відправляти дії?

Я б сказав, щонайменше, з двох причин:

Перша причина - це роз'єднання проблем, це не завдання action creatorвикликати apiта повернути дані, вам доведеться передати два аргументи вашому action creator function, action typea і a payload.

Друга причина - redux storeце те, що чекає звичайний об'єкт із обов'язковим типом дії та необов'язково payload(але тут вам також потрібно передати корисну навантаження).

Творець дії повинен бути простим об'єктом, як показано нижче:

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

І робота Redux-Thunk midlewareна dispacheрезультат вашого api callдо відповідного action.


0

Працюючи в проекті підприємства, існує багато вимог, що пред'являються до середніх виробів, наприклад (saga), недоступних у простому асинхронному потоці, нижче:

  • Паралельно виконується запит
  • Затягування майбутніх дій без необхідності чекати
  • Неблокуючі дзвінки Ефект гонки, наприклад, перший підбір
  • відповідь на ініціювання процесу Послідовність завдань (спочатку під час першого дзвінка)
  • Композиція
  • Скасування завдання Динамічне розгортання завдання.
  • Підтримка Concurrency Running Saga за межами резервного програмного забезпечення.
  • Використання каналів

Список довгий, просто перегляньте розширений розділ документації саги

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.