Redux: How to create a generic list as a reducer and component enhancer?

Created on 30 Sep 2015  ·  50Comments  ·  Source: reduxjs/redux

Hello. What would be a good way to extend the counter example to a dynamic list of independent counters?

By dynamic I mean that in the UI at the end of the list of counters there would be + and - buttons for adding a new counter to the list or removing the last one.

Ideally the counter reducer and component would stay as they are. How would one create a generalized list store+component to collect any kind of entities? Would it be possible to generalize the list store+component even further to take both counters and todo-items from the todomvc-example?

It would be great to have something like this in the examples.

examples

Most helpful comment

How would one create a generalized list store+component to collect any kind of entities? Would it be possible to generalize the list store+component even further to take both counters and todo-items from the todomvc-example?

Yes, definitely possible.
You'd want to create a higher order reducer and a higher order component.

For higher order reducer see the approach here:

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
});

(I haven't run it but it should work with minimal changes.)

All 50 comments

I agree this is a good example.
Redux is similar here to Elm Architecture so feel free to get inspired by examples there.

How would one create a generalized list store+component to collect any kind of entities? Would it be possible to generalize the list store+component even further to take both counters and todo-items from the todomvc-example?

Yes, definitely possible.
You'd want to create a higher order reducer and a higher order component.

For higher order reducer see the approach here:

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
});

(I haven't run it but it should work with minimal changes.)

Thanks - I'll try to get this approach working.

I'm still wondering if I can reuse the list functionality through combineReducers to have both a list of counters and a list of todo-items. And if that would make sense. But I will definitely give it a try.

I'm still wondering if I can reuse the list functionality through combineReducers to have both a list of counters and a list of todo-items. And if that would make sense. But I will definitely give it a try.

Yes, totally:

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

@gaearon how should the list action get its index?

We managed to create a higher order reducer with your instructions but we are struggling with the higher order component. Currently our component is not generic enough to be used with other components than Counter. Our issue is how to add the index to the actions in a generic way.

You can see our solution here: https://github.com/Zeikko/redux/commit/6a222885c8c93950dbdd0d4cf3532cd99a32206c

I added some a comment to the commit to highlight the problematic part.

It would be great to have a have a general list reducer + component who can do a lists of lists of counters.

Currently our component is not generic enough to be used with other components than Counter. Our issue is how to add the index to the actions in a generic way.

Can you explain what do you mean by “adding the index in a generic way”? Do you mean that you want to have different names for index key in the action?

I think I get what you mean now.

Sorry I'm not able to comment much just now. I'll get back tomorrow.

I understand the problem now. Looking into it.

I quickly bumped into some limitations inherent to how Redux deviates from Elm architecture.
It's a pity I didn't understand them before!

  • By the time component is wrapped, it needs to accept a dispatch prop and not callbacks. This means you'd need to either avoid making action creators your props and just use dispatch() in the composed components, or we need to introduce a bindActionCreators(Component, actionCreators) => Component that is just like connect() but doesn't actually connects to the store, instead just replacing action-creator-as-props-wanting-component with this.props.dispatch-wanting-component.
  • You can't dispatch middleware-requiring actions from wrapped components. This is a bummer! If I wrap counter with a list, I can no longer dispatch incrementAsync() because function (dispatch, getState) { ... } I just dispatched is turned into { action: function (dispatch, getState) { ... } } by the list—and bam! the thunk middleware no longer recognizes it.

There may be non-awkward solutions to this, but I don't see them yet.
For now, please see this commit as an example (with the limitations described above).

Here's the code:

components/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;

components/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>
        )
      }
    }
  };
}

actions/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);
  };
}

actions/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
  };
}

reducers/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;
  }
}

reducers/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;
    }
  }
}

reducers/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;

containers/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 — here's an example of current middleware + React Redux design breaking down somewhat.
We can declare this as wontfix but maybe you'd like to take a look if there's any way we can dodge this.

I've been experimenting with the use of a service (IoC) container for React, and created this test-repo yesterday: https://github.com/magnusjt/react-ioc

I think it could potentially solve part of the problem, since you can pass down an action creator to the Counter without CounterList knowing about it. This is possible because the action creator goes in the constructor for Counter, not in the props.

For every new Counter component you create, you can pass a different action creator (perhaps by binding an index value to the action creator). Of course you still have the problem with getting the data down to the counter. I'm not sure yet if that is something that could be solved with a service container.

@gaearon, your example looks about right to me. You have to pass the action creators and dispatch all the way down. This way you can altar actions with high-order functions.

I'm not so sure your second point is necessary though. You'll miss the middleware because of the new message format, but a bigger issue with performInList is that you've limited abstraction to just one list up. @pe3 mentioned a list of lists of counters. For arbitrary abstraction like that, I think you'll need to nest actions somehow.

In this ticket, I come up with a way for nesting the types:
https://github.com/rackt/redux/issues/897

But I think more fundamentally, you'll want to nest the actions entirely....

Ok, I just had a go at it.

I simplified things quite a bit. Here's the counter app before doing this fancy stuff:

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

And here it is after:

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

I'm not sure "lift" is the proper term -- I know it means something to in functional programming, but felt ok to me.

Basically, by lifting an action, you're nesting an action within another.

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

And the nested action gets pealed away by lifting the reducer. The lifting reducer basically applies the sub-reducer (which is partially applied with the appropriate action) to some substate.

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

So for the list of components reducer, I have an action that specifies which component at which index the sub-action applies to.

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

And I have a "high-order" (another fancy term that just felt right, haha ;) reducer that applies the sub-reducer to the appropriate sub-state.

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;
  }
}

And all thats left is to "lift" the count reducer into the list reducer.

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

And now for the list of counters, we just need to lift the actions as we pass them down to the counters.

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>
    );
  }
}

I think this could be more formalized with proper lingo. I think lenses could be used here for the high-order reducers as well, but I've never successfully used them, haha.

And I take back what I said in the last comment -- @gaearon is right. By nesting the action like this, you're going to miss the middleware, and you have to pass dispatch all the way down so you can manipulate the action creators. Perhaps to support this, Redux will have to apply all sub-actions through the middleware. Also, another issue is initializing the state within the list...

What you're describing is known as Elm Architecture. Please see here: https://github.com/gaearon/react-elmish-example

Dude, you're always a step ahead! Shower me with links to cool stuff :+1:

You can't dispatch middleware-requiring actions from wrapped components. This is a bummer! If I wrap counter with a list, I can no longer dispatch incrementAsync() because function (dispatch, getState) { ... } I just dispatched is turned into { action: function (dispatch, getState) { ... } } by the list—and bam! the thunk middleware no longer recognizes it.

@gaearon how about this solution ? instead of the generic reducer calling itself the child reducer like this

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

provide the store with some special method dispatchTo(reducer, state, action, callback) which acts like dispatch except it dispatch directly to a child reducer (through all configured middelwares) and notify the callback on each child state modification

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;
    }
  }
}

I'm not aware if this is doable in Redux. An idea is to implement dispatchTo using some internal store.derive(reducer, state) method which would return a child store for that portion of state tree configured with the some middlewares as the root store. for example

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

This is just an idea as i said i'm not aware of the internals of Redux so maybe i missed something

EDIT
_probably this is going be awkward since reducer methods are supposed to be synchronous. making a method async implies all the chaine up has to be async as well.
Maybe the best solution is to expoer directly the store.derive(reducer) method and build generic reducers using some kind of Store composition_

That's too much a complication and isn't worth it IMO. If you'd like to do it this way, just don't use the middleware (or use some alternative implementation of applyMiddleware) and you're all set.

Also, I'm closing because we don't plan to act on this.

@gaearon for the sake of discussion:
the code example you attached in this commit: https://github.com/rackt/redux/commit/a83002aed8e36f901ebb5f139dd14ce9c2e4cab4

In case I have 2 lists of counters (or even separate 'models'), dispatching addToList would add item to both lists, because action types are the same.

// 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;

so how does the reducers/list help here? Don't you need to prefix action types or something?

In case I have 2 lists of counters (or even separate 'models'), dispatching addToList would add item to both lists, because action types are the same.

Please take a close look at a83002aed8e36f901ebb5f139dd14ce9c2e4cab4. It nests actions. dispatch that is passed down from the container component wraps actions into performInList action. Then the inner action is retrieved in the reducer. This is pretty much how Elm Architecture works.

@gaearon maybe I'm missing something, but along with the extra counterList2 reducer mentioned above, this UI still updates both lists on each action (which is expected according to how it's built, but what's the solution?):

// 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 you'll need to wrap it in a list again so that the actions don't clash for the two lists, just in the same way we did with the list of counters

@ccorcos

to wrap it in a list again

wrap what exactly?

@ccorcos
I uploaded the example here: http://elado.s3-website-us-west-1.amazonaws.com/redux-counter/
It has source maps

Still not sure entirely what you meant. As I said - the current behavior is the expected one because action names are the same, and there's no indication in the action creator in which list to perform it, so it runs the action on all reducers which eventually affects both lists.

So I don't know the actual Redux functions very well, but I'm very familiar with elm.

In this new example, we're not binding the action creators at the top level. Instead, we pass the dispatch function down to the lower components and those lower components can pass an action into the dispatch function.

To get abstraction working well so we don't have action collisions, when the "listOf" component passes the dispatch down to its children, it actually passes a function that wraps the action in a format that the list component can understand.

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

So now you can compose listOf(counter) or listOf(listOf(counter)) and if you want to create a component called pairOf, then you need to make sure to wrap the actions when you pass them down. Right now, the App component just renders them side by side without wrapping the dispatch functions, thus you have action collisions.

@ccorcos

Thanks. So to conclude, there's obviously no "magic" happening, actions need all the info they need to tell the reducer which instance to perform the action on. If it's a listOf(listOf(counter)) the action will need both indices of the 2d array. listOf(listOf(counter)) can work but having all counters in a single flat list indexed by unique ID which is the only thing passed in an action seems more flexible.

It looks like the only way to build a flexible and complex Redux app is by having all entities in the system indexed by ID in the store. Any other simpler approach, which is sometimes given in examples, will reach its limits fast. This index is almost a mirror of a relational DB.

the action will need both indices of the 2d array

its sounds like youre thinking and action looks like:

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

but it should really look like:

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

That way you can do a listOf(listOf(listOf(...listOf(counter)...))) forever!

This all comes from Elm, btw. Check out the Elm Architecture Tutorial

I'm a little slow to the party, but I don't see where this "doesn't work with middleware"? If you are offering infinite nesting as @ccorcos, can't you just wire up middleware to handle the nesting? Or are we talking exclusively about redux-thunk where nested actions would mean weirdness?

How would middleware know whether to interpret action as is, or look for nested actions?

Aha.

@gaearon

Hi.

I am not positive I understood the resolution on the issue and would really appreciate a simple answer.

Is it _do not use middleware (and basically most of the Redux ecosystem), if you have multiple copies of component on the same page_? I also didn't see whether someone responded to multireducer suggestion.

Any clarification would help.

No, this is not the resolution. The thread is not about having multiple identities of a component on the page. To implement this you can just pass an ID in the action. The thread was about _writing a generic function to do that_. Which unfortunately does clash with the concept of middleware.

Thank you.

Hello everyone,
Maybe I solved this problem, however I prefer to use deku over react ^^

I would be happy if someone give me his insight about this experiment, especially about the taskMiddleware idea. @gaearon do you have some time to check it out? :tongue:

List of Counters

Thanks!

Just want to clarify that none of these solutions aim to handle an initial state.
Could this be achieved somehow @gaearon ? Allowing the List reducer above to have an initial list of counters?

Why would that be problematic? I imagine you could call the child reducers with undefined state and use those values.

When the store is initialized with the first dispatch I need (for my internal logic) to have an initial state for that list, and it should be a predefined list of counters (the type of the list's items)

Something like this?

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

You could make this an argument to list as well if you’d like

This seems really complicated just so I can have multiple of the same component on a page.

I'm still wrapping my head around Redux, but creating multiple stores seems much simpler, even if its not a recommended Redux usage pattern.

@deevus : don't let some of these discussions scare you off. There's a number of people in the Redux community who are very oriented towards Functional Programming, and while some of the concepts discussed in this thread and other similar ones have value, they also tend to be something of an attempt to go for the "perfect", rather than the merely "good".

You can totally have multiple instances of a component on a page, in general. What this discussion is aiming for is arbitrary composition of nested components, which is interesting, but also not something that most apps will need to do.

If you've got specific concerns beyond that, Stack Overflow is usually a good place to ask question. Also, the Reactiflux community on Discord has a bunch of chat channels dedicate to discussing React and related technologies, and there's always some people hanging out willing to talk and help out.

@markerikson Thanks. I'll try and get some help from Reactiflux on Discord.

Following up on this:
I found a scalable way to reuse reducers in my project, even reuse reducers within reducers.

The namespace paradigm

Its the concept that actions that act upon one of many "partial" reducers hold a "namespace" property which determines which reducer will handle the action contrary to _all_ the reducers handling it because they listen the same action type (example in https://github.com/reactjs/redux/issues/822#issuecomment-172958967)

However actions that don't hold a namespace will still be propagated to all partial reducers within other reducers

Say you have the partial reducer A and with an initial state of A(undefined, {}) === Sa and a Reducer B with an initial state of B(undefined, {}) === { a1: Sa, a2: Sa } where the keys a1 and a2 are instances of A.

An action with namespace of ['a1'] (* namespaces are always ordered arrays of strings that resemble the state's key to the partial reducer) cast upon B will produce the following result

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

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

And the counter example of an action with no namespace

const action = {
  type: UNIQUE_ID
};


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

Caveats

  • If A doesn't handle the given action (it exhaust the reducer) it should return the same state, this means that for an action p that is unhandable for reducer A the result of B(undefined, p) should be { a1: Sa, a2: Sa } which is incidentally the same as the initial state for B
  • The action passed down to partial reducers (denoted as action* above) must be stripped of the namespace used to narrow down the scope. So if the action passed to B was { type: UNIQUE_ID, namespace: ['a1'] } then the action passed down to A is { type: UNIQUE_ID, namespace: [] }
  • In order to achieve this structure all the reducers in the store must handle namespacing actions, if this point is met there's no limit on how many namespaces youre using and how many partial reducers you can nest

In order to achieve this I've come up with some pseudocode for handling namespaces in your reducer. For this to work we must know beforehand if a reducer can handle an action and the amount of partial reducers that exist in the 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);
}

Its up to you how to decide if the reducer can or can't handle the action beforehand, I use an object map in which the action types are the keys and the handler functions are the values.

I might be a bit late to the game but I wrote some generic-purpose reducers which might help:
https://gist.github.com/crisu83/42ecffccad9d04c74605fbc75c9dc9d1

I think mutilreducer is a great implementation

@jeffhtli multireducer is not a good solution because it doesnt allow an undefined amount of reducers, instead it preemptively asks you to build a static reducer list
Instead I created a small project to solve this issue using UUIDs for each instance of components and a unique state for each UUID
https://github.com/eloytoro/react-redux-uuid

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ramakay picture ramakay  ·  3Comments

ms88privat picture ms88privat  ·  3Comments

mickeyreiss-visor picture mickeyreiss-visor  ·  3Comments

ilearnio picture ilearnio  ·  3Comments

captbaritone picture captbaritone  ·  3Comments