Redux: Попытка разместить вызовы API в правильном месте

Созданный на 20 июл. 2015  ·  115Комментарии  ·  Источник: reduxjs/redux

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

В настоящее время я использую actions -> reducer (switch case with action calling API) -> success/error on response triggering another action .

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

я что-то упускаю?

Редуктор

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';
import Immutable from 'immutable';
import LoginApiCall from '../utils/login-request';

const initialState = new Immutable.Map({
  email: '',
  password: '',
}).asMutable();

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      console.log(action.user);
      LoginApiCall.login(action.user);
      return state;
    case LOGGED_FAILED:
      console.log('failed from reducer');
      return state;
    case LOGGED_SUCCESSFULLY:
      console.log('success', action);
      console.log('success from reducer');
      break;
    default:
      return state;
  }
}

действия

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return dispatch => {
    dispatch({ error, type: LOGGED_FAILED });
  };
}

/*
 * Should add the route like parameter in this method
*/
export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard'); // will fire CHANGE_ROUTE in its change handler
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return dispatch => {
    dispatch({ user, type: LOGIN_ATTEMPT });
  };
}

Вызовы API

 // Use there fetch polyfill
 // The main idea is create a helper in order to handle success/error status
import * as LoginActions from '../actions/LoginActions';

const LoginApiCall = {
  login(userData) {
    fetch('http://localhost/login', {
      method: 'post',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userData.email,
        password: userData.password,
      }),
    })
    .then(response => {
      if (response.status >= 200 && response.status < 300) {
        console.log(response);
        LoginActions.loginSuccess(response);
      } else {
        const error = new Error(response.statusText);
        error.response = response;
        LoginActions.loginError();
        throw error;
      }
    })
    .catch(error => { console.log('request failed', error); });
  },
};

export default LoginApiCall;
docs question

Самый полезный комментарий

И, наконец, куда же тогда поместить вызов API входа в систему?

Именно для этого и нужен создатель действий dispatch => {} . Побочные эффекты!

Это просто еще один создатель экшена. Совместите с другими действиями:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}

export function login(userData) {
  return dispatch =>
    fetch('http://localhost/login', {
      method: 'post',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userData.email,
        password: userData.password,
      }),
    })
    .then(response => {
      if (response.status >= 200 && response.status < 300) {
        console.log(response);
        dispatch(loginSuccess(response));
      } else {
        const error = new Error(response.statusText);
        error.response = response;
        dispatch(loginError(error));
        throw error;
      }
    })
    .catch(error => { console.log('request failed', error); });
}

В ваших компонентах просто позвоните

this.props.login(); // assuming it was bound with bindActionCreators before

// --or--

this.props.dispatch(login()); // assuming you only have dispatch from Connector

Все 115 Комментарий

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

// CounterActions
export function increment() {
  return { type: INCREMENT }
}


// Some other file
import { increment } from './CounterActions';
store.dispatch(increment()); <--- This will work assuming you have a reference to the Store
increment(); <---- THIS DOESN'T DO ANYTHING! You're just calling your function and ignoring result.


// SomeComponent
import { increment } from './CounterActions';

@connect(state => state.counter) // will inject store's dispatch into props
class SomeComponent {
  render() {
    return <OtherComponent {...bindActionCreators(CounterActions, this.props.dispatch)} />
  }
}


// OtherComponent
class OtherComponent {
  handleClick() {
    // this is correct:
    this.props.increment(); // <---- it was bound to dispatch in SomeComponent

    // THIS DOESN'T DO ANYTHING:
    CounterActions.increment(); // <---- it's just your functions as is! it's not bound to the Store.
  }
}

Теперь перейдем к вашему примеру. Первое, что я хочу уточнить, вам не нужна форма async dispatch => {} если вы синхронно отправляете только одно действие и не имеете побочных эффектов (верно для loginError и loginRequest ).

Этот:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return dispatch => {
    dispatch({ error, type: LOGGED_FAILED });
  };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return dispatch => {
    dispatch({ user, type: LOGIN_ATTEMPT });
  };
}

можно упростить как

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

// You'll have a side effect here so (dispatch) => {} form is a good idea
export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}

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

Этот:

const initialState = new Immutable.Map({
  email: '',
  password: '',
  isLoggingIn: false,
  isLoggedIn: false,
  error: null
}).asMutable(); // <---------------------- why asMutable?

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      console.log(action.user);
      LoginApiCall.login(action.user); // <------------------------ no side effects in reducers! :-(
      return state;
    case LOGGED_FAILED:
      console.log('failed from reducer');
      return state;
    case LOGGED_SUCCESSFULLY:
      console.log('success', action);
      console.log('success from reducer');
      break;
    default:
      return state;
  }

вероятно, должно быть больше похоже на

const initialState = new Immutable.Map({
  email: '',
  password: '',
  isLoggingIn: false,
  isLoggedIn: false,
  error: null
});

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      return state.merge({
        isLoggingIn: true,
        isLoggedIn: false,
        email: action.email,
        password: action.password // Note you shouldn't store user's password in real apps
      });
    case LOGGED_FAILED:
      return state.merge({
        error: action.error,
        isLoggingIn: false,
        isLoggedIn: false
      });
    case LOGGED_SUCCESSFULLY:
      return state.merge({
        error: null,
        isLoggingIn: false,
        isLoggedIn: true
      });
      break;
    default:
      return state;
  }

И, наконец, куда же тогда поместить вызов API входа в систему?

Именно для этого и нужен создатель действий dispatch => {} . Побочные эффекты!

Это просто еще один создатель экшена. Совместите с другими действиями:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}

export function login(userData) {
  return dispatch =>
    fetch('http://localhost/login', {
      method: 'post',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userData.email,
        password: userData.password,
      }),
    })
    .then(response => {
      if (response.status >= 200 && response.status < 300) {
        console.log(response);
        dispatch(loginSuccess(response));
      } else {
        const error = new Error(response.statusText);
        error.response = response;
        dispatch(loginError(error));
        throw error;
      }
    })
    .catch(error => { console.log('request failed', error); });
}

В ваших компонентах просто позвоните

this.props.login(); // assuming it was bound with bindActionCreators before

// --or--

this.props.dispatch(login()); // assuming you only have dispatch from Connector

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

Техника, которую я описал выше (создатели действий с подписью dispatch => {} ), прямо сейчас включен в Redux, но в 1.0 будет доступен только как отдельный пакет, называемый redux-thunk . Пока вы это делаете, вы можете также проверить redux-prom-middleware или redux-prom .

@gaearon : clap: потрясающее объяснение! ;) Это обязательно должно быть в документации.

@gaearon отличное объяснение! :трофей:

@gaearon Что делать, если вам нужно выполнить некоторую логику, чтобы определить, какой вызов API следует вызывать? Разве это не доменная логика, которая должна быть в одном месте (редукторы)? Сохранение этого в действии для создателей звучит для меня как нарушение единственного источника правды. Кроме того, в большинстве случаев вам нужно параметризовать вызов API некоторым значением из состояния приложения. Скорее всего, вы тоже захотите как-нибудь проверить логику. Мы обнаружили, что вызовы API в редукторах (атомарный поток) очень полезны и тестируются в крупномасштабном проекте.

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

Это нарушает запись / воспроизведение. Функции с побочными эффектами тестировать труднее, чем чистые функции по определению. Вы можете использовать Redux таким образом, но это полностью противоречит его замыслу. :-)

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

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

Что, если вам нужно выполнить некоторую логику, чтобы определить, какой вызов API следует вызывать? Разве это не доменная логика, которая должна быть в одном месте (редукторы)?

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

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

// Simple pure action creator
function loginFailure(error) {
  return { type: LOGIN_FAILURE, error };
}

// Another simple pure action creator
function loginSuccess(userId) {
  return { type: LOGIN_SUCCESS, userId };
}

// Another simple pure action creator
function logout() {
  return { type: LOGOUT };  
}


// Side effect: uses thunk middleware
function login() {
  return dispatch => {
    MyAwesomeAPI.performLogin().then(
      json => dispatch(loginSuccess(json.userId)),
      error => dispatch(loginFailure(error))
    )
  };
}


// Side effect *and* reads state
function toggleLoginState() {
  return (dispatch, getState) => {
    const { isLoggedIn } = getState().loginState;
    if (isLoggedIn) {
      dispatch(login());
    } else {
      dispatch(logout());
    }
  };
}

// Component
this.props.toggleLoginState(); // Doesn't care how it happens

Создатели действий и промежуточное ПО _ предназначены_ для выполнения побочных эффектов и дополняют друг друга.
Редукторы - это просто машины состояний и не имеют ничего общего с асинхронностью.

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

Это действительно очень важный момент, спасибо.

Я открою для потомков, пока версия этого не появится в документации.

: +1:

Я бы сказал, что в то время как редукторы являются конечными автоматами, создатели действий в целом тоже (но неявно).

Скажем, пользователь дважды нажал кнопку «Отправить». В первый раз вы отправите HTTP-запрос на сервер (в создателе действия) и измените состояние на «отправка» (в редукторе). Второй раз вы не хотите этого делать.

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

Таким образом, ваш поток управления фактически разделен на два конечных автомата, и один из них является неявным и полон побочных эффектов.

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

https://github.com/Day8/re-frame - пример другого подхода, когда у них есть единственный обработчик событий, который действует как единственный FSM.

Но тогда у редукторов есть побочные эффекты. Так что интересно, как они справляются с функцией "воспроизведения" в таких случаях - спросили их в https://github.com/Day8/re-frame/issues/86.

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

Держите нас в курсе о повторном кадре и побочных эффектах!

В моей книге есть 3 типа состояний в любом приличном веб-приложении Redux.

1) Просмотр компонентов (React this.state). Этого следует избегать, но иногда я просто хочу реализовать какое-то поведение, которое можно использовать повторно, просто повторно используя компонент независимо от Redux.

2) Общее состояние приложения, т.е. Redux.state. Это состояние, когда уровень представления используется для представления приложения пользователю, а компоненты представления используют его для «взаимодействия» друг с другом. И это состояние можно использовать для «общения» между ActionCreators, Middlewares, представлениями, поскольку их решения могут зависеть от этого состояния. Следовательно, «общее» важно. Однако это не обязательно должно быть полное состояние приложения. Или я считаю нецелесообразным реализовывать все как этот тип состояния (в плане действий).

3) Состояние, которое поддерживается другими сложными модулями / библиотеками / службами с побочными эффектами. Я бы написал их для обработки сценариев, которые описывает владар. Или реактивный маршрутизатор - другой пример. Вы можете рассматривать его как черный ящик, который имеет собственное состояние, или вы можете решить поднять часть состояния реактивного маршрутизатора в общее состояние приложения (Redux.state). Если бы я написал сложный HTTP-модуль, который бы умело обрабатывал все мои запросы и их тайминги, меня обычно не интересовали бы детали тайминга запросов в Redux.state. Я бы использовал этот модуль только из ActionCreators / Middlewares, возможно, вместе с Redux.state, чтобы получить новый Redux.state.

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

@vladar Вы имеете в виду, что AC / MW - это машины неявных состояний? Это потому, что они не удерживают состояние сами по себе, но они все еще зависят от состояния, хранящегося где-то еще, и что они могут определять логику управления, как эти состояния развиваются во времени? В некоторых случаях я думаю, что они все еще могут быть реализованы как замыкания и удерживать собственное состояние, становясь явными конечными автоматами?

В качестве альтернативы я мог бы назвать Redux.state «публичным состоянием», в то время как другие состояния являются частными. Создание моего приложения - это действие, чтобы решить, что оставить как частное состояние, а что как общедоступное. Мне это кажется хорошей возможностью для инкапсуляции, поэтому я не вижу проблем в разделении состояния на разные места, поскольку это не становится адом, как они влияют друг на друга.

@vladar

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

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

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

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

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

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

Путешествие во времени прекрасно работает с нечистыми создателями действий, потому что _ оно происходит на уровне отправленных (окончательных) сырых действий_. Это простые объекты, поэтому они полностью детерминированы. Да, вы не можете «откатить» вызов API, но я не вижу в этом проблемы. Вы можете откатить созданные им необработанные действия.

Это потому, что они не удерживают состояние сами по себе, но они все еще зависят от состояния, хранящегося где-то еще, и что они могут определять логику управления, как эти состояния развиваются во времени?

Да, именно это я имел в виду. Но вы также можете получить дублирование вашей логики управления. Код, демонстрирующий проблему (из моего примера выше с двойным щелчком мыши).

function submit(data) {
  return (dispatch, getState) => {
    const { isSubmitting } = getState().isSubmitting;
    if (!isSubmitting) {
      dispatch(started(data));

      MyAPI.submit(data).then(
        json => dispatch(success(json)),
        error => dispatch(failure(error))
      )
    }
  }
}

function started(data) {
   return { type: SUBMIT_STARTED, data };
}

function success(result) {
  return { type: SUBMIT_SUCCESS, result };
}

function failure(error) {
  return { type: SUBMIT_FAILURE, error };
}

И тогда у вас есть редуктор, чтобы снова проверить состояние isSubmitting.

const initialState = new Immutable.Map({
  isSubmitting: false,
  pendingData: null,
  error: null
});

export default function form(state = initialState, action) {
  switch (action.type) {
    case SUBMIT_STARTED:
      if (!state.isSubmitting) {
        return state.merge({
          isSubmitting: true,
          pendingData: action.data
        });
      } else {
        return state;
      }
  }
}

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

Я думаю, что в последнем примере проверка не должна быть внутри редуктора. В противном случае он может быстро привести к несогласованности (например, действие отправляется по ошибке, но игнорируется, но затем также отправляется «успешное» действие, но оно было неожиданным, и состояние могло быть неправильно объединено).

(Извините, если вы об этом говорили ;-)

Я согласен с gaeron, потому что! IsSubmitting уже закодирован в том факте, что действие SUBMIT_STARTED было отправлено. Когда действие является результатом какой-то логики, эту логику не следует воспроизводить в редукторе. Тогда ответственность редуктора заключается в том, что каждый раз, когда он получает SUBMIT_STARTED, он не думает и просто обновляет состояние с полезной нагрузкой действия, потому что кто-то другой взял на себя ответственность решить, что SUBMIT_STARTED.

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

Затем Reducer может дополнительно основываться на факте SUBMIT_STARTED и расширять его дополнительной логикой, но эта логика должна быть другой. Fe:

case SUBMIT_STARTED:
  if (goodMood) loading...
  else nothing_happens

Я могу представить, что иногда бывает сложно решить, кто за что должен нести ответственность. ActionCreator, Middleware, автономный комплексный модуль, редуктор?

Я бы стремился иметь как можно больше решений в редукторах, сохраняя при этом их чистоту, потому что их можно перезагружать. Если для побочных эффектов / async требуются решения, они отправляются в AC / MW /where_else. Это также может зависеть от того, как будут развиваться лучшие практики в отношении повторного использования кода, т.е. редукторы или промежуточное ПО.

AC не может изменять state.isSubmitting напрямую. Следовательно, если его логика зависит от этого, AC необходимы гарантии, что state.isSubmitting будет синхронизироваться с его логикой. Если логика AC хочет установить для state.isSubmitted значение true с новыми данными, а чтение возвращается, она ожидает, что оно установлено как таковое. Другая логика не должна потенциально изменять эту предпосылку. ACTION - это сам примитив синхронизации. В основном действия определяют протокол, а протоколы должны быть хорошо и четко определены.

Но я думаю, что знаю, что вы пытаетесь сказать. Redux.state - это общее состояние. Общее состояние всегда непросто. В редукторах легко написать код, который изменяет состояние неожиданным для логики AC способом. Следовательно, я могу представить, что может быть трудно синхронизировать логику между AC и редукторами в некоторых очень сложных сценариях. Это может привести к ошибкам, за которыми будет сложно следить и отлаживать. Я считаю, что для таких сценариев всегда можно свернуть логику до AC / Middleware и заставить редукторы тупо действовать на основе четко определенных действий без логики или с небольшой логикой.

Кто-нибудь всегда может добавить новый редуктор, который сломает какой-то старый зависимый AC. Следует дважды подумать, прежде чем делать AC зависимым от Redux.state.

У меня есть создатель действия, которому требуется токен аутентификации, возвращаемый запросом API с учетными данными пользователя. У этого токена есть ограничение по времени, и его нужно обновлять и чем-то управлять. То, что вы предлагаете, похоже, указывает на то, что это состояние не принадлежит Redux.state?

Куда он должен идти? Концептуально ли он передается в конструктор хранилища Redux как неизменное внешнее состояние?

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

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

Если вы полагаетесь на правильный порядок событий из AC - вы просто неявно перемещаете части вашего механизма состояний в AC и связываете его с редуктором.

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

Следует дважды подумать, прежде чем делать AC зависимым от Redux.state.

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

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

Я прочитал (слишком быстро) через Re-frame, и это то же самое, что и Redux, с некоторыми различиями в деталях реализации. Компоненты представления Re-frame основаны на наблюдаемых, Events - это действия, Re-frame отправляет события (действия) напрямую как объекты (они создаются в компоненте представления) без использования создателей, обработчики событий чистые, и они то же самое, что Redux редукторы.

Обработка побочных эффектов мало обсуждается, или я это пропустил, но я предполагаю, что они обрабатываются в промежуточном программном обеспечении. И в этом главное отличие. Промежуточное ПО обертывает обработчики событий (редукторы), компоновка с ними извне, в то время как промежуточное ПО Redux не обертывает редукторы. Другими словами, Re-frame создает побочные эффекты напрямую для чистых редукторов, в то время как Redux разделяет их.

Это означает, что нельзя записывать события Re-frame и легко их воспроизводить.

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

Разница заключается в связывании промежуточного программного обеспечения с редукторами. Когда промежуточное ПО завершается в Re-frame, они напрямую передают результат обработчикам событий (редукторам), этот результат не записывается как событие / действие, поэтому не может быть воспроизведен. Я бы сказал, что Redux просто ставит перехватчики между ними в форме действий, поэтому я считаю, что технически могу достичь того же, что делает Re-frame, с большей гибкостью за счет большего количества наборов (создания действий) и, вероятно, большего пространства для этого. неправильно.

В конце концов, Redux не мешает мне реализовать тот же подход, что и в Re-frame. Я могу создать любую функцию, которая принимает редуктор в качестве параметра, выполнять в ней некоторую обработку, а затем напрямую вызывать функцию редуктора с результатом и называть ее промежуточное ПО редуктора или что-то еще. Это как раз о том, хочу ли я сделать это немного проще за счет потери DevTools и большей связи.

Мне все еще нужно подумать, если есть значительное преимущество в подходе Re-frame перед некоторым упрощением (уменьшением количества действий). Я готов доказать свою неправоту, поскольку уже сказал, что прочитал, чтобы быстро это прочитать.

На самом деле, я был немного неправ. Разница не в реализациях Redux и Re-frame, а в том, «куда вы помещаете свои побочные эффекты».

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

Но на самом деле - разница есть. @gaearon очень хорошо это объяснил: Redux ожидает, что ваши редукторы будут чистыми, иначе функция воспроизведения

Re-frame также утверждает, что обработчики событий чистые, но даже в Readme упоминается, что HTTP-запросы инициируются в обработчиках событий. Мне это немного непонятно, но похоже, что их ожидания разные: вы можете поместить свои побочные эффекты в обработчики событий.

Тогда интересно, как они подходят к воспроизведению (и это то, что я спросил на https://github.com/Day8/re-frame/issues/86).

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

Горячая перезагрузка все еще может работать, за исключением того, что она не может повлиять на «прошлые» события - только создает новую ветвь состояния во времени.

Так что разница в дизайне, вероятно, небольшая, но для меня она важна.

Я думаю, что они выполняют HTTP-запросы, составляя чистые обработчики событий с промежуточным ПО с побочными эффектами. Эта композиция возвращает новый обработчик событий, который больше не является чистым, но внутренний остается чистым. Я не думаю, что они предлагают создавать обработчики событий, которые смешивают мутацию app-db (состояние) и побочные эффекты. Это делает код более тестируемым.

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

Я думаю, что могу сказать, что промежуточное ПО Re-frame является проактивным, в то время как Redux допускает реактивность (любой специальный редуктор может прослушивать результат промежуточного ПО / ac). Я начинаю понимать, что AC в основном используется в Flux и промежуточном программном обеспечении - это то же самое, что и контроллеры.

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

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

@merk Я постараюсь написать об этом, вероятно, в понедельник, так как я, вероятно, не приеду сюда в выходные.

Документы по переформатированию рекомендуют разговаривать с сервером в обработчиках событий: https://github.com/Day8/re-frame#talking -to-a-server

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

Хм, я не вижу отличий от Redux:

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

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

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

@vladap Подводя итог разнице, как я ее вижу:

1.

  • в Redux вы инициируете запрос к серверу в создателе действий (см. ответ @gaearon на исходный вопрос этой проблемы); ваши редукторы должны быть чистыми
  • в Re-frame вы делаете это в обработчике событий (редукторе); ваши обработчики событий (редукторы) могут иметь побочные эффекты

2.

  • В первом случае ваш поток управления разделен между AC и редуктором.
  • Во втором случае весь ваш поток управления находится в одном месте - обработчике событий (редукторе).

3.

  • Если ваш поток управления разделен - вам нужна координация между AC и reducer (даже если это неявно): AC считывает состояние, созданное reducer; reducer предполагает правильное упорядочивание событий из AC. Технически вы вводите соединение переменного тока и редуктора, потому что изменения в редукторе могут также повлиять на переменный ток и наоборот.
  • Если ваш поток управления находится в одном месте - вам не нужна дополнительная координация + вы можете использовать такие инструменты, как FSM или Statecharts, в ваших обработчиках / редукторах событий


    1. Redux-способ применения чистых редукторов и перенос побочных эффектов в AC позволяет выполнять горячую перезагрузку и воспроизведение

  • Переделанный способ появления побочных эффектов в обработчиках событий (редукторах) закрывает дверь для воспроизведения, как это делается в Redux (путем переоценки редукторов), но другие (вероятно, менее мощные) параметры все еще возможны - как упоминалось в @ tomkis1

Что касается разницы в опыте разработки - она ​​появляется только тогда, когда у вас много асинхронных вещей (таймеры, параллельные HTTP-запросы, веб-сокеты и т. Д.). Что касается синхронизации, то все то же самое (поскольку в AC нет потока управления).

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

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

1.

в Redux вы инициируете запрос к серверу в создателе действий (см. ответ @gaearon на исходный вопрос этой проблемы); ваши редукторы должны быть чистыми
в Re-frame вы делаете это в обработчике событий (редукторе); ваши обработчики событий (редукторы) могут иметь побочные эффекты

Документ, который я выделил жирным шрифтом, явно говорит, что я должен инициировать вызов webapi в обработчике событий, а затем отправить событие при его успехе. Я не понимаю, как я могу обработать это отправленное событие в том же обработчике событий. Это успешное событие отправляется на маршрутизатор (я думаю, как диспетчер), и мне нужен еще один обработчик событий для его обработки. Этот второй обработчик событий является чистым и эквивалентен Redux reducer, первый эквивалент ActionCreator. Для меня это то же самое, или я явно упускаю какую-то важную мысль.

Что касается разницы в опыте разработки - она ​​появляется только тогда, когда у вас много асинхронных вещей (таймеры, параллельные HTTP-запросы, веб-сокеты и т. Д.). Что касается синхронизации, то все то же самое (поскольку в AC нет потока управления).

Мы не согласны с этим. Асинхронные вещи на самом деле не имеют значения, вы не можете воспроизвести их в Redux, поэтому у вас нет опыта разработки здесь. Разница появляется, когда у вас много сложных чистых редукторов. В Redux вы можете взять состояние A, некоторую последовательность действий, редуктор, воспроизвести его и получить состояние B. Вы сохраните все то же самое, но вы измените код редуктора и перезагрузите его, воспроизведите ту же последовательность действий из состояния A и вы получить состояние C, сразу увидев влияние изменения вашего кода. Вы не можете сделать это с помощью подхода

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

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

Ленивая оценка / реактивность - это базовый метод перемещения кода с побочными эффектами как можно дальше от чистого кода. Ленивая оценка - это то, как Haskell, наконец, применил непрактичную концепцию функциональной чистоты, чтобы получить программу, которая делает что-то практическое, потому что полностью чистая программа не может сделать много. Используя монадическую композицию и другие, вся программа создается как поток данных, и чтобы последний шаг в конвейере оставался чистым и никогда не вызывал побочных эффектов в вашем коде, программа возвращает описание того, что должно быть сделано. Он передается среде выполнения, которая выполняет его лениво - вы не запускаете выполнение императивным вызовом. По крайней мере, так я понимаю функциональное программирование с высоты птичьего полета. У нас нет такой среды выполнения, мы моделируем ее с помощью ActionCreators и реактивности. Отказ от ответственности, не принимайте это как должное, это то, что я понял, не так много читая о ФП, является здесь авторитетом. Возможно, я ушел, но я действительно считаю, что понял суть.

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

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

Этот второй обработчик событий является чистым и эквивалентен Redux reducer, первый эквивалент ActionCreator.

Я не думаю, что первый эквивалент Action Creator. Иначе зачем вам вообще нужны Action Creators? Можно ли сделать то же самое в редукторе?

Вы не можете сделать это с помощью подхода

Согласен. Именно это я имел в виду, когда писал «другие (возможно, менее мощные) варианты».

Я не думаю, что первый эквивалент Action Creator. Иначе зачем вам вообще нужны Action Creators? Можно ли сделать то же самое в редукторе?

ActionCreators используются для изоляции побочных эффектов от чистого приложения (которое можно воспроизвести). Поскольку Redux разработан, вы не можете (не должны) выполнять побочные эффекты в редукторах. Вам нужна другая конструкция, в которую можно поместить побочный код. В Redux эти конструкции представляют собой AC или промежуточное ПО. Единственное различие, которое я вижу в повторном кадре, заключается в том, что делают промежуточные программы, и в этом повторном кадре нет промежуточного программного обеспечения для преобразования событий (действий) до запуска обработчиков.

Вместо AC / промежуточного программного обеспечения на самом деле можно использовать все, что угодно - служебный модуль, службы (например, службы Angular), вы можете создать его как отдельную библиотеку, которую вы затем взаимодействуете с помощью AC или промежуточного программного обеспечения и транслируете ее API в действия. Если вы спросите меня, AC - не самое подходящее место для размещения этого кода. AC должны быть просто создателями / фабриками, конструирующими объекты действий из аргументов. Если бы я хотел быть пуристом, было бы лучше разместить побочный код строго в промежуточном программном обеспечении. ActionCreator - плохой термин для обозначения ответственности контроллера. Но если этот код представляет собой просто однострочный вызов webapi, то есть тенденция просто поместить его в AC, и для простого приложения это может быть нормально.

AC, промежуточное ПО, сторонние библиотеки образуют слой, который я называю сервисным слоем. Исходный Facebook WebApiUtils будет частью этого уровня, так же, как если бы вы использовали response-router и слушали его изменения и переводили их в действия для обновления Redux.state, Relay можно использовать как уровень обслуживания (чем использовать Redux для приложения / просмотра штат). Поскольку большинство сторонних библиотек в настоящее время запрограммированы, они плохо работают с воспроизведением. Очевидно, я не хочу переписывать все с нуля, чтобы это было. Как мне нравится думать об этом, так это то, что эти библиотеки, службы, утилиты и т. Д. Расширяют среду браузера, образуя вместе платформу, поверх которой я могу создавать свое воспроизводимое приложение. Эта платформа полна побочных эффектов, на самом деле это то место, куда я намеренно перемещаю побочные эффекты. Я перевожу API этой платформы на действия, и мое приложение косвенно подключается к этой платформе, слушая эти объекты действий (пытаясь имитировать подход Haskell, который я пытался описать в сообщении выше). Это своего рода хитрость в функциональном программировании, позволяющая достичь чистоты (возможно, не в абсолютном академическом смысле), но вызвать побочные эффекты.

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

Весь этот бизнес действий похож по концепции на события DOM. Я действительно не хотел бы смешивать свой бизнес-код с кодом, как браузер обнаруживает событие mousemove, и я уверен, что вы тоже не станете. К счастью, я привык работать поверх addListener ('mousemove', ...), потому что эти обязанности хорошо разделены. Дело в том, чтобы добиться этого хорошего разделения задач и для моей бизнес-логики. ActionCreators, промежуточное ПО - инструменты для этого.

Представьте, что я пишу приложение для устаревшего webapi, который не соответствует потребностям моего бизнеса. Чтобы получить необходимые данные, мне пришлось бы вызвать пару конечных точек, использовать их результат для создания следующих вызовов, а затем объединить все результаты в каноническую форму, которую я бы использовал в Redux.state. Эта логика не просочилась бы в мои редукторы, чтобы мне не приходилось снова и снова преобразовывать данные в моих редукторах. Это было бы в промежуточном программном обеспечении. По сути, я бы изолировал беспорядочный устаревший api и разработал свое бизнес-приложение, так как оно было бы написано против хорошего api. После этого, возможно, я получу изменение, чтобы перестроить наш webapi, а затем просто перепишу свое промежуточное ПО.

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

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

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

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

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

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

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

Более опытные Reduxers могут иметь другое мнение по этому поводу ( @johanneslumpe , @acdlite , @emmenko ...)

@vladap Думаю, этот разговор слишком далеко ушел от темы этого номера. Вы уже упомянули суть моих комментариев здесь:

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

Быстрый пример:

Ситуация 1. Где-то на странице у вас есть список из 10 лучших авторов блогов. Теперь вы удалили категорию сообщений в блоге. При успешном удалении вам необходимо обновить этот список авторов с сервера, чтобы он был актуальным.

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

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

Параметры:
А) ставим эту логику в AC. Затем AC коснется CategoryApi (для удаления), будет читать из userList состояния и commentList состояния (чтобы проверить, какие списки присутствуют в текущем состоянии), поговорите с UserListApi и / или CommentListApi (для обновления списков) + отправка TOP_AUTHORS_UPDATED и / или TOP_COMMENTS_UPDATED . Таким образом, он будет касаться трех разных доменов.

Б) помещаем в обработчики событий userList и commentList . Оба обработчика будут прослушивать событие DELETE_CATEGORY_SUCCESS , затем вызывать свою службу API и, в свою очередь, отправлять событие TOP_AUTHORS_UPDATED / TOP_COMMENTS_UPDATED . Таким образом, каждый обработчик касается только служб / состояния своего собственного домена.

Этот пример, вероятно, слишком упрощен, но даже на этом уровне все становится менее привлекательно с вызовами API в AC.

Разница заключается в том, что, в отличие от обработчиков событий в перефрейминге, ActionCreators проактивно обрабатывают бизнес-логику. И только один AC может быть выполнен на основе события DOM. Однако этот AC верхнего уровня может вызывать другие AC. Могут быть отдельные AC для UserListApi и CommentListApi, чтобы домены лучше разделялись, но всегда должен быть AC (как контроллер), который их связывает. Эта часть представляет собой классический императивный код, в то время как рефрейминг полностью основан на событиях. Немного поработав, его можно заменить предпочтительным подходом, основанным на событиях, наблюдаемыми, cps и т. Д.

@vladap Было бы интересно посмотреть, жизнеспособен ли другой подход: когда редукторы могут иметь побочные эффекты, но также могут изолировать их, чтобы их можно было игнорировать при воспроизведении.

Скажем, подпись редукторов изменится на: (state, action) => (state, sideEffects?) где sideEffects - это закрытие. Затем, в зависимости от контекста, фреймворк может либо оценивать эти побочные эффекты, либо игнорировать их (в случае воспроизведения).

Тогда пример из исходного описания проблемы будет выглядеть так:

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      return [state, () => {LoginApiCall.login(action.user)}];
    case LOGGED_FAILED:
      // do something
      return state;
    case LOGGED_SUCCESSFULLY:
      // do something else
     return state;
    default:
      return state;
  }
}

(все остальное остается почти таким же)

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

Вы также можете написать обычный FSM или код, подобный диаграмме состояний (с составом редукторов / обработчиков событий)

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

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

Скажем, подпись редукторов изменится на: (state, action) => (state, sideEffects?), Где sideEffects - это закрытие. Затем, в зависимости от контекста, фреймворк может либо оценивать эти побочные эффекты, либо игнорировать их (в случае воспроизведения).

На самом деле это похоже на то, что я слышал от Элма с (State, Action) => (State, Request?) . Я еще не видел примеров, но если кто-то хочет изучить это, не стесняйтесь.

Еще один подход, который, возможно, вдохновляет, - это реализация eventourcing поверх модели Actor - Akka Persistance , PersistentFSM . Я не говорю, что это лучше, но знать другие попытки неплохо. Актеры могут использовать побочный эффект, но, если я правильно помню, иногда требуется написать явный код, как воспроизвести.

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

Если бы я отложил функцию воспроизведения, как бы я написал свой бизнес-код? Я бы хотел, чтобы моя бизнес-логика была перед журналом действий и была в одном месте. Конечные автоматы бизнес-логики, или что бы я ни использовал, будут принимать все сложные сложные решения, приводящие к уже разрешенному потоку ФАКТОВ (действий). Редукторы обычно являются простыми средствами обновления, и их основная ответственность будет заключаться в том, как применить полезную нагрузку действия к Redux.state. Они могут иметь некоторую логику, но в некотором смысле иначе интерпретировать факты, чтобы получить другой вид потока фактов (действий), чтобы представить его пользователю.

Чтобы разделить обязанности, мне тоже нравится думать об этом так. Какой код потенциально может быть запрошен для повторной реализации на сервере? Fe для лучшей поддержки медленных устройств, перемещая вычислительную дорогостоящую бизнес-логику, возможно, из соображений безопасности, скрывая интеллектуальные свойства. Я не могу так легко перенести логику с редукторов на сервер, потому что она привязана к слою представления. Если перед журналом действий написана сложная бизнес-логика, я могу заменить ее вызовами REST, транслируемыми в тот же поток действий, и мой уровень представления должен работать (возможно).

Обратной стороной является то, что в отличие от Akka Persistance у меня нет возможности воспроизвести этот код.

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

@gaearon Спасибо, что упомянули Вяза. Нашел этот разговор - https://gist.github.com/evancz/44c90ac34f46c63a27ae - с похожими идеями (но гораздо более продвинутыми).

Они вводят понятие задач (http, db и т.д.) и эффектов (просто очередь задач). Так что ваш «редуктор» действительно может возвращать новое состояние и эффекты.

Классная вещь в этом (и я не думал об этом таким образом) - это то, что если вы можете собирать задачи, связывая свои редукторы, а затем применять какую-то функцию к этому списку. Скажем, вы можете пакетировать http-запросы, оборачивать запросы БД транзакциями и т. Д.

Или вы могли бы сказать «менеджер воспроизведения», что не более чем просто наложение эффектов.

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

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

Один из вариантов - начать обратный отсчет истечения срока в AC входа в систему, и я предполагаю, что он работает бесконечно и умирает вместе с приложением, или выход из системы должен его очистить. Когда срабатывает интервал, он делает то, что необходимо для подтверждения токена, а если истекло, он отправляет действие LOGIN_EXPIRED, а прослушивающий редуктор очищает сеанс пользователя и меняет местоположение, что, в свою очередь, запускает переход маршрутизатора к / login.

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

Вы можете сохранить его в Redux.state, но это состояние непостоянно. С большим состоянием мы можем получить те же проблемы, что и программирование с глобальными переменными. И проверить это сложно, наверное, невозможно. Редукторы легко протестировать, но потом можно доказать, что это работает, когда только один редуктор обновляет этот конкретный ключ в объекте состояния. Это может плохо сказаться в большом проекте с большим количеством разработчиков разного уровня подготовки. Кто угодно может решить написать редуктор и подумать, что некоторая часть состояния подходит для обновления, и я не могу представить, как протестировать все редукторы вместе во всех возможных последовательностях обновления.

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

@vladar @vladap

Вау, этот Elm gist чертовски крут! Это также напоминает мне архитектуру «драйверов»

@merk На самом деле, автономный LOGIN_EXPIRED не сильно повлияет на воспроизведение, потому что он попадет в конец журнала действий и не будет немедленно обработан воспроизведением, и, возможно, он вообще не попадет в журнал действий - я не знаю как именно реализован повтор.

@gaeron Похоже, что на высоком уровне Elm и Cycle реализуют шаблон, который я пытался описать, если я правильно его понимаю. У меня есть браузер, который дает мне базовую платформу, я расширяю эту платформу своим уровнем обслуживания (http, db ...), чтобы создать платформу для своего приложения. Затем мне нужен какой-то клей, который взаимодействует со слоем службы и позволяет моему чистому приложению косвенно связываться с ним, чтобы мое приложение могло описывать то, что оно хочет делать, но не выполняло его само (сообщения, отправленные драйверам Cycle, Elm Effects имея список задач). Я мог бы сказать, что мое приложение создает «программу» с использованием структур данных (напоминает мне о коде в качестве мантры данных), которая затем передается на уровень обслуживания для выполнения, и мое приложение интересует только результат этой «программы», который он затем применяет. к своему состоянию.

@merk : Далее. Если токен и срок его действия будут помещены в Redux.state / или где-нибудь в любом другом js-сервисе, он не переносится, когда пользователь открывает приложение на новой вкладке. Я бы сказал, что пользователь ожидает, что он останется в системе. Поэтому я предполагаю, что это состояние должно быть на самом деле в SessionStore.

Даже без этого состояния могут быть разделены, когда истечение срока действия токена не означает немедленную очистку информации о пользователе и переход к / login. Пока пользователь не коснется сервера своими взаимодействиями (у него достаточно кэшированных данных), он может продолжать работать, и действие LOGIN_EXPIRED срабатывает только после того, как он сделает что-то, требующее сервера. Состояние isLogged на основе Redux.state не обязательно означает, что действительный токен существует. Состояние isLogged в Redux.state используется для решения, что отображать, состояние вокруг токена может быть скрыто на уровне представления и поддерживаться исключительно на уровне создателей действий без необходимости писать для него действия и редукторы, кроме тех, которые влияют на уровень представления.

@vladar Думаю, я смогу понять вашу

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

actionCreator() {
  ... getState
  ... some logic L1
  ... dispatch({ACTION1})
}

reducer() {
  case ACTION1
    ... some logic L2
    ... x, y result from L2
    ... some logic L3
    ... resulting in new state
    ... return new state
}

Есть 3 варианта, как запустить асинхронную операцию, используя x, y, вычисленные в reducer. Ни один из них не идеален.

1) Переместите логику от редуктора к создателю действий и уменьшите мощность горячей перезагрузки. Редуктор - это только средство обновления тупого состояния или имеет логику L3 и далее.

2) Сохраните x, y в Redux.state, загрязняя его временными / переходными значениями и в основном используя его как глобальный канал связи между редукторами и создателями действий, что я не уверен, что мне нравится. Я думаю, что это нормально, если это актуальное состояние, но не для таких ценностей. Создатель действия изменится на:

actionCreator() {
  ... getState
  ... some logic L1
  ... dispatch({ACTION1})
  // I assume I get updated state if previous dispatch is sync.
  // If previous dispatch is async it has to be chained properly.
  // If updated state can't be received here after a dispatch
  // then I would think there is a flaw.
  ... getState
  ... asyncOperation(state.x, state.y)
}

3) Сохраните x, y в состояние, используйте его в качестве реквизита в некотором компоненте и туннелируйте его через весь цикл уровня представления, используя componentWillReceiveProps, запускающий новый создатель действия и операцию async. Худший вариант, если вы спросите меня, - повсюду распространять бизнес-логику.

@vladar
Как правило, не имеет значения, где запускается async, поскольку результат этой операции упаковывается как действие, отправляется и применяется редуктором. Будет работать так же. То же самое, что и в случае с vanilla flux, следует ли запускать async в создателях действий или в магазинах.

Для этого потребуется, чтобы Redux мог идентифицировать и игнорировать эти асинхронные вызовы при воспроизведении, что именно вы предлагаете.

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

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

Рассмотрим обобщенный поток перехода между состояниями (в мире автоматов):

  1. Событие прибывает
  2. Учитывая текущее состояние, FSM вычисляет новое целевое состояние
  3. FSM выполняет операции по переходу из текущего состояния в новое состояние

Обратите внимание, что текущее состояние важно! Одно и то же событие может привести к разным результатам в зависимости от текущего состояния. И как результат к разным последовательностям операций (или аргументов) на шаге 3.

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

Но как насчет асинхронных действий? Большинство примеров Flux с асинхронными действиями заставляют вас думать, что он инвертирован:

  1. AC выполняет асинхронную операцию (независимо от текущего состояния)
  2. AC отправляет действие в результате этой операции
  3. Reducer / Store обрабатывает это и изменяет состояние

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

  1. AC получает текущее состояние до отправки действия
  2. AC отправляет действие
  3. AC читает новое состояние после действия
  4. Заданное состояние до действия и после - он решает, какие операции выполнять (или какие значения передать в качестве аргументов)

Твой второй пример. Я не говорю, что все эти шаги требуются постоянно. Часто некоторые из них можно опустить. Но в сложных случаях - так действовать придется. Это обобщенный поток любого перехода между состояниями. И это также означает, что границы вашего конечного автомата переместились в AC, а не в reducer / store.

Но перемещение границ вызывает другие проблемы:

  1. Если бы асинхронные операции были в хранилище / редукторе - у вас могло бы быть два независимых конечных автомата, реагирующих на одно и то же событие. Итак, чтобы добавить новую функцию - вы можете просто добавить хранилище / редуктор, который реагирует на какое-то существующее событие, и все.
    Но с операциями в AC - вам нужно будет добавить хранилище / редуктор, а также пойти и отредактировать существующий AC, добавив туда асинхронную часть логики. Если он уже содержал асинхронную логику для какого-то другого редуктора ... это не круто.
    Очевидно, что поддерживать такой код сложнее.
  2. Поток управления вашим приложением отличается в случае синхронных и асинхронных действий. Для действий синхронизации reducer контролирует переход, в случае async - AC эффективно контролирует все переходы, которые потенциально могут быть вызваны этим событием / действием.

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

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

К сожалению, «побочная» часть перехода между состояниями все еще является частью перехода между состояниями, а не какой-то отдельной независимой частью логики. Поэтому для меня (state, action) => (state, sideEffects) выглядит естественнее. Да, это уже не сигнатура "уменьшить"%) Но сфера фреймворка - это не преобразования данных, а переходы состояний.

То же самое, что и в случае с vanilla flux, следует ли запускать async в создателях действий или в магазинах.

Да, но Flux не запрещает вам иметь асинхронные вещи в магазинах, если вы напрямую используете dispatch () в асинхронных обратных вызовах и изменяющем состоянии. Они отчасти непредубежденны, даже если в большинстве реализаций рекомендуется использовать AC для асинхронной обработки. Лично я думаю, что это напоминание о MVC, потому что удобно мысленно рассматривать AC как контроллеры, а не делать мысленный переход к конечным автоматам.

@vladar

(state, action) => (state, sideEffects)

Просто думаю. Это означает, что нам нужно будет изменить тип возвращаемого значения функции combReducers на [state, SideEffects] или [state, [SideEffects]] и изменить его код, чтобы объединить побочные эффекты от редукторов. Исходный (состояние, действие) => тип редуктора состояния может оставаться поддерживаемым, потому чтоcommonReducers будет приводить (состояние) возвращаемый тип к [состояние] или [состояние, []].

Затем нам нужно выполнить и отправить SideEffects где-нибудь в Redux. Это можно сделать в Store.dispatch (), который применит новое состояние, выполнит список SideEffects и отправит его. Я думаю, сможем ли мы попасть в какую-нибудь приятную бесконечную рекурсию.

В качестве альтернативы он может быть выполнен где-то на уровне промежуточного программного обеспечения, но тогда я думаю, что Store.dispatch должен будет вернуть список SideEffects вместе с действием.

@vladar

Да, но Flux не запрещает вам иметь асинхронные вещи в магазинах, если вы напрямую используете dispatch () в асинхронных обратных вызовах и изменяющем состоянии. Они отчасти непредубежденны, даже если в большинстве реализаций рекомендуется использовать AC для асинхронной обработки. Лично я думаю, что это напоминание о MVC, потому что удобно мысленно рассматривать AC как контроллеры, а не делать мысленный переход к конечным автоматам.

Я согласен с тем, что относиться к ActionCreators как к контроллерам - не лучший настрой. Это причина маршрута, по которой я в конечном итоге думаю, что для сложной логики мне нужно вернуть управление от редуктора обратно в AC. Мне это не нужно. Если AC обрабатывает async, они должны быть максимально похожи на обработчики запросов - выполняться на основе решения, принятого в другом месте, и этот запрос не должен пересекать домены.

Чтобы избежать их роли в качестве контроллеров, единственный вариант сейчас - маршрутизировать логику через интеллектуальные компоненты и использовать ActionCreators только для выполнения начального шага логики в прямом ответе на пользовательский интерфейс. Фактическое первое решение принимается интеллектуальным компонентом на основе Redux.state и / или локального состояния компонента.

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

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

Акторы делают то же самое, они могут выполнять async и отправлять сообщение (объект действия) другому актору или отправлять его себе, чтобы продолжить в своем FSM.

Я закрываюсь, потому что здесь, похоже, нет ничего действенного. Документ по асинхронным действиям должен ответить на большинство вопросов. Дальнейшие теоретические обсуждения не кажутся мне ценными без реального кода, который мы могли бы запустить. ;-)

@gaearon, можете ли вы добавить реальный код, реализующий правильный способ обработки вызовов API, в один из примеров с redux (за пределами «реального» примера, который использует промежуточное ПО)?

У меня все еще есть вопросы о фактическом размещении после прочтения всех документов и этой проблемы несколько раз. Мне не удается понять, в какие файлы помещать эти побочные эффекты https://github.com/rackt/redux/issues/291#issuecomment -123010379 . Я думаю, что это «правильный» пример из этой темы.

Заранее благодарим за любые разъяснения в этой области.

Хочу отметить важную мысль о создателях нечистых действий:

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

Приложение, которое я разрабатываю, будет иметь как минимум две версии: одну для работы на Raspberry Pi на месте, а другую на портале. Могут быть даже отдельные версии для публичного портала / портала, управляемого клиентом. Неизвестно, придется ли мне использовать разные API, но я хочу подготовиться к этой возможности как можно лучше, и промежуточное ПО позволяет мне это делать.

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

@ jedwards1211 Возможно, вас заинтересуют https://github.com/redux-effects/redux-effects, если вы еще не проверили их, а также обсудите в # 569.

@gaearon cool, спасибо, что указали на это! В любом случае, использовать мое собственное промежуточное ПО пока что не так уж сложно :)

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

@gregwebs Мне нравится redux-side-effect , хотя было бы неудобно и с заменяемой логикой управления, потому что возникает вопрос, как я могу добавить функцию sideEffect в свои модули редуктора, когда существует несколько различных конфигураторов хранилища модули для использования в разных сборках.

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

Что-то вроде https://github.com/rackt/redux/pull/569/ близко к идеалу для того, как я хочу работать, хотя, конечно, я не хочу использовать его в производственном проекте, если он не станет стандартной частью API.

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

sideEffectMiddleware.js :

export default store => next => action => {
  let sideEffects = [];
  action.sideEffect = callback => sideEffects.push(callback);
  let result = next(action);
  sideEffects.forEach(sideEffect => sideEffect(store));
  return result;
};

Есть несколько способов применить простой подход sideEffect. Пожалуйста, попробуйте свои варианты и сообщите об этом

Я реорганизовал свой код, чтобы использовать описанный выше подход sideEffectMiddleware , и мне очень нравится полученная организация кода. Приятно не импортировать функцию sideEffect откуда угодно в мои редукторы, она просто входит в action .

@ jedwards1211 Я опубликовал ваш код как actionSideEffectMiddleware в версии 2.1.0 пакета.

@gregwebs круто! Не возражаете указать меня как соавтора?

@ jedwards1211 вы добавлены. Может быть, вы добавите соответствующую поддержку воспроизведения :) Я пока не могу воспользоваться этим, где я использую это.

@gaearon Я так понимаю, что записанные и воспроизведенные действия вообще не проходят через промежуточное ПО, верно?

Они это делают, просто к моменту записи они уже являются простыми объектами, поэтому промежуточное ПО обычно не вмешивается.

@gaearon Хммм. Так что я на самом деле не пробовал эту функцию, но ... Я предполагаю, что некоторые промежуточные программы побочных эффектов также могут нарушить запись / воспроизведение? Возьмем, к примеру, redux-effects-fetch , он все равно будет выполнять запрос ajax для действия FETCH простого объекта:

function fetchMiddleware ({dispatch, getState}) {
  return next => action =>
    action.type === 'FETCH'
      ? fetch(action.payload.url, action.payload.params).then(checkStatus).then(deserialize, deserialize)
      : next(action)
}

Если записывающее устройство удаляет обратные вызовы [success, failure] из steps действия FETCH, то промежуточное ПО redux-effects не отправляет никаких действий, изменяющих состояние, но все же никто не захочет воспроизведение, чтобы вызвать кучу запросов ajax.

Есть ли способ отделить «чистое» промежуточное ПО (которое никогда не отправляет дополнительные действия), такое как redux-logger от «нечистого» промежуточного ПО (которое может отправлять действия)?

К сожалению, я не изучал redux-effects внимательно, поэтому не могу ответить на ваш вопрос.

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

Это не так. Ожидается, что devTools() будет помещен _after_ applyMiddleware в цепочку композиции энхансера, так что к моменту достижения действия это будет «финальное» действие, не нуждающееся в дальнейшей интерпретации.

О, я понимаю, тогда после того, как промежуточное ПО выполняет побочный эффект, оно может просто удалить любые поля из действия / настроить его так, чтобы оно не запускало побочный эффект, когда оно возвращается через промежуточное ПО при воспроизведении, а затем оно должно работать с инструментами разработчика, верно?

Да, похоже, это хороший способ.

Отлично, спасибо, что просветили меня!

@gaearon @vladar @ jedwards1211 Возможно, вас заинтересует https://github.com/salsita/redux-side-effects. По сути, это реализация elmish (state, action) => (state, sideEffects) но вместо того, чтобы редуктор возвращал кортеж (что, как вы уже заметили, невозможно), это эффекты доходности.
Вскоре поиграл, горячая перезагрузка и повтор вроде в порядке. Я пока не вижу, чтобы какая-либо функция редукции сломалась, но, возможно, некоторые из старших редукторов могут что-то найти. :)
Для меня это выглядит действительно важным дополнением к стеку redux, поскольку оно позволяет логике быть в основном в редукторах, что делает их эффективными и тестируемыми конечными автоматами, которые делегируют эффекты (действия перехода, результат которых не зависит только от текущего состояния) внешним службам ( очень похоже на архитектуру Elm, эффекты и услуги).

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

@gaearon Я считаю, что @minedeljkovic имел в виду

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

в качестве основного преимущества перед традиционным подходом, потому что https://github.com/yelouafi/redux-saga/ очень похож на то, как работает redux-thunk , вместо некоторого синтаксического сахара поверх него, что упрощает длительные транзакции .

https://github.com/salsita/redux-side-effects, с другой стороны, больше похожа на архитектуру Elm.

Да, redux-saga и redux-side-effects используют генераторы только для объявления побочных эффектов, сохраняя чистоту sagas и redux соответственно. Это похоже.

Две причины, по которым я предпочитаю это в редукторах:

  1. У вас есть явный доступ к текущему состоянию, которое может повлиять на то, как должен быть объявлен эффект (я думаю, это был один из моментов @vladar во время этого обсуждения)
  2. Нет новой концепции (Сага в редукс-саге)

Мой комментарий в https://github.com/rackt/redux/issues/1139#issuecomment -165419770 все еще действителен, поскольку redux-saga не решит эту проблему, и нет другого способа решить эту проблему, кроме принятия модели. используется в архитектуре Вязов.

Я сформулировал простую суть, которая пытается подчеркнуть мои соображения о побочных эффектах редукции: https://gist.github.com/minedeljkovic/7347c0b528110889aa50

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

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

Ограничения архитектуры:

  1. Функциональность CountrySelection должна быть модульной (в духе redux ducks ), чтобы ее можно было повторно использовать в нескольких частях приложения (например, также в части приложения «Администрирование продукта», где необходимо указать страну производства)
  2. Срез состояния CountrySelection не следует рассматривать как глобальный (чтобы находиться в корне состояния redux), но он должен быть локальным для модуля (и контролироваться этим модулем), который его вызывает.
  3. Основная логика этой функциональности должна включать как можно меньше движущихся частей (в моей сути эта основная логика реализована только в редукторах (и только в наиболее важной части логики). Компоненты должны быть тривиальными, так как все компоненты, управляемые редуктором, должны быть :). Их единственная ответственность будет заключаться в отображении текущего состояния и отправке действий.)

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

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

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

@ tomkis1 , я хотел бы высказать ваше мнение, использую ли я здесь вашу библиотеку или злоупотребляю

(В этой сущности есть немного другая реализация https://gist.github.com/minedeljkovic/9d4495c7ac5203def688, где не используются globalActions. Это не важно для этой темы, но, возможно, кто-то захочет прокомментировать этот шаблон)

@minedeljkovic Спасибо за суть. Я вижу в вашем примере одну концептуальную проблему. redux-side-effects предназначен для использования только при побочных эффектах. В вашем примере, однако, нет побочных эффектов, а скорее долгоживущая бизнес-транзакция, и поэтому redux-saga гораздо более подходит. @slorber и @yelouafi могут пролить свет на это.

Другими словами, меня больше всего беспокоит синхронность dispatching нового действия в редукторе (https://github.com/salsita/redux-side-effects/pull/9#issuecomment-165475991):

yield dispatch => dispatch({type: COUNTRY_SELECTION_SUCCESS, payload: state.selectedCountryId});

Я считаю, что в @slorber появился термин "побочный эффект бизнеса", и это как раз ваш случай. redux-saga блестит в решении этой конкретной проблемы.

@mindjuice Я не совсем уверен, что понимаю ваш пример, но мне нравится пример подключения, который я привел здесь: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Шаблон саги также позволяет сделать некоторые неявные вещи явными.
Например, вы просто описываете, что произошло, запуская действия (я предпочитаю события, потому что для меня они должны быть в прошедшем времени), а затем вступают в силу некоторые бизнес-правила и обновляют пользовательский интерфейс. В вашем случае отображение ошибки неявное. С сагой, при подтверждении, если страна не выбрана, вы, вероятно, отправите действие «NO_COUNTRY_SELECTED_ERROR_DISPLAYED» или что-то в этом роде. Я предпочитаю, чтобы это было полностью откровенным.

Также вы могли видеть сагу как связующее звено между утками.
Например, у вас есть duck1 и duck2 с локальными действиями для каждого из них. Если вам не нравится идея связать двух уток (я имею в виду, что одна утка будет использовать ActionsCreators второй утки), вы можете просто позволить им описать, что произошло, а затем создать сагу, которая связывает некоторые сложные бизнес-правила между 2 утки.

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

Предположим, у вас есть асинхронный action() и ваш код должен либо указывать на ошибку, либо показывать результат.

Первый подход заключался в том, чтобы сделать это как

// the action call
action().then(dispatch(SUCCESS)).catch(dispatch(FAILURE))

// the reducer
case SUCCESS:
    state.succeeded = true
    alert('Success')

case FAILURE:
    state.succeeded = false
    alert('Failure')

Но оказывается, что это не Redux-way, потому что теперь редуктор содержит «побочные эффекты» (я не знаю, что это должно означать).
Короче говоря, Правильный способ - переместить эти alert() куда-нибудь из редуктора.

Это может быть компонент React, который вызывает это action .
Итак, теперь мой код выглядит так:

// the reducer
case SUCCESS:
    state.succeeded = true

case FAILURE:
    state.succeeded = false

// the React component
on_click: function()
{
    action().then
    ({
        dispatch(SUCCESS)

        alert('Success')
        // do something else. maybe call another action
    })
    .catch
    ({
        dispatch(FAILURE)

        alert('Failure')
        // do something else. maybe call another action
    })
}

Это правильный способ сделать это сейчас?
Или мне нужно изучить больше и применить стороннюю библиотеку?

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

@ halt-hammerzeit Сам Redux очень простой; сбивающая с толку фрагментация возникает из-за разных потребностей или мнений об интеграции / разделении побочных эффектов редукторов при использовании Redux.

Кроме того, фрагментация происходит из-за того, что на самом деле не так уж и сложно добиться побочных эффектов с Redux. Мне потребовалось всего около 10 строк кода, чтобы развернуть собственное промежуточное ПО с побочными эффектами. Так что примите свою свободу и не беспокойтесь о «правильном» пути.

@ jedwards1211 «Сам Redux» не имеет никакой ценности или смысла и не имеет никакого смысла, потому что его основная цель - решать повседневные проблемы разработчиков Flux. Это подразумевает AJAX, красивую анимацию и все прочие _ необычные и ожидаемые_ вещи.

@ halt-hammerzeit ты прав. Redux, безусловно, кажется, предназначен для того, чтобы заставить большинство людей согласиться с тем, как управлять обновлениями состояния, поэтому жаль, что большинство людей не пришли к соглашению о том, как выполнять побочные эффекты.

Вы видели в репо папку "примеры"?

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

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

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

^ что он сказал.

Я бы предпочел, чтобы все великие умы согласились на единственное Праведное решение
и высечь его в камне, чтобы мне не пришлось читать 9000 экранов
нить

В четверг, 7 января 2016 г., Энди Эдвардс [email protected] написал:

Да, я знаю ... все, что я говорю, это то, что эта ветка проиллюстрировала отсутствие
консенсус (по крайней мере, среди людей здесь) о том, как лучше всего выполнять побочные
эффекты.

-
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169751432.

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

В четверг, 7 января 2016 г., Николай Кучумов [email protected] написал:

^ что он сказал.

Я бы предпочел, чтобы все великие умы согласились на единственное Праведное решение
и высечь его в камне, чтобы мне не пришлось читать 9000 экранов
нить

В четверг, 7 января 2016 г., Энди Эдвардс < [email protected]
<_e i = "16">

Да, я знаю ... все, что я говорю, это то, что эта ветка проиллюстрировала отсутствие
консенсус (по крайней мере, среди людей здесь) о том, как лучше всего выполнять побочные
эффекты.

-
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169751432.

Этот и аналогичные потоки существуют потому, что 1% пользователей Redux любят искать более мощные / чистые / декларативные решения для побочных эффектов. Пожалуйста, не создавайте впечатление, что никто не может с этим согласиться. Молчаливое большинство использует мысли и обещания, и в основном им нравится такой подход. Но, как и у любой технологии, у нее есть недостатки и недостатки. Если у вас сложная асинхронная логика, вы можете отказаться от Redux и вместо этого изучить Rx. Шаблон Redux легко выразить в Rx.

Хорошо спасибо

В четверг, 7 января 2016 г., Дэн Абрамов [email protected] написал:

Эта и подобные темы существуют, потому что 1% пользователей Redux любят искать
более мощные / чистые / декларативные решения для побочных эффектов. Пожалуйста не
создается впечатление, что с этим никто не может согласиться. Молчаливое большинство использует
думает и обещает, и в основном довольны таким подходом. Но как и любой
tech у него есть недостатки и компромиссы. Если у вас сложная асинхронная логика
вы можете отказаться от Redux и вместо этого изучить Rx. Шаблон Redux - это
легко выразителен в Rx.

-
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169761410.

@gaearon да, ты прав. И я ценю, что Redux достаточно гибок, чтобы приспособить все наши подходы!

@ halt-hammerzeit взгляните на мой ответ здесь, где я объясняю, почему redux-saga может быть лучше, чем redux-thunk: http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for- асинхронный поток в редукции / 34599594

@gaearon Кстати, ваш ответ на stackoverflow имеет тот же недостаток, что и документация - он охватывает только простейший случай
http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594#34599594
Реальные приложения быстро выходят за рамки простой выборки данных через AJAX: они также должны обрабатывать ошибки и выполнять некоторые действия в зависимости от вывода вызова действия.
Ваш совет - просто dispatch какое-нибудь другое мероприятие и все.
Так что, если мой код хочет alert() пользователя после завершения действия?
Вот где эти thunk s не помогут, а вот обещания помогут.
В настоящее время я пишу свой код, используя простые обещания, и он работает именно так.

Я прочитал соседний комментарий о redux-saga .
Не понимаю, что он делает, лол.
Я не так сильно monads вещами thunk , и мне не нравится это странное слово с самого начала.

Хорошо, тогда я продолжаю использовать Promises в компонентах React.

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

@gaearon Да, вот о чем я говорю: on_click() { dispatch(load_stuff()).then(show_modal('done')) }
Что будет делать.

Я думаю, тебе все еще чего-то не хватает.
См. README для redux-thunk.
Он показывает, как связать создателей действий преобразователя в разделе «Состав».

@gaearon Да, именно это я и делаю:

store.dispatch(
  makeASandwichWithSecretSauce('My wife')
).then(() => {
  console.log('Done!');
});

Это именно то, что я написал выше:

on_click()
{
    dispatch(load_stuff())
        .then(() => show_modal('done'))
        .catch(() => show_error('not done'))
}

Этот раздел README хорошо написан, спасибо

Нет, я не об этом. Взгляните на makeSandwichesForEverybody . Это создатель действия thunk вызывает других создателей действия thunk. Вот почему вам не нужно складывать все по компонентам. См. Также пример async в этом репо для получения дополнительной информации.

@gaearon Но я думаю, что было бы неуместно помещать, скажем, мой модный код анимации в создатель действий, не так ли?
Рассмотрим этот пример:

button_on_click()
{
    this.props.dispatch(load_stuff())
        .then(() =>
        {
                const modal = ReactDOM.getDOMNode(this.refs.modal)
                jQuery(modal).fancy_animation(1200 /* ms */)
                setTimeout(() => jQuery.animate(modal, { background: 'red' }), 1500 /* ms */)
        })
        .catch(() =>
        {
                alert('Failed to bypass the root mainframe protocol to override the function call on the hyperthread's secondary firewall')
        })
}

Как бы вы его правильно переписали?

@ halt-hammerzeit вы можете заставить actionCreator возвращать обещания, чтобы компонент мог отображать какой-то счетчик или что-то еще, используя локальное состояние компонента (в любом случае этого трудно избежать при использовании jquery)

В противном случае вы можете управлять сложными таймерами для управления анимацией с помощью redux-saga.

Взгляните на это сообщение в блоге: http://jaysoo.ca/2016/01/03/managing-processes-in-redux-using-sagas/

О, в этом случае можно оставить его в компоненте.
(Примечание: обычно в React лучше использовать декларативную модель для всего, а не показывать модальные окна императивно с помощью jQuery. Но ничего страшного.)

@slorber Да, я уже возвращаю обещания и прочее из моих "панков" (или как вы их называете; эээ, мне не нравится это странное слово), так что я могу справиться с прядильщиками и тому подобным.

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

Я доволен поддержкой в ​​этой ветке. Кажется, теперь мои сомнения развеялись.

Соответствующее новое обсуждение: https://github.com/reactjs/redux/issues/1528

@gaearon очень

Была ли эта страница полезной?
0 / 5 - 0 рейтинги