Redux: 尝试将API调用放置在正确的位置

创建于 2015-07-20  ·  115评论  ·  资料来源: reduxjs/redux

我正在尝试使登录成功/出错,但是我主要关心的是在哪里可以放置此逻辑。

目前,我正在使用actions -> reducer (switch case with action calling API) -> success/error on response triggering another action

这种方法的问题在于,当我从API调用中调用动作时,reducer无法正常工作。

我错过了什么吗?

减速器

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条评论

这几乎是正确的,但问题是_您不能只打电话给纯粹的动作创建者并期望事情会发生_。 不要忘记,您的动作创建者只是指定_what_需要分派的函数。

// 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.
  }
}

现在,让我们来看您的示例。 我要澄清的第一件事是,如果您仅同步分派单个操作并且没有副作用(对于loginErrorloginRequest为true,则不需要异步dispatch => {}表单) 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 };
}

其次,你的化简应该是_没有副作用的纯函数_。 不要尝试从reducer调用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

最后,如果您发现自己经常像这样编写大型动作创建者,那么为异步调用编写自定义中间件是一个好主意,与promise兼容的异步调用就是您用于异步的东西。

我上面描述的技术(具有dispatch => {}签名的动作创建者)现在已包含在Redux中,但在1.0中将仅作为单独的软件包(称为redux-thunk)提供。 在使用它时,您不妨检出redux-promise-middlewareredux-promise

@gaearon :clap:这是一个了不起的解释! ;)绝对应该将其纳入文档。

@gaearon很棒的解释! :杯:

@gaearon如果您需要执行一些逻辑以确定应该调用哪个API调用,该怎么办? 域逻辑不是应该放在一个地方(归约器)吗? 让行动起来的创作者在我看来听起来像是打破了单一的真理来源。 此外,大多数情况下,您需要根据应用程序状态中的某些值对API调用进行参数设置。 您很可能还希望以某种方式测试逻辑。 我们发现在大型项目中,在Reducers(原子通量)中进行API调用非常有用且可测试

我们发现在大型项目中,在Reducers(原子通量)中进行API调用非常有用且可测试。

这会中断记录/重播。 根据定义,具有副作用的功能比纯功能更难测试。 您可以像这样使用Redux,但这完全违背了它的设计。 :-)

让行动起来的创作者在我看来听起来像是打破了单一的真理来源。

“单一事实来源”意味着数据生活在一个地方,没有独立的副本。 这并不意味着“所有领域逻辑都应该放在一个地方”。

如果您需要执行一些逻辑以确定应该调用哪个API调用,该怎么办? 域逻辑不是应该放在一个地方(归约器)吗?

减速器指定状态如何通过动作转换。 他们不必担心这些动作的起源。 它们可能来自组件,动作创建者,记录的序列化会话等。这就是使用动作的概念之美。

任何API调用(或确定调用哪个API的逻辑)都发生在reducers之前。 这就是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请求(在动作创建者中),并将状态更改为“提交”(在reducer中)。 第二次您不希望这样做。

在当前模型中,您的动作创建者将不得不根据当前状态选择要分发的动作。 如果当前不是“提交”,则发送HTTP请求并调度一个动作,否则,什么也不做,甚至调度其他动作(如警告)。

因此,您的控制流有效地分为两个状态机,其中一个是隐式的并且充满副作用。

我想说,这种Flux方法可以很好地处理典型Web应用程序的大多数用例。 但是,如果您具有复杂的控制逻辑,则可能会出现问题。 现有的所有示例都没有复杂的控制逻辑,因此这个问题有些隐藏。

https://github.com/Day8/re-frame是不同方法的示例,其中它们具有充当唯一FSM的单个事件处理程序。

但是,还原剂会产生副作用。 因此,在这种情况下,他们如何处理“重播”功能很有趣-在https://github.com/Day8/re-frame/issues/86中询问他们

总的来说,我认为这是一个真正的问题,而且一次又一次出现也就不足为奇了。 有趣的是,最终将出现什么解决方案。

向我们发布有关重新构架和副作用的信息!

在我的书中,任何体面的Redux Web应用程序都有3种状态类型。

1)查看组件(React this.state)。 应该避免这种情况,但是有时候我只想实现一些行为,这些行为可以通过仅重用独立于Redux的组件来重用。

2)应用共享状态,即Redux.state。 状态是视图层用于向用户展示应用程序,视图组件使用该视图在彼此之间“通信”。 此状态可用于在ActionCreator,中间件和视图之间进行“通信”,因为它们的决策可能取决于此状态。 因此,“共享”很重要。 不过,它不一定是完整的应用程序状态。 或者我认为以这种状态(就动作而言)来实现所有内容都是不切实际的。

3)由其他副作用复杂模块/库/服务所持有的状态。 我会写这些来处理vladar描述的场景。 另一个例子是react-router。 您可以将其视为具有自己状态的黑盒子,也可以决定将部分React-Router状态提升为应用共享状态(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操作。 当某个动作是某些逻辑的结果时,则不应在reducer中复制该逻辑。 那么reducer的责任就是,只要收到SUBMIT_STARTED,它就不会思考,只是用动作有效负载更新状态,因为其他人已经负责确定SUBMIT_STARTED。

人们应该始终以单一事物为单一决策负责。

然后,Reducer可以进一步基于SUBMIT_STARTED事实,并使用其他逻辑对其进行扩展,但是此逻辑应该有所不同。 铁

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

我可以想象,有时由谁来决定应该为谁负责可能有些棘手。 ActionCreator,中间件,独立的复杂模块,reducer?

我的目标是在使减速器保持纯净的同时尽可能多地做出决定,因为它们可以被热装。 如果需要对副作用/异步做出决定,则转到AC / MW / somewhere_else。 它也可以取决于有关代码重用的最佳实践如何发展,即缩减器与中间件。

AC无法更改state.isSubmitting。 因此,如果其逻辑依赖于AC需求,则可以保证state.isSubmitting将与其逻辑同步。 如果AC逻辑希望对新数据将state.isSubmitted设置为true并返回读取,则它期望这样进行设置。 不应通过其他逻辑潜在地改变此前提。 ACTION是同步原语本身。 基本上,动作定义了协议,并且协议应该被明确定义。

但是我想我知道您要说的是什么。 Redux.state是共享状态。 共享状态总是棘手的。 在减速器中编写代码很容易,它会以交流逻辑无法预料的方式改变状态。 因此,我可以想象在某些非常复杂的情况下,很难使交流电和减速器之间的逻辑保持同步。 这可能会导致难以跟踪和调试的错误。 我相信,在这种情况下,总是有可能将逻辑折叠到AC /中间件上,并使减速器仅基于定义明确的动作而愚蠢地动作,而没有或只有很少的逻辑。

有人总是可以添加新的减速器,这会破坏一些旧的依赖AC。 在让AC依赖Redux.state之前,应该三思。

我有一个动作创建者,该动作创建者需要具有用户凭据的API请求返回的身份验证令牌。 此令牌有时间限制,需要通过某种方式刷新和管理。 您提议的内容似乎表明该状态不属于Redux.state?

应该去哪里? 从概念上讲,它是否作为不变的外部状态传递到Redux存储的构造函数中?

因此,我可以想象在某些非常复杂的情况下,很难使交流电和减速器之间的逻辑保持同步。

是的我并不是说这是一个秀场停止者,但它可能会变得不一致。 状态机的重点是对事件做出反应-不管事件出现的顺序如何,并保持一致。

如果您依赖于来自AC的事件的正确排序,则只需将状态引擎的一部分隐式移动到AC并将其与reducer耦合。

也许这只是一个偏好问题,但是当存储/ reducer始终保持一致并且不依赖外部的东西来保持其他地方的一致性时,我会感觉好很多。

在让AC依赖Redux.state之前,应该三思。

我认为在复杂的情况下(如果不将一些副作用转移到reducers / event-handlers),您将无法避免。

看起来这是一个折衷方案:“删除热重装或重放功能”与“在一个地方管理状态”。 在以后的情况下,您可以编写真实的FSM或状态图,如重新建议框架;在前一种情况下,它就像我们现在在Flux中一样具有临时控制逻辑。

我已经通过Re-frame阅读(太快了),这和Redux是一样的事情,但实现细节有所不同。 重新构架视图组件基于可观察对象,事件是动作,重新构架将事件(动作)直接作为对象(它们在视图组件中构造)作为对象(不使用创建器)进行分配,事件处理程序是纯函数,它们与Redux相同减速器。

处理副作用的讨论不多,或者我错过了,但是我假设它们是在中间件中处理的。 这是主要区别。 中间件包装事件处理程序(约简器),从外部与它们组合,而Redux中间件不包装约简器。 换句话说,Re-frame将副作用直接合成为纯归约化方法,而Redux则使它们分离。

这意味着无法记录重新构架事件并无法轻松重播它们。

当我谈论可能的不一致时,我将共享状态视为根本原因,并且我相信重新框架具有相同的风险。

区别在于将中间件与减速器耦合在一起。 当中间件在重新框架中完成时,它们直接将结果传递给事件处理程序(缩减器),该结果不会记录为事件/动作,因此无法重播。 我会说Redux只是在动作形式之间插入了钩子,因此我相信我可以在技术上实现Re-frame所能达到的相同效果,并具有更多的灵活性,但要花更多的时间来打字(创建动作),并且可能有更多的执行空间错误。

最后,Redux并没有阻止我实施与Re-frame中完全相同的方法。 我可以创建任何以reducer为参数的函数,在其中进行一些处理,然后直接将带有结果的reducer函数调用并将其称为Reducer Middlewares或其他任何东西。 就是说,我想使它更容易些,但要以失去DevTools和更多耦合为代价。

我仍然需要更多地考虑重新框架方法是否具有比简化(减少操作量)更大的优势。 我愿意证明自己的观点是错误的,正如我已经说过的,我已经读过了。

其实我有点不对。 区别不在于Redux与Re-frame实现,而在于“副作用所在”。

如果您在事件处理程序(缩减程序)中执行此操作,并且不允许操作创建者影响您的控制流,那么从技术上讲,这没有什么区别-您也可以在Redux中使用fsm / statecharts并将控制流放在一个位置。

但实际上-有所不同。 @gaearon很好地解释了这一点:Redux期望您的reducer是纯净的,否则重放功能会中断。 因此,您不应该这样做(在减速器中产生副作用),因为它不利于Redux设计。

重新框架还指出事件处理程序是纯净的,但是即使在自述文件中,他们也提到HTTP请求是在事件处理程序中启动的。 因此,对于我来说这还不太清楚,但是看起来他们的期望有所不同:您可以将副作用放在事件处理程序中。

然后有趣的是它们如何重播(这就是我在https://github.com/Day8/re-frame/issues/86上问的内容)。

我假设事件处理程序可能会有副作用的唯一方法是提到的@ tomkis1-记录每个事件处理程序返回的状态(而不是在每个事件上重新运行reducer)。

热重装仍然可以工作,除了它不会影响“过去”事件-仅在时间上产生一些新的状态分支。

因此,设计上的差异可能微妙,但对我而言,这很重要。

我认为它们通过将纯事件处理程序与副作用中间件组成来进行HTTP请求。 此组合返回新的事件处理程序,该事件处理程序不再是纯粹的,但内部事件保持不变。 我不认为他们建议构建混合了app-db(状态)突变和副作用的事件处理程序。 它使代码更具可测试性。

如果记录事件处理程序的结果,则无法使其可热重载,这意味着您可以随时更改其代码。 您将不得不记录中间件的结果,这与Redux所做的完全相同-将结果记录为操作并将其传递给reducer(听它)。

我想我可以说Re-frame中间件是主动的,而Redux允许响应性(任何临时的reducer都可以监听中间件/ ac的结果)。 我开始意识到的一件事是,在Flux和中间件中最常使用的AC与控制器是同一回事。

据我了解, @ tomkis1建议通过保存状态进行时间旅行,可能是在给定的时间间隔内进行,因为我认为如果对每个突变进行操作都会产生性能影响。

好的,意识到reduces /事件处理程序会返回完整的新状态,因此您也是如此。

@merk我可能会在星期一尝试写我的想法,因为我可能不在周末到这里。

重新框架文档建议在事件处理程序中与服务器对话: https :

这就是它与Redux的不同之处。 而且这种差异比乍看之下要深得多。

嗯,我看不到与Redux的区别:

发起事件的处理程序应该组织这些HTTP请求的成功或失败处理程序本身只是调度一个新事件。 他们永远不要尝试自己修改app-db。 这总是在处理程序中完成。

将事件替换为操作。 Redux只是一个纯事件处理程序的特殊名称-一个reducer。 我想我错过了一些东西,或者这个周末我的大脑不舒服。

我可以闻到事件执行的方式有很大的不同-排队和异步。 我认为Redux会执行动作同步和逐步执行动作,以确保状态一致性。 它不确定性是否会进一步影响重播性,因为我不确定是否仅保存事件并一次又一次地重播会导致相同的结束状态。 无视我不能那样做,因为我不知道哪个事件会触发副作用,这与Redux中的动作总是触发纯代码不同。

@vladap总结一下我所看到的区别:

1。

  • 在Redux中,您在动作创建者中发起服务器请求(请参阅@gaearon对此问题的原始问题的回复); 您的减速器必须是纯净的
  • 在Re-frame中,您可以在事件处理程序(reducer)中进行; 您的事件处理程序(减少程序)可能会有副作用

2。

  • 在第一种情况下,您的控制流程将在AC和减速器之间分配。
  • 在第二种情况下,所有控制流都放在一个地方-事件处理程序(缩减程序)。

3。

  • 如果控制流是分开的,则需要在AC和减速器之间进行协调(即使它是隐式的):AC读取减速器产生的状态; 减速器假定来自AC的事件正确排序。 从技术上讲,您会引入交流电和异径管的耦合,因为对异径管的更改也可能会影响交流电,反之亦然。
  • 如果您的控制流在一个地方-您不需要其他协调+您可以在事件处理程序/缩减程序中使用FSM或Statecharts之类的工具


    1. Redux实施纯reducer并将副作用转移到AC的方法可实现热重载和重播

  • 在事件处理程序(还原器)中具有副作用的重新框架方式可以像在Redux中所做的那样(通过重新评估还原器)关闭重播之门,但是其他(可能功能更弱)选项仍然可行-如@ tomkis1所述

至于开发经验上的差异-它仅在您有许多异步内容(计时器,并行http请求,Web套接字等)时出现。 对于同步内容,它们都是相同的(因为AC中没有控制流)。

我想我需要准备一些有关这种复杂情况的真实示例,以使我的观点更加清楚。

我可能会让你发疯,对此我感到抱歉,但是我们在一件基本的事情上没有达成共识。 也许我应该阅读重新构造示例代码,有一天我会学习Clojure。 也许您做了,因此您了解更多。 感谢您的努力。

1。

在Redux中,您在动作创建者中发起服务器请求(请参阅@gaearon对此问题的原始问题的回复); 您的减速器必须是纯净的
在Re-frame中,您可以在事件处理程序(reducer)中进行; 您的事件处理程序(减少程序)可能会有副作用

我以粗体显示的文档明确表示,我应该在事件处理程序中启动webapi调用,然后在事件成功时调度事件。 我不明白如何在同一事件处理程序中处理此调度事件。 此成功事件已分派到路由器(我认为是分派器的等效项),我需要另一个事件处理程序来处理它。 第二个事件处理程序是纯函数,等效于Redux reducer,第一个事件处理程序等效于ActionCreator。 对我来说,这是同一回事,或者我显然错过了一些重要的见识。

至于开发经验上的差异-它仅在您有许多异步内容(计时器,并行http请求,Web套接字等)时出现。 对于同步内容,它们都是相同的(因为AC中没有控制流)。

我们不同意这一点。 异步内容实际上并不重要,您无法在Redux中重播它们,因此您在这里没有开发经验。 当您有许多复杂的纯减速器时,就会出现区别。 在Redux中,您可以采用状态A,一些动作序列,化简器,重播它并获得状态B。您将所有内容保持不变,但是将代码更改为化简器并热重载,从状态A回放相同的动作序列,获得状态C,立即看到您的代码更改的影响。 您不能使用@ tomkis1方法做到这

您甚至可以在其上构建测试方案。 保存一些初始状态,保存一些动作序列,将动作序列减少到状态A,获取状态B并声明它。 然后对您希望导致相同状态B的代码进行更改,重播测试场景以断言它为真,并且您的更改未破坏您的应用程序。 我并不是说这是最好的测试方法,但是可以做到。

实际上,如果我认为Clojurists(就像Haskellers一样是函数式编程的顽强实践者)会建议将副作用与代码混合在一起,而这些代码至少在没有一定程度的编写的情况下是纯净的,我会感到非常惊讶。

惰性评估/反应性是一种基本技术,该技术如何将副作用代码与原本纯净的代码尽可能地移开。 惰性评估是Haskell最终如何应用不切实际的功能纯净概念来获得一个实用的程序的原因,因为完全纯净的程序不能做很多事情。 使用monadic组合和其他组合,整个程序将作为数据流创建,并保持流水线的最后一步是纯净的,并且永远不会在代码中实际调用副作用,程序将返回说明应执行的操作。 它被传递到运行时,它会延迟执行-您不必通过命令调用来触发执行。 至少这是我从鸟瞰的角度理解函数式编程的方式。 我们没有这样的运行时,可以使用ActionCreators和反应性对其进行仿真。 免责声明,不要认为这是理所当然的,这是我从对FP的阅读不多的理解中了解到的,这里的FP是任何授权。 我可能会离开,但实际上我相信我明白这一点。

我不明白如何在同一事件处理程序中处理此调度事件。

我不是那个意思。 “副作用”是指在事件处理程序(缩减程序)中启动HTTP请求。 即使不影响返回函数,执行IO也是副作用。 我不是在成功/错误处理程序中直接修改状态方面的“副作用”。

第二个事件处理程序是纯函数,等效于Redux reducer,第一个事件处理程序等效于ActionCreator。

我认为第一个不等同于Action Creator。 否则,为什么您需要动作创建者来完全做到这一点? 如果可以在reducer中做同样的事情?

您不能使用@ tomkis1方法做到这

同意。 这就是我编写“其他(可能功能不那么强大)的选项”时的意思。

我认为第一个不等同于Action Creator。 否则,为什么您需要动作创建者来完全做到这一点? 如果可以在reducer中做同样的事情?

ActionCreators用于隔离纯应用程序(可以重放)的副作用。 由于Redux是经过设计的,因此您不能(不应)在reducer中产生副作用。 您需要另一种可以放置副作用代码的构造。 在Redux中,这些构造是AC或中间件。 我在重组中看到的唯一区别是中间件做什么,而重组没有在触发处理程序之前转换事件(动作)的中间件。

实际上,可以使用AC模块或服务(例如Angular服务)代替AC /中间件,而可以将其构建为单独的库,然后通过AC或中间件进行接口并将其API转换为操作。 如果您问我AC不是放置此代码的正确位置。AC应该只是简单的创建者/工厂,它可以根据参数构造操作对象。 如果我想成为一个纯粹主义者,最好将副作用代码严格放置在中间件上。 ActionCreator对于控制器责任而言是个坏词。 但是,如果此代码只是单行webapi调用,则有一种趋势是将其简单地放在AC中,对于简单的应用程序来说就可以了。

AC,中间件,第三方库形成了一个我喜欢称之为服务层的层。 原始的Facebook WebApiUtils将是此层的一部分,就像您将使用react-router并听其更改并将其转换为更新Redux.state的操作一样。州)。 由于大多数第三方库当前都已编程,因此它们在重放时不能很好地播放。 显然,我不想像这样从头开始重写所有内容。 我想怎么想呢,这些库,服务,实用程序等扩展了浏览器环境,形成了一个平台,我可以在该平台上构建可重播的应用程序。 这个平台充满了副作用,实际上,这是我有意转移副作用的地方。 我将此平台api转换为动作,并且我的应用通过侦听这些动作对象而间接连接至该平台(尝试模拟我在上一篇文章中描述的Haskell方法)。 这是函数编程中的一种技巧,即如何仍然达到纯度(可能不是从绝对的学术意义上),而是引发副作用。

还有另一件事使我感到困惑,但我可能会误解你。 我认为您说过,对于复杂的逻辑,将所有内容都放在一个位置是有益的。 我认为是完全相反的。

整个动作业务在概念上与DOM事件类似。 我真的不想将我的业务代码与浏览器如何检测到mousemove事件的代码混合在一起,我敢肯定您也不会。 幸运的是,我习惯于在addListener('mousemove',...)之上工作,因为这些职责是完全分开的。 关键是要实现对我的业务逻辑的良好关注分离。 ActionCreators,中间件是用于此的工具。

想象一下,我将针对过时的webapi编写应用程序,而该webapi不能很好地满足我的业务需求。 为了获得所需的数据,我将不得不调用两个端点,使用它们的结果来创建下一个调用,然后将所有结果合并为将在Redux.state中使用的规范形式。 这种逻辑不会泄漏到我的简化器中,因此我不必一次又一次地转换我的简化器中的数据。 它将在中间件中。 实际上,我将隔离凌乱的过时api并开发我的业务应用程序,因为它是针对不错的api编写的。 完成后,也许我会得到一些改变来重建我们的webapi,然后我将重写中间件。

因此,对于一个简单的应用程序来说,将所有内容都放在一个地方似乎很容易。 但是对于复杂的事情却完全相反,我认为良好的关注点分离是有益的。 但也许我不了解您所指的观点或用例。

实际上,可能我会将尽可能多的数据转换以规范的形式转移到reducer并使用reducers组合,因为它将为我提供这段通常纯净的代码的重播性和可测试性。

我不明白如何在同一事件处理程序中处理此调度事件。
我不是那个意思。

我以为您想将webapi调用与reducers逻辑混合在一起以将其放在一个地方。 我的意思是您也不要重新框架,因为他们建议成功时调度事件。

当webapi->大量逻辑-> webapi->大量逻辑-> ...-> reducer的结果时,可能会出现这种情况,整个链由一次单击触发。 在这种情况下,大多数逻辑可能都在交流中,直到所有副作用都消除,我才将其划分。 这是您指的吗?

我在这里能做的最好的事情就是将尽可能多的逻辑转移到纯函数上,以实现可测试性,但是它们将在AC范围内被调用。

它认为我不喜欢将其作为一系列间接连接的事件处理程序来实现。 这将是应允或可观察的链。

更有经验的Reduxers可能对该想法有不同的看法( @gaearon@johanneslumpe@acdlite@emmenko ...)

@vladap我认为这次对话离本期主题太远了。 您已经在这里提到了我的评论的要点:

因此,我可以想象在某些非常复杂的情况下,很难使交流电和减速器之间的逻辑保持同步。

快速示例:

情况1:页面上某处有十大博客作者列表。 现在,您删除了博客中的帖子类别。 成功删除后,您需要从服务器更新此作者列表以使其为最新。

情况2:您在其他页面上。 它没有作者列表,但是有前10条评论列表。 您还可以删除某些类别的博客文章,并且必须在成功删除后更新评论列表。

那么我们如何在状态引擎的层面上处理呢? (我们也可以使用React钩子来寻求帮助,但是好的状态引擎应该能够保持一致)

选项:
A)我们将此逻辑放在AC中。 那么AC会触及CategoryApi (删除),将读取userList状态和commentList状态(以检查其列表呈现在状态了),聊到UserListApi和/或CommentListApi (以刷新列表)+调度TOP_AUTHORS_UPDATED和/或TOP_COMMENTS_UPDATED 。 因此,它将基本上涉及3个不同的域。

B)我们将其放入事件处理程序userListcommentList 。 这些处理程序都将监听DELETE_CATEGORY_SUCCESS事件,然后调用其API服务,然后分派TOP_AUTHORS_UPDATED / TOP_COMMENTS_UPDATED事件。 因此,每个处理程序仅涉及其自己域的服务/状态。

这个示例可能太简单了,但是即使在这个级别上,AC中的API调用也会使事情变得不太漂亮。

区别来自于这样一个事实,即与重新框架中的事件处理程序不同,ActionCreators在处理业务逻辑方面很主动。 并且基于DOM事件只能执行一个AC。 但是,此顶级AC可以呼叫其他AC。 UserListApi和CommentListApi可以有单独的AC,以便更好地分离域,但是始终必须有一个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或类似Statechart的代码(包含reducers / event-handlers的组成)

另外,您无需在动作创建者中返回带有预定义签名的闭包。

不知道这种方法可能会导致什么问题,但就我而言,值得一试。

假设reducers签名将变为:(state,action)=>(state,sideEffects?),其中sideEffects是一个闭包。 然后根据上下文框架可以评估这些副作用,也可以忽略它们(在重播的情况下)。

这实际上类似于我听说Elm用(State, Action) => (State, Request?)做的事情。 我还没有看到任何示例,但是如果有人想探索这一点,请随时。

另一个可能从中获得启发的方法是在Actor模型的基础上实现事件外包-Akka PersistancePersistentFSM 。 我不是说更好,但知道其他尝试也不错。 演员可以使用副作用,但是如果我没有记错的话,有时需要编写明确的代码来重播。

也许重播功能可能会使我们决定代码所属的地方。 似乎我们开始认为是……“我想要这个可重播的游戏,因此它必须去做减速器”。 它可能导致代码边界不清晰,因为我们没有按职责/角色对其进行划分,而且我们倾向于人为地分割业务逻辑以使其可重播。

如果我不考虑重播功能,该如何编写业务代码? 我的目标是将我的业务逻辑放在动作日志的前面,并放在一个地方。 业务逻辑状态机或任何我将使用的状态机将执行所有困难的复杂决策,从而导致已经解决的FACTS(动作)流。 还原器通常是简单的更新器,它们的主要职责是如何将操作有效负载应用于Redux.state。 他们可能有一些逻辑,但从某种意义上说,它们以不同的方式解释事实,从而对事实(动作)流有了另一种看法,以将其呈现给用户。

为了分担责任,我也喜欢这样思考。 可能会要求在服务器上重新实现哪些代码? Fe可以通过移动计算昂贵的业务逻辑(也许出于安全原因,隐藏任何智能属性)来更好地支持慢速设备。 我无法轻松地将逻辑从reducers迁移到服务器,因为它已绑定到视图层。 如果在操作日志的前面编写了复杂的业务逻辑,我可以用REST调用来替换它,翻译成相同的操作流,并且我的视图层应该可以工作。

缺点是,与Akka Persistance不同,我没有办法重播此代码。

我有一天必须对Elm进行更深入的了解,并了解您的建议将如何工作。

@gaearon感谢您提到Elm。 发现了这个对话-https: //gist.github.com/evancz/44c90ac34f46c63a27ae-具有类似的想法(但更为高级)。

他们介绍了任务(http,db等)和效果(只是一系列任务)的概念。 因此,您的“减速器”确实可以返回新的状态和效果。

关于它的很酷的事情(我以前没有这样考虑过)-是如果您可以在链接简化器的同时收集任务-然后将一些功能应用于此列表。 假设您可以批处理http请求,使用事务包装数据库查询等。

或者,您也可以说“重放管理器”,这只是一种效果。

@merk我想大多数人已经写过了。 这种自治的副作用代码可能是最棘手的。 假设timetravel在相同的代码上运行并且间隔也将以重放模式开始,则间隔可能不会与timetravel同步,这可能会严重破坏重放。

通常的应用程序不需要向用户提供令牌和到期倒计时,因此从技术上讲,它不必处于Redux.state中。 管理员应用可能需要安装它。 因此,您可以根据自己的需要以两种方式实现它。

一种选择是在登录AC中启动到期倒计时,我认为它会无限运行并死于应用程序,或者注销必须清理它。 当间隔触发时,它将执行确认令牌所需的操作,如果间隔触发,则将分派LOGIN_EXPIRED操作,侦听缩减器清除用户会话并更改位置,进而触发路由器转换到/ login。

另一个方法是使用第三方库并将问题委托给它,并将其api转换为LOGIN_EXPIRED操作。 或在视图层中不需要时编写自己的令牌并保持令牌和倒数状态。

您可以将其保留在Redux.state中,但是此状态是易失的。 在大状态下,我们可以遇到与使用全局变量编程相同的问题。 而且很难测试,可能无法测试。 测试化简器很容易,但是只有当一个化简器更新状态对象中的特定键时,才能证明它是有效的。 在具有许多不同素质的开发人员的大型项目中,这可能会变得很糟。 任何人都可以决定编写reducer并认为某种状态可以进行更新,而我无法想象如何在所有可能的更新序列中一起测试所有reducer。

根据您的用例,保护此状态可能是有意义的,因为它是登录名-非常重要的功能,并将其分开。 如果在视图层中需要此状态,则以类似的方式将其复制到Redux.state,例如在某些示例中,将react-router位置状态复制到Redux.state。 如果您的状态和团队很小,并且举止得体,则可以将其放在一个地方。

@vladar @vladap

哇,那个榆木要领真酷! 这也让我想起了Cycle.js的“驱动程序”架构。

@merk实际上,自主LOGIN_EXPIRED不会对重播产生太大影响,因为它会进入动作日志的结尾,并且不会立即被重播处理,也许根本不会进入动作日志-我不知道如何精确地实现重播。

@gaeron似乎在Elm和Cycle的高层实现了我试图描述的模式,如果我理解正确的话。 我有一个浏览器,它为我提供了基础平台,并通过服务层(http,db ...)扩展了该平台,从而为我的应用程序构建了平台。 然后,我需要某种胶水,它可以与服务层进行交互,并允许我的纯应用程序与之间接通信,以便我的应用程序可以描述它想要执行的操作,但实际上不能自己执行(发送给Cycle驱动程序,Elm Effects的消息)有任务列表)。 我可以说我的应用程序正在使用数据结构构建一个“程序”(使我想起了代码作为数据的口头禅),然后将其传递到服务层以执行该程序,而我的应用程序仅对该应用程序对该“程序”的结果感兴趣达到其状态。

@merk :进一步。 如果将令牌及其到期日放置在Redux.state /或任何其他js服务中的任何位置,则当用户在新标签页中打开应用时,该令牌不会转移。 我会说用户希望他保持登录状态。 因此,我认为此状态应实际上在SessionStore中。

即使没有这种情况,当令牌到期并不意味着立即清除用户信息并过渡到/ login时,状态可能会分离。 直到用户不与服务器交互(他缓存了足够的数据),他就可以继续工作,并且LOGIN_EXPIRED操作仅在他完成需要服务器的操作后才触发。 基于Redux.state的isLogged状态不必表示存在有效令牌。 Redux.state中的isLogged状态用于决定要呈现的内容,令牌周围的状态可以隐藏到视图层中,并且仅在操作创建者级别上维护,而无需为其编写操作和简化器(影响视图层的对象除外)。

@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个选项,用于使用在reducer中计算的x,y来启动异步操作。 他们都不是完美的。

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触发整个操作层隧道并触发新的动作创建者和异步操作。 如果您问我,这是最糟糕的选择,到处都是业务逻辑。

@vladar
通常,只要将此操作的结果打包为动作,由化简器分派和应用,异步就在哪里启动就无关紧要了。 它将工作相同。 如果应该在动作创建者或商店中启动异步,则与香草流量讨论相同。

这就要求Redux能够识别并忽略重播时的这些异步调用,这正是您的建议。

没有简单的方法将控制权从化简器返回给动作创建者并传递一些值。

我会说这是症状之一。 您的第二个示例很好地说明了我的观点(请参见下文)。

考虑状态转移的一般流程(在FSM世界中):

  1. 活动到来
  2. 给定当前状态,FSM计算新的目标状态
  3. FSM执行操作以从当前状态过渡到新状态

请注意,当前状态很重要! 根据当前状态,同一事件可能导致不同的结果。 并且由于在步骤3中有不同的操作序列(或参数)。

因此,这几乎是同步动作时Flux中状态转换的工作方式。 您的减速机/商店确实是FSM。

但是异步动作呢? 大多数带有异步操作的Flux示例都迫使您认为它是倒置的:

  1. AC执行异步操作(无论当前状态如何)
  2. AC通过此操作调度动作
  3. Reducer / Store处理它并更改状态

因此,将忽略当前状态,并假定目标状态始终相同。 实际上,一致的状态转换流程仍然相同。 它必须看起来像这样(带有AC):

  1. AC在采取行动之前已获取当前状态
  2. 交流行动
  3. AC采取行动后读取新状态
  4. 行动前后的状态-决定要执行的操作(或要作为参数传递的值)

正是您的第二个例子。 我并不是说,所有这些步骤都是一直需要的。 通常,您可以忽略其中的一些。 但是在复杂的情况下-这就是您必须采取的行动。 它是任何状态转换的广义流。 这也意味着您的FSM的边界已移至AC与减速器/存储之间。

但是移动边界会导致其他问题:

  1. 如果异步操作在存储/减速器中-您可以使两个独立的FSM对同一事件做出反应。 因此,要添加新功能-您只需添加可对某些现有事件做出反应的存储/减速器即可。
    但是使用AC进行操作-您必须添加存储/归约器,还必须通过在其中添加逻辑的异步部分来编辑现有的AC。 如果它已经包含了一些其他reducer的异步逻辑...它将变得很酷。
    维护这样的代码显然更加困难。
  2. 在同步操作与异步操作的情况下,您的应用程序的控制流有所不同。 对于同步动作,reducer控制转换,如果发生异步,则AC有效控制此事件/动作可能引起的所有转换。

请注意,大多数问题是大多数Web应用程序的简单状态要求所隐藏的。 但是,如果您有复杂的有状态应用程序-您一定会遇到这些情况。

在大多数情况下,您可能会想出各种解决方法。 但是,如果状态转换逻辑得到了更好的封装,并且您的FSM没有在AC和Reducer之间分开,则可以首先避免使用它们。

不幸的是,状态转换的“副作用”部分仍然是状态转换的一部分,而不是一些单独的独立逻辑。 这就是为什么对我来说(state, action) => (state, sideEffects)看起来更自然。 是的,那不再是“减少”签名%)但是框架的范围不是数据转换,而是状态转换。

如果应该在动作创建者或商店中启动异步,则与香草流量讨论相同。

是的,但是Flux不会禁止您在商店中拥有异步内容,只要您在异步回调与直接改变状态中进行dispatch()即可。 即使大多数实现都建议使用AC进行异步,但它们还是不受质疑的。 我个人认为这是MVC的回忆,因为从心理上将AC视为控制者与将思想转变为FSM相比很方便。

@vladar

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

只是想。 这意味着我们必须将CombineReducers函数的返回类型更改为[state,SideEffects]或[state,[SideEffects]],并更改其代码以合并来自reducers的SideEffects。 原始(状态,操作)=>状态简化程序类型可以保持受支持,因为CombineReducers将强制(状态)返回类型为[state]或[state,[]]。

然后,我们需要在Redux中的某个位置执行和调度SideEffects。 可以在Store.dispatch()中完成,它将应用新状态并执行SideEffects列表并将其分派。 我在想是否可以进行一些不错的无穷递归。

另外,它也可以在中间件级别的某个位置执行,但是我认为Store.dispatch将需要返回SideEffects列表以及操作。

@vladar

是的,但是Flux不会禁止您在商店中拥有异步内容,只要您在异步回调与直接改变状态中进行dispatch()即可。 即使大多数实现都建议使用AC进行异步,但它们还是不受质疑的。 我个人认为这是MVC的回忆,因为从心理上将AC视为控制者与将思想转变为FSM相比很方便。

我同意将ActionCreators视为控制器不是一个好主意。 这就是导致我最终认为对于复杂逻辑我需要将控制从减速器返回到AC的原因。 我不需要那个如果AC处理异步,则它们最多应该类似于请求处理程序-根据其他地方的决策执行,并且该请求不应跨域使用。

为了避免它们充当控制器的角色,现在唯一的选择是通过智能组件路由逻辑,并仅使用ActionCreators在对UI的直接响应中执行逻辑的初始步骤。 实际的第一个决定是在基于Redux.state和/或组件的本地状态的智能组件中做出的。

在您之前的示例中,使用webapi作为响应,这种删除类别并刷新/删除其他内容的方法并不理想。 我认为它将导致基于开发人员偏好的各种技术的混合-是否应在交流电,减速器,智能组件中使用? 让我们看看模式将如何演变。

我买点是要保持异步呼叫远离商店。 但是我认为这是不正确的,因为,恕我直言,它大大增加了其他地方的复杂性。 另一方面,就存储/归约器中的异步功能而言,我认为仅在此执行并调度-或纯方法将这些功能作为数据处理并在遥远的某个地方触发它们,我认为它没有太多的复杂性。

角色执行类似的操作,他们可以执行异步并将消息(动作对象)发送给其他角色,或者将其发送给自己以继续他们的FSM。

我要关闭,因为这里似乎没有什么可操作的。 异步操作文档应回答大多数问题。 没有我们可以运行的实际代码,进一步的理论讨论对我而言似乎并不有价值。 ;-)

@gaearon您可以在redux的示例之一(使用中间件的“真实世界”示例之外)中添加实现正确的API调用处理方式的实际代码吗?

阅读所有文档后,我仍然对实际放置有疑问,并且有几次此问题使我无法理解放置哪些文件的副作用https://github.com/rackt/redux/issues/291#issuecomment -123010379 。 我认为这是此问题线程中的“正确”示例。

预先感谢您对此领域的任何澄清。

我想指出有关不当行为创作者的重要思想:

在操作创建者中放置异步调用,或者一般而言,将不正确的操作创建者置于其中,这对我来说有一个很大的缺点:它以一种完全交换您的应用程序的控制逻辑的方式将控制逻辑弄成碎片,而不像仅仅交换您的商店那样简单创作者。 我打算始终将中间件用于异步流程,并完全避免不纯正的动作创建者。

我正在开发的应用程序将至少具有两个版本:一个在现场Raspberry Pi中运行,另一个在门户中运行。 由客户操作的公共门户/门户甚至可能有单独的版本。 我是否必须使用不同的API尚不确定,但是我想尽我所能为这种可能性做准备,而中间件允许我这样做。

对我而言,在动作创建者之间分配API调用的概念与Redux对状态更新进行集中控制的概念完全相反。 当然,API请求在后台运行这一事实并不属于Redux存储状态的一部分-但它仍然是应用程序状态,您需要对其进行精确控制。 而且我发现,与Redux商店/减速机一样,集中控制而不是分散控制时,可以对其进行最精确的控制。

@ jedwards1211如果您尚未检查它们,以及在#569进行讨论,您可能会对https://github.com/redux-effects/redux-effects感兴趣。

@gaearon很酷,感谢您指出! 无论如何,到目前为止,使用我自己的中间件并不是很困难:)

我创建了一种类似Elm的方法: redux-side-effect自述文件解释了该方法,并将其与替代方法进行了比较。

@gregwebs我喜欢redux-side-effect ,尽管它也会与可交换控制逻辑一起使用,这很麻烦,因为当存在多个不同的商店配置程序时,我有一个问题是如何将sideEffect函数放入我的reducer模块中在不同版本中使用的模块。

不过,我可以使用一个有趣的技巧:将sideEffect state本身存储在

https://github.com/rackt/redux/pull/569/之类的东西对于我的工作方式非常接近,尽管我当然不想在生产项目中使用它,除非它成为标准零件API。

这是一个想法:让中间件在操作上粘贴sideEffect函数。 稍微有点hacky,但是为了使reduce能够启动异步代码,需要进行最小的更改:

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函数从任何地方导入到我的reducer中,这很好,它只是出现在action

@ jedwards1211我在软件包的版本2.1.0actionSideEffectMiddleware

@gregwebs酷! 介意将我列为合作者?

@ jedwards1211您已添加。 也许您会添加适当的重播支持:)在使用此功能的地方,我无法利用它。

@gaearon我认为记录和重放的动作根本不会通过中间件,对吗?

它们确实如此,只是在记录时它们已经是普通对象,因此中间件通常不会进行干预。

@gaearon嗯。 所以我实际上还没有尝试过该功能,但是...我猜有些副作用中间件也会破坏记录/重播吗? 以redux-effects-fetch为例,它仍将对普通对象FETCH操作执行ajax请求:

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

如果记录器从FETCH操作的steps清除[success, failure]回调,则redux-effects中间件将不会分派任何更改状态的操作,但是仍然不希望这样做重放以触发一堆ajax请求。

是否有某种方法可以将“ redux-logger类的“纯”中间件(从不调度其他动作)与“不纯”中间件(可能会调度动作)分开?

不幸的是,我没有仔细研究redux-effects所以我无法回答您的问题。

没问题,在任何情况下,记录器都向中间件可用来决定是否执行副作用的动作添加任何标志吗? 我只是想在自己的中间件中找到一种方法,以能够正确地支持devtools

没有。 可以预期,将devTools()放在____ applyMiddleware的增强子组成链中,这样,在到达该动作时,它是一个“最终”动作,不需要进一步解释。

哦,我知道了,因此,在中间件执行副作用之后,它可以从动作中删除任何字段/对其进行调整,以便在重播时返回中间件时不会触发副作用,然后应该使用开发工具,对吗?

是的,这似乎是个好方法。

太好了,谢谢赐教!

@gaearon @vladar @ jedwards1211您可能对https://github.com/salsita/redux-side-effects感兴趣(state, action) => (state, sideEffects)的实现,但是不是reducer返回元组(您已经注意到这是不可能的),而是产生收益的效果。
我很快就玩了,热重载和重播似乎还可以。 到目前为止,我看不到任何redux功能都已损坏,但是也许某些高级reduxer可能会发现一些东西。 :)
对我来说,这对redux堆栈来说确实很重要,因为它允许逻辑主要用于化简器,使它们成为有效且可测试的状态机,将效果(转换动作的结果不仅仅取决于当前状态)委派给外部服务(非常类似于Elm的架构,效果和服务)。

另一种方法是redux-saga ,它也使用生成器,但使副作用与减速器分开。

@gaearon我相信@minedeljkovic的意思是

对我来说,这看起来是Redux堆栈的真正重要补充,因为它允许逻辑主要用于化简器,使它们成为有效且可测试的状态机,然后将效果(转换动作的结果不仅仅取决于当前状态)委托给外部服务(非常类似于榆木建筑,效果和服务)。

作为相对于传统方法的主要优势,因为https://github.com/yelouafi/redux-saga/redux-thunk工作方式非常相似,而不是在其之上添加一些语法糖,这使得长时间运行的交易更加容易。

另一方面, https://github.com/salsita/redux-side-effects更像Elm架构。

是的,redux-saga和redux-side-effects仅使用生成器来声明副作用,分别保持sagas和reducers的纯净。 那是相似的。

我更喜欢减速器的两个原因:

  1. 您可以显式访问当前状态,这可能会影响应如何声明效果(我认为这是本次讨论中@vladar的要点之一)
  2. 没有引入新概念(redux-saga中的Saga)

我在https://github.com/rackt/redux/issues/1139#issuecomment -165419770中的评论仍然有效,因为redux-saga不会解决此问题,除了接受模型之外,没有其他方法可以解决此问题用于榆木建筑。

我提出了一个简单的要点,试图强调我关于redux副作用的观点: https :

我试图定义最简单的可能场景,我认为这仅仅是“现实世界”,但仍然强调了本次讨论的一些要点。

场景是:
应用程序具有“用户注册”部分,应在其中输入个人用户数据。 在其他个人数据中,出生国家是从国家列表中选择的。 在“国家/地区选择”对话框中执行选择,用户可以在其中选择国家/地区并选择确认或取消选择。 如果用户试图确认选择,但未选择任何国家,则应警告她。

架构约束:

  1. CountrySelection功能应该是模块化的(按照redux ducks的精神),因此可以在应用程序的多个部分中重用(例如,在应用程序的“产品管理”部分中应输入生产国)
  2. CountrySelection的状态切片不应视为全局切片(位于redux状态的根目录中),而应在调用该模块的模块本地(并由该模块控制)。
  3. 此功能的核心逻辑需要包含尽可能少的活动部件(在我的主旨中,该核心逻辑仅在reducer中实现(并且仅是逻辑中最重要的部分)。组件应微不足道,因为所有由redux驱动的组件都应如此是:)。 他们的唯一职责是呈现当前状态并调度操作。)

就此对话而言,要点最重要的部分是countrySelection reducer处理CONFIRM_SELECTION动作的方式。

@gaearon ,我的发言大部分是您的意见,即作为标准redux的补充,redux副作用为考虑约束的最简单解决方案提供了表面。

实现此方案的可能替代想法(不使用redux-side-effects,但使用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 :

传奇模式还可以使某些隐式事物变得明确。
例如,您仅描述触发动作发生的情况(我喜欢事件,因为对于我来说,它们应该是过去时),然后加入一些业务规则并更新UI。 在您的情况下,错误的显示是隐式的。 带有传奇的内容,一经确认,如果未选择任何国家,则您可能会分派动作“ NO_COUNTRY_SELECTED_ERROR_DISPLAYED”或类似的动作。 我希望这是完全明确的。

您也可以将Saga视为鸭子之间的耦合点。
例如,您有duck1和duck2,每个对象都有本地操作。 如果您不喜欢将两只鸭子组合在一起的想法(我是说一只鸭子会使用第二只鸭子的actionsCreators),则可以让它们描述发生了什么,然后创建一个Saga,以将一些复杂的业务规则联系起来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方式,因为现在reducer包含“副作用”(我不知道那是什么意思)。
长话短说,正确的方法是将这些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时,令人困惑的碎片来自不同的需求或关于整合/分离来自reducer的副作用的观点或观点。

而且,碎片化来自以下事实:无论您要使用Redux,副作用确实并不难。 滚动我自己的基本副作用中间件只用了大约10行代码。 因此,拥抱您的自由,不要担心“正确”的方式。

@ jedwards1211 “ Redux本身”没有任何价值或意义,也没有任何意义,因为其主要目的是解决Flux开发人员的日常问题。 这意味着AJAX,精美的动画以及所有其他的“普通的”和“预期的”东西

@ halt-hammerzeit,您在这里有个要点。 Redux当然似乎是想让大多数人就如何管理状态更新达成一致,所以遗憾的是,它没有让大多数人就如何执行副作用达成共识。

您是否在存储库中看到过“ examples”文件夹?

尽管有多种执行副作用的方法,但通常建议还是在Redux外部或在动作创建者内部进行,就像我们在每个示例中一样。

是的,我知道...我只是想说的是,该主题说明(至少在这里的人们之间)关于最佳的副作用产生方法缺乏共识。

尽管也许大多数用户确实使用了重击动作创建器,但我们只是离群值。

^他说了什么。

我希望所有最伟大的头脑都同意一个正义的解决方案
并用石头雕刻它,这样我就不必阅读9000屏幕
线

2016年1月7日星期四,安迪·爱德华兹(Andy Edwards) [email protected]写道:

是的,我知道...我只想说的是,该主题说明缺乏
(至少在这里的人们之间)关于最佳表演方式的共识
效果。

-
直接回复此电子邮件或在GitHub上查看
https://github.com/rackt/redux/issues/291#issuecomment -169751432。

所以,我想,目前商定的解决方法是在实际中进行rverything
创作者。 我将尝试使用这种方法,以防万一我发现任何缺陷
我会寄回这里

2016年1月7日,星期四,НиколайКучумовkuchumovn@ gmail.com写道:

^他说了什么。

我希望所有最伟大的头脑都同意一个正义的解决方案
并用石头雕刻它,这样我就不必阅读9000屏幕
线

2016年1月7日星期四,安迪·爱德华兹(Andy Edwards)< [email protected]
<_e i =“ 16”>

是的,我知道...我只想说的是,该主题说明缺乏
(至少在这里的人们之间)关于最佳表演方式的共识
效果。

-
直接回复此电子邮件或在GitHub上查看
https://github.com/rackt/redux/issues/291#issuecomment -169751432。

存在此线程和类似线程是因为1%的Redux用户喜欢寻找更强大/纯净/声明性的副作用解决方案。 请不要以为没有人同意这一印象。 沉默的大多数人使用思想和诺言,并且大多数人对此方法感到满意。 但是像任何技术一样,它也有缺点和折衷。 如果您具有复杂的异步逻辑,则可能要删除Redux并探索Rx。 Redux模式很容易在Rx中表达。

好,谢谢

2016年1月7日,星期四,Dan Abramov [email protected]写道:

存在此线程和类似线程是因为1%的Redux用户喜欢查找
更强大/纯净/声明性的副作用解决方案。 请不要
觉得没有人能对此达成共识。 沉默的多数使用
思考和承诺,并且大多数人对此方法感到满意。 但像任何
科技,它有缺点和折衷。 如果您有复杂的异步逻辑
您可能要删除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-

@gaearon顺便说一下,您对stackoverflow的回答与文档有相同的缺陷-它仅涵盖了最简单的情况
http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594#34599594
现实世界中的应用程序不仅仅可以通过AJAX来获取数据:它们还应该处理错误并根据操作调用的输出执行某些操作。
您的建议是仅dispatch其他事件即可。
那么,如果我的代码想让用户在操作完成时alert()该怎么办?
那是那些thunk不会帮助的地方,但是诺言会的。
我目前使用简单的Promise编写我的代码,并且可以这样工作。

我已阅读了有关redux-saga的相邻评论。
不知道它在做什么,哈哈。
我不太喜欢monads东西,而且我仍然不知道thunk是什么,而且我不喜欢这个奇怪的词开头。

好的,所以我继续在React组件中使用Promises。

我们建议您在整个文档中都从笨拙的人那里返回承诺。

@gaearon是的,这就是我在说的: on_click() { dispatch(load_stuff()).then(show_modal('done')) }
这样就可以了。

我认为您仍然缺少一些东西。
请参阅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'))
}

自述部分写得很好,谢谢

不,这不是我的意思。 看一下makeSandwichesForEverybody 。 这是一个重击动作创建者,正在呼叫其他重击动作创建者。 这就是为什么您不需要将所有内容都放在组件中的原因。 有关此内容的更多信息,请参见此存储库中的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返回promise,以便组件可以使用局部组件状态显示一些微调框或其他内容(无论如何,在使用jquery时都很难避免)

否则,您可以管理复杂的计时器以使用redux-saga驱动动画。

看看这篇博客文章: http :

哦,在这种情况下,可以将其保留在组件中。
(注意:通常,在React中,最好对所有内容使用声明性模型,而不是使用jQuery命令式显示模式。但是没什么大不了的。)

@slorber是的,我已经从我的“ thunk”(或任何你称呼他们的东西; errr,我不喜欢这个奇怪的词)中返回Promises和东西,所以我可以处理Spinner等。

@gaearon好,我知道了。 在理想的机械世界中,当然可以保留在声明性编程模型中,但是从机器的角度看,现实有其自身的要求,例如非理性。 人们是非理性的生物,不仅仅是用纯0和1进行运算,而且它需要牺牲代码的美感和纯净度,以便能够执行一些非理性的工作。

我对该线程的支持感到满意。 我的疑虑现在似乎已经解决。

相关新讨论: https :

@gaearon非常棒的解释! :trophy:谢谢:+1:

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

timdorr picture timdorr  ·  3评论

elado picture elado  ·  3评论

mickeyreiss-visor picture mickeyreiss-visor  ·  3评论

vraa picture vraa  ·  3评论

parallelthought picture parallelthought  ·  3评论