Redux: 如何创建一个通用列表作为减速器和组件增强器?

创建于 2015-09-30  ·  50评论  ·  资料来源: reduxjs/redux

你好。 将计数器示例扩展到独立计数器的动态列表的好方法是什么?

动态是指在计数器列表末尾的 UI 中,将有+-按钮,用于向列表中添加新计数器或删除最后一个计数器。

理想情况下,计数器减速器和组件将保持原样。 如何创建一个通用列表存储+组件来收集任何类型的实体? 是否可以进一步概括 list store+component 以从todomvc-example中获取 counters 和 todo-items ?

在示例中包含这样的内容会很棒。

examples

最有用的评论

如何创建一个通用列表存储+组件来收集任何类型的实体? 是否可以进一步概括列表存储+组件以从 todomvc 示例中获取计数器和待办事项项?

是的,绝对有可能。
您想要创建一个更高阶的 reducer 和一个更高阶的组件。

对于高阶减速器,请参见此处的方法:

function list(reducer, actionTypes) {
  return function (state = [], action) {
    switch (action.type) {
    case actionTypes.add:
      return [...state, reducer(undefined, action)];
    case actionTypes.remove:
      return [...state.slice(0, action.index), ...state.slice(action.index + 1)];
    default:
      const { index, ...rest } = action;
      if (typeof index !== 'undefined') {
        return state.map(item => reducer(item, rest));
      }
      return state;
    }
  }
}

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return counter + 1;
  case 'DECREMENT':
    return counter - 1;
  }
}

const listOfCounters = list(counter, {
  add: 'ADD_COUNTER',
  remove: 'REMOVE_COUNTER'
});

const store = createStore(listOfCounters);
store.dispatch({
  type: 'ADD_COUNTER'
});
store.dispatch({
  type: 'ADD_COUNTER'
});
store.dispatch({
  type: 'INCREMENT',
  index: 0
});
store.dispatch({
  type: 'INCREMENT',
  index: 1
});
store.dispatch({
  type: 'REMOVE_COUNTER',
  index: 0
});

(我还没有运行它,但它应该可以通过最小的更改来工作。)

所有50条评论

我同意这是一个很好的例子。
Redux 在这里与Elm Architecture类似,因此请随意从那里的示例中获得灵感。

如何创建一个通用列表存储+组件来收集任何类型的实体? 是否可以进一步概括列表存储+组件以从 todomvc 示例中获取计数器和待办事项项?

是的,绝对有可能。
您想要创建一个更高阶的 reducer 和一个更高阶的组件。

对于高阶减速器,请参见此处的方法:

function list(reducer, actionTypes) {
  return function (state = [], action) {
    switch (action.type) {
    case actionTypes.add:
      return [...state, reducer(undefined, action)];
    case actionTypes.remove:
      return [...state.slice(0, action.index), ...state.slice(action.index + 1)];
    default:
      const { index, ...rest } = action;
      if (typeof index !== 'undefined') {
        return state.map(item => reducer(item, rest));
      }
      return state;
    }
  }
}

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return counter + 1;
  case 'DECREMENT':
    return counter - 1;
  }
}

const listOfCounters = list(counter, {
  add: 'ADD_COUNTER',
  remove: 'REMOVE_COUNTER'
});

const store = createStore(listOfCounters);
store.dispatch({
  type: 'ADD_COUNTER'
});
store.dispatch({
  type: 'ADD_COUNTER'
});
store.dispatch({
  type: 'INCREMENT',
  index: 0
});
store.dispatch({
  type: 'INCREMENT',
  index: 1
});
store.dispatch({
  type: 'REMOVE_COUNTER',
  index: 0
});

(我还没有运行它,但它应该可以通过最小的更改来工作。)

谢谢-我会尝试使这种方法起作用。

我仍然想知道是否可以通过combineReducers重用列表功能来同时拥有计数器列表和待办事项列表。 如果这有意义的话。 但我一定会试一试。

我仍然想知道是否可以通过 combineReducers 重用列表功能以同时拥有计数器列表和待办事项列表。 如果这有意义的话。 但我一定会试一试。

是的,完全:

const reducer = combineReducers({
  counterList: list(counter, {
    add: 'ADD_COUNTER',
    remove: 'REMOVE_COUNTER'
  }),
  todoList: list(counter, {
    add: 'ADD_TODO',
    remove: 'REMOVE_TODO'
  }),
});

@gaearon列表操作应该如何获得它的index

我们设法按照您的指示创建了一个更高阶的减速器,但我们正在努力处理更高阶的组件。 目前我们的组件还不够通用,无法与 Counter 以外的其他组件一起使用。 我们的问题是如何以通用的方式将索引添加到操作中。

您可以在这里查看我们的解决方案: https ://github.com/Zeikko/redux/commit/6a222885c8c93950dbdd0d4cf3532cd99a32206c

我在提交中添加了一些注释以突出显示有问题的部分。

如果有一个通用的列表缩减器 + 可以列出计数器列表的组件,那就太好了。

目前我们的组件还不够通用,无法与 Counter 以外的其他组件一起使用。 我们的问题是如何以通用的方式将索引添加到操作中。

您能解释一下“以通用方式添加索引”是什么意思吗? 你的意思是你想在动作中为index键使用不同的名称吗?

我想我现在明白你的意思了。

抱歉,我现在无法发表太多评论。 我明天回来。

我现在明白了这个问题。 调查它。

我很快就遇到了 Redux 如何偏离 Elm 架构所固有的一些限制。
可惜之前没看懂!

  • 当组件被包装时,它需要接受dispatch属性而不是回调。 这意味着您需要避免让动作创建者成为您的道具,而只在组合组件中使用dispatch() ,或者我们需要引入一个bindActionCreators(Component, actionCreators) => Component ,就像connect()但实际上并没有连接到商店,而只是将 action-creator-as-props-wanting-component 替换this.props.dispatch -wanting-component。
  • 您不能从包装的组件中分派需要中间件的操作。 这是一个无赖! 如果我用一个列表包裹计数器,我就不能再发送incrementAsync()因为我刚刚发送的 $# function (dispatch, getState) { ... } { action: function (dispatch, getState) { ... } } — 并且 bam! thunk 中间件不再识别它。

对此可能有非尴尬的解决方案,但我还没有看到它们。
现在,请将此提交视为示例(具有上述限制)。

这是代码:

组件/Counter.js

import React, { Component, PropTypes } from 'react';
import { increment, incrementIfOdd, incrementAsync, decrement } from '../actions/counter';

class Counter extends Component {
  render() {
    const { dispatch, counter } = this.props;
    return (
      <p>
        Clicked: {counter} times
        {' '}
        <button onClick={() => dispatch(increment())}>+</button>
        {' '}
        <button onClick={() => dispatch(decrement())}>-</button>
        {' '}
        <button onClick={() => dispatch(incrementIfOdd())}>Increment if odd</button>
        {' '}
        <button onClick={() => dispatch(incrementAsync())}>Increment async</button>
      </p>
    );
  }
}

Counter.propTypes = {
  dispatch: PropTypes.func.isRequired,
  counter: PropTypes.number.isRequired
};

export default Counter;

组件/list.js

import React, { Component, PropTypes } from 'react';
import { addToList, removeFromList, performInList } from '../actions/list';

export default function list(mapItemStateToProps) {
  return function (Item) {
    return class List extends Component {
      static propTypes = {
        dispatch: PropTypes.func.isRequired,
        items: PropTypes.array.isRequired
      };

      render() {
        const { dispatch, items } = this.props;
        return (
          <div>
            <button onClick={() =>
              dispatch(addToList())
            }>Add counter</button>

            <br />
            {items.length > 0 &&
              <button onClick={() =>
                dispatch(removeFromList(items.length - 1))
              }>Remove counter</button>
            }
            <br />
            {this.props.items.map((item, index) =>
              <Item {...mapItemStateToProps(item)}
                    key={index}
                    dispatch={action =>
                      dispatch(performInList(index, action))
                    } />
            )}
          </div>
        )
      }
    }
  };
}

动作/counter.js

export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';

export function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

export function decrement() {
  return {
    type: DECREMENT_COUNTER
  };
}

export function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState();

    if (counter % 2 === 0) {
      return;
    }

    dispatch(increment());
  };
}

export function incrementAsync(delay = 1000) {
  return dispatch => {
    setTimeout(() => {
      dispatch(increment());
    }, delay);
  };
}

动作/list.js

export const ADD_TO_LIST = 'ADD_TO_LIST';
export const REMOVE_FROM_LIST = 'REMOVE_FROM_LIST';
export const PERFORM_IN_LIST = 'PERFORM_IN_LIST';

export function addToList() {
  return {
    type: ADD_TO_LIST
  };
}

export function removeFromList(index) {
  return {
    type: REMOVE_FROM_LIST,
    index
  };
}

export function performInList(index, action) {
  return {
    type: PERFORM_IN_LIST,
    index,
    action
  };
}

减速器/counter.js

import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter';

export default function counter(state = 0, action) {
  switch (action.type) {
  case INCREMENT_COUNTER:
    return state + 1;
  case DECREMENT_COUNTER:
    return state - 1;
  default:
    return state;
  }
}

减速器/list.js

import { ADD_TO_LIST, REMOVE_FROM_LIST, PERFORM_IN_LIST } from '../actions/list';

export default function list(reducer) {
  return function (state = [], action) {
    const {
      index,
      action: innerAction
    } = action;

    switch (action.type) {
    case ADD_TO_LIST:
      return [
        ...state,
        reducer(undefined, action)
      ];
    case REMOVE_FROM_LIST:
      return [
        ...state.slice(0, index),
        ...state.slice(index + 1)
      ];
    case PERFORM_IN_LIST:
      return [
        ...state.slice(0, index),
        reducer(state[index], innerAction),
        ...state.slice(index + 1)
      ];
    default:
      return state;
    }
  }
}

减速器/index.js

import { combineReducers } from 'redux';
import counter from './counter';
import list from './list'

const counterList = list(counter);

const rootReducer = combineReducers({
  counterList
});

export default rootReducer;

容器/App.js

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import Counter from '../components/Counter';
import list from '../components/list';

const CounterList = list(function mapItemStateToProps(itemState) {
  return {
    counter: itemState
  };
})(Counter);

export default connect(function mapStateToProps(state) {
  return {
    items: state.counterList
  };
})(CounterList);

cc @acdlite — 这是当前中间件 + React Redux 设计有些崩溃的示例。
我们可以将其声明为 wontfix,但也许您想看看是否有任何方法可以避免这种情况。

我一直在尝试为 React 使用服务 (IoC) 容器,并在昨天创建了这个测试仓库: https ://github.com/magnusjt/react-ioc

我认为它可能会解决部分问题,因为您可以在 CounterList 不知道的情况下将动作创建者传递给 Counter。 这是可能的,因为动作创建者进入 Counter 的构造函数,而不是 props。

对于您创建的每个新 Counter 组件,您可以传递不同的操作创建者(可能通过将索引值绑定到操作创建者)。 当然,您仍然有将数据传送到柜台的问题。 我还不确定这是否可以通过服务容器解决。

@gaearon ,你的例子对我来说很合适。 您必须传递动作创建者并一路调度。 通过这种方式,您可以使用高阶函数来祭坛操作。

不过,我不太确定您的第二点是否必要。 由于新的消息格式,您会错过中间件,但performInList的一个更大问题是您将抽象限制在一个列表中。 @pe3提到了一个计数器列表。 对于这样的任意抽象,我认为您需要以某种方式嵌套操作。

在这张票中,我想出了一种嵌套类型的方法:
https://github.com/rackt/redux/issues/897

但我认为更根本的是,你会想要完全嵌套动作......

好吧,我只是试了一下。

我简化了很多事情。 这是在做这些花哨的东西之前的计数器应用程序:

https://github.com/ccorcos/redux-lifted-reducers/blob/80295a09c4d04654e6b36ecc8bc1bfac4ae821c7/index.js#L49

这是之后:

https://github.com/ccorcos/redux-lifted-reducers/blob/1fdafe8ed29303822018cde4973fda3305b43bb6/index.js#L57

我不确定“lift”是否合适——我知道它在函数式编程中意味着什么,但对我来说感觉还可以。

基本上,通过提升一个动作,您将一个动作嵌套在另一个动作中。

const liftActionCreator = liftingAction => actionCreator => action => Object.assign({}, liftingAction, { nextAction: actionCreator(action) })

并且通过提升减速器来消除嵌套动作。 提升减速器基本上将子减速器(通过适当的动作部分应用)应用于某些子状态。

const liftReducer = liftingReducer => reducer => (state, action) => liftingReducer(state, action)((subState) => reducer(subState, action.nextAction))

因此,对于组件化简器列表,我有一个操作来指定子操作适用于哪个索引处的哪个组件。

// list actions
const LIST_INDEX = 'LIST_INDEX'
function actOnIndex(i) {
  return {
    type: LIST_INDEX,
    index: i
  }
}

而且我有一个“高阶”(另一个感觉不错的花哨术语,哈哈;)减速器,它将子减速器应用于适当的子状态。

const list = (state=[], action) => (reduce) => {
  switch (action.type) {
    case LIST_INDEX:
      let nextState = state.slice(0)
      nextState[action.index] = reduce(nextState[action.index])
      return nextState
    default:
      return state;
  }
}

剩下的就是将计数减少器“提升”到列表减少器中。

const reducer = combineReducers({
  counts: liftReducer(list)(count)
});

现在对于计数器列表,我们只需要在将动作传递给计数器时解除它们。

class App extends Component {
  render() {
    const counters = [0,1,2,3,4].map((i) => {
      return (
        <Counter count={this.props.state.counts[i]}
                 increment={liftActionCreator(actOnIndex(i))(increment)}
                 decrement={liftActionCreator(actOnIndex(i))(decrement)}
                 dispatch={this.props.dispatch}
                 key={i}/>
      )
    })
    return (
      <div>
        {counters}
      </div>
    );
  }
}

我认为这可以用适当的术语更正式。 高阶减速器我觉得这里也可以用镜头,不过我没用过,哈哈。

我收回我在上一条评论中所说的话—— @gaearon是对的。 通过像这样嵌套动作,您将错过中间件,并且您必须一直向下传递调度,以便您可以操纵动作创建者。 也许为了支持这一点,Redux 必须通过中间件应用所有子操作。 此外,另一个问题是初始化列表中的状态......

你所描述的被称为榆树架构。 请看这里: https ://github.com/gaearon/react-elmish-example

哥们,你永远领先一步! 向我展示一些很酷的东西的链接:+1:

您不能从包装的组件中分派需要中间件的操作。 这是一个无赖! 如果我用列表包装计数器,我不能再调度 incrementAsync() 因为我刚刚调度的 function (dispatch, getState) { ... } 变成了 { action: function (dispatch, getState) { ... } }清单——然后砰! thunk 中间件不再识别它。

@gaearon这个解决方案怎么样? 而不是通用减速器自称是这样的子减速器

case PERFORM_IN_LIST:
      return [
        ...state.slice(0, index),
        reducer(state[index], innerAction),
        ...state.slice(index + 1)
      ];

为商店提供一些特殊的方法dispatchTo(reducer, state, action, callback) ,它的作用类似于dispatch ,只是它直接调度到子减速器(通过所有配置的中间件)并在每个子状态修改时通知回调

export default function list(reducer, dispatchTo) {
  return function (state = [], action) {
    ...
    case PERFORM_IN_LIST:
      dispatchTo(reducer, state[index], innerAction, newState =>
         [
           ...state.slice(0, index),
           newState,
           ...state.slice(index + 1)
        ]);
       default:
          return state;
    }
  }
}

我不知道这在 Redux 中是否可行。 一个想法是使用一些内部store.derive(reducer, state)方法来实现dispatchTo ,该方法将为配置有一些中间件作为根存储的状态树部分返回一个存储。 例如

function dispatchTo(reducer, state, action, callback) {
  const childStore = store.derive(reducer, state)
  childStore.subscribe(() => setRootState( callback(getState() ))
  childStore.dispatch(action)
}

这只是一个想法,因为我说我不了解 Redux 的内部结构,所以也许我错过了一些东西

编辑
_可能这会很尴尬,因为 reducer 方法应该是同步的。 使方法异步意味着所有链接也必须是异步的。
也许最好的解决方案是直接暴露store.derive(reducer)方法并使用某种商店组合构建通用减速器_

这太复杂了,IMO不值得。 如果您想这样做,只需不要使用中间件(或使用applyMiddleware的一些替代实现)就可以了。

另外,我要结束了,因为我们不打算对此采取行动。

@gaearon为了讨论起见:
您在此提交中附加的代码示例: https ://github.com/rackt/redux/commit/a83002aed8e36f901ebb5f139dd14ce9c2e4cab4

如果我有 2 个计数器列表(甚至是单独的“模型”),则调度addToList会将项目添加到两个列表中,因为操作类型是相同的。

// reducers/index.js

import { combineReducers } from 'redux';
import counter from './counter';
import list from './list'

const counterList = list(counter);
const counterList2 = list(counter);

const rootReducer = combineReducers({
  counterList,
  counterList2
});

export default rootReducer;

那么reducers/list在这里有什么帮助呢? 您不需要为操作类型或其他东西添加前缀吗?

如果我有 2 个计数器列表(甚至是单独的“模型”),调度 addToList 会将项目添加到两个列表中,因为操作类型是相同的。

请仔细查看 a83002aed8e36f901ebb5f139dd14ce9c2e4cab4。 它嵌套了动作。 从容器组件向下传递的dispatch将操作包装到performInList操作中。 然后在reducer中检索内部动作。 这就是 Elm Architecture 的工作原理。

@gaearon也许我遗漏了一些东西,但是除了上面提到的额外的counterList2减速器,这个 UI 仍然会更新每个操作的两个列表(根据它的构建方式,这是预期的,但是解决方案是什么?):

// reducers/index.js

import { combineReducers } from 'redux';
import counter from './counter';
import list from './list'

const counterList = list(counter);
const counterList2 = list(counter);

const rootReducer = combineReducers({
  counterList,
  counterList2
});

export default rootReducer;


// containers/App.js

import React from 'react';

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import counter from '../components/Counter';
import list from '../components/list';

let CounterList = list(function mapItemStateToProps(itemState) {
  return {
    counter: itemState
  };
})(counter);

CounterList = connect(function mapStateToProps(state) {
  return {
    items: state.counterList
  };
})(CounterList);

let CounterList2 = list(function mapItemStateToProps(itemState) {
  return {
    counter: itemState
  };
})(counter);

CounterList2 = connect(function mapStateToProps(state) {
  return {
    items: state.counterList2
  };
})(CounterList2);


export default class App extends React.Component {
  render() {
    return (
      <div>
        <CounterList />
        <CounterList2 />
      </div>
    )
  }
}

@elado您需要再次将其包装在一个列表中,以便两个列表的操作不会发生冲突,就像我们对计数器列表所做的那样

@ccorcos

再次将其包装在列表中

到底包什么?

@ccorcos
我在这里上传了示例:http: //elado.s3-website-us-west-1.amazonaws.com/redux-counter/
它有源地图

仍然不完全确定你的意思。 正如我所说 - 当前行为是预期的,因为动作名称是相同的,并且动作创建者中没有指示在哪个列表中执行它,因此它在所有减速器上运行最终影响两个列表的动作。

所以对实际的Redux功能不是很了解,但是对elm很熟悉。

在这个新示例中,我们没有在顶层绑定动作创建者。 相反,我们将调度函数向下传递给较低的组件,而这些较低的组件可以将一个动作传递给调度函数。

为了让抽象工作得很好,这样我们就不会发生动作冲突,当“listOf”组件将调度传递给它的孩子时,它实际上传递了一个函数,该函数以列表组件可以理解的格式包装动作。

children.map((child, i) => {
  childDispatch = (action) => dispatch({action, index: i})
  // ...

所以现在你可以编写listOf(counter)listOf(listOf(counter))并且如果你想创建一个名为pairOf的组件,那么你需要确保在传递它们时包装这些动作。 现在, App组件只是将它们并排呈现,而不包装调度函数,因此您会遇到动作冲突。

@ccorcos

谢谢。 所以总而言之,显然没有“魔法”发生,动作需要他们需要的所有信息来告诉减速器在哪个实例上执行动作。 如果它是listOf(listOf(counter)) ,则该操作将需要 2d 数组的两个索引。 listOf(listOf(counter))可以工作,但将所有计数器放在一个由唯一 ID 索引的单个平面列表中,这是在操作中传递的唯一内容似乎更灵活。

看起来构建灵活而复杂的 Redux 应用程序的唯一方法是让系统中的所有实体都按商店中的 ID 进行索引。 有时在示例中给出的任何其他更简单的方法将很快达到其极限。 该索引几乎是关系数据库的镜像。

该动作将需要二维数组的两个索引

听起来你的想法和行动看起来像:

{type: 'increment', index:[0 5]}

但它应该看起来像:

{type:'child', index: 0, action: {type: 'child', index: 5, action: {type: 'increment'}}}

这样你就可以永远做一个listOf(listOf(listOf(...listOf(counter)...)))

这一切都来自榆树,顺便说一句。 查看Elm 架构教程

我对聚会有点慢,但我看不出这个“不适用于中间件”的地方? 如果您以@ccorcos的形式提供无限嵌套,您不能只连接中间件来处理嵌套吗? 或者我们只是在谈论redux-thunk嵌套动作意味着怪异?

中间件如何知道是按原样解释动作,还是寻找嵌套动作?

我知道了。

@gaearon

你好。

我不肯定我理解这个问题的解决方案,并且非常感谢一个简单的答案。

如果您在同一页面上有多个组件副本,是否_不使用中间件(基本上是大多数 Redux 生态系统)? 我也没有看到是否有人回应了multireducer 的建议。

任何澄清都会有所帮助。

不,这不是决议。 该线程与页面上组件的多个身份无关。 要实现这一点,您只需在操作中传递一个 ID。 该线程是关于_编写一个通用函数来做到这一点_。 不幸的是,这确实与中间件的概念发生了冲突。

谢谢你。

大家好,
也许我解决了这个问题,但是我更喜欢使用deku而不是react ^^

如果有人告诉我他对这个实验的见解,尤其是关于taskMiddleware的想法,我会很高兴。 @gaearon你有时间看看吗? :舌头:

计数器列表

谢谢!

只是想澄清一下,这些解决方案都不是为了处理初始状态。
这可以以某种方式实现@gaearon吗? 允许上面的 List reducer 有一个初始的计数器列表?

为什么会有问题? 我想你可以用undefined状态调用子减速器并使用这些值。

当使用第一次调度初始化商店时,我需要(对于我的内部逻辑)具有该列表的初始状态,并且它应该是预定义的计数器列表(列表项的类型)

像这样的东西?

export default function list(reducer) {
  return function (state = [

    // e.g. 2 counters with default values
    reducer(undefined, {}),
    reducer(undefined, {}),

      ], action) {
    const {
      index,
      action: innerAction
    } = action;
   // ...
  }
}

如果您愿意,您也可以将此作为list的参数

这看起来真的很复杂,所以我可以在一个页面上拥有多个相同的组件。

我仍然在思考 Redux,但创建多个商店似乎要简单得多,即使它不是推荐的 Redux 使用模式。

@deevus :不要让这些讨论吓跑你。 Redux 社区中有很多人非常倾向于函数式编程,虽然在这个线程中讨论的一些概念和其他类似的概念很有价值,但它们也往往是一种追求“完美”的尝试,而不仅仅是“好”。

通常,您可以在一个页面上完全拥有一个组件的多个实例。 本次讨论的目标是嵌套组件的任意组合,这很有趣,但也不是大多数应用程序需要做的事情。

如果您有除此之外的具体问题,Stack Overflow 通常是提问的好地方。 此外,Discord 上的 Reactiflux 社区有很多聊天频道,专门讨论 React 和相关技术,总有一些人愿意闲聊和提供帮助。

@markerikson谢谢。 我会尝试从 Discord 上的 Reactiflux 获得一些帮助。

跟进此事:
我找到了一种可扩展的方式来在我的项目中重用减速器,甚至在减速器中重用减速器。

命名空间范式

它的概念是,作用于许多“部分”减速器之一的动作具有“命名空间”属性,该属性确定哪个减速器将处理与_所有_处理它的减速器相反的动作,因为它们侦听相同的动作type (例如在 https://github.com/reactjs/redux/issues/822#issuecomment-172958967)

但是,不包含命名空间的操作仍将传播到其他减速器中的所有部分减速器

假设你有一个部分减速器A并且初始状态为A(undefined, {}) === Sa和一个减速器B初始状态为B(undefined, {}) === { a1: Sa, a2: Sa } ,其中键a1a2A的实例。

名称空间为['a1']的操作(* 名称空间始终是有序的字符串数组,类似于状态对部分 reducer 的键)投射到B将产生以下结果

const action = {
  type: UNIQUE_ID,
  namespace: ['a1']
};

B(undefined, action) == { a1: A(undefined, action*), a2: Sa }

以及没有命名空间的动作的反例

const action = {
  type: UNIQUE_ID
};


B(undefined, action) == { a1: A(undefined, action), a2: A(undefined, action) }

注意事项

  • 如果A不处理给定的动作(它耗尽减速器)它应该返回相同的状态,这意味着对于减速器A无法处理的动作p #$ B(undefined, p)的结果应该是{ a1: Sa, a2: Sa }顺便说一下与B的初始状态相同
  • 传递给部分 reducer 的操作(上面表示为action* )必须剥离用于缩小范围的命名空间。 因此,如果传递给B的操作是{ type: UNIQUE_ID, namespace: ['a1'] } ,那么传递给A的操作是{ type: UNIQUE_ID, namespace: [] }
  • 为了实现这种结构,store 中的所有 reducer 都必须处理命名空间操作,如果满足这一点,则对您使用的命名空间数量以及可以嵌套多少部分 reducer 没有限制

为了实现这一点,我提出了一些伪代码来处理减速器中的命名空间。 为此,我们必须事先知道减速器是否可以处理一个动作以及减速器中存在的部分减速器的数量。

(state = initialState, { ...action, namespace = [] }) => {
    var partialAction = { ...action, namespace: namespace.slice(1) };
    var newState;
    if (reducerCanHandleAction(reducer, action) and namespaceExistsInState(namespace, state)) {
        // apply the action to the matching partial reducer
        newState = {
            ...state,
            [namespace]: partialReducers[namespace](state[namespace], partialAction)
        };
    } else if (reducerCantHandleAction(reducer, action) {
        // apply the action to all partial reducers
        newState = Object.assign(
            {},
            state,
            ...Object.keys(partialReducers).map(
                namespace => partialReducers[namespace](state[namespace], action)
            )
        );
    } else {
        // can't handle the action
        return state;
    }

    return reducer(newState, action);
}

由您决定如何事先决定减速器是否可以处理动作,我使用一个对象映射,其中动作类型是键,处理函数是值。

我可能有点迟到了,但我写了一些通用的减速器,这可能会有所帮助:
https://gist.github.com/crisu83/42ecffccad9d04c74605fbc75c9dc9d1

我认为mutilreducer是一个很好的实现

@jeffhtli multireducer 不是一个好的解决方案,因为它不允许未定义数量的减速器,而是先发制人地要求您构建静态减速器列表
相反,我创建了一个小项目来解决这个问题,它使用每个组件实例的 UUID 和每个 UUID 的唯一状态
https://github.com/eloytoro/react-redux-uuid

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

相关问题

mickeyreiss-visor picture mickeyreiss-visor  ·  3评论

dmitry-zaets picture dmitry-zaets  ·  3评论

timdorr picture timdorr  ·  3评论

ramakay picture ramakay  ·  3评论

captbaritone picture captbaritone  ·  3评论