reduxjs / redux

A JS library for predictable global state management
https://redux.js.org
MIT License
60.88k stars 15.27k forks source link

How to create a generic list as a reducer and component enhancer? #822

Closed pe3 closed 8 years ago

pe3 commented 9 years ago

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.

gaearon commented 9 years ago

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

gaearon commented 9 years ago

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.)

pe3 commented 9 years ago

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

pe3 commented 9 years ago

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.

gaearon commented 9 years ago

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'
  }),
});
pe3 commented 9 years ago

@gaearon how should the list action get its index?

Zeikko commented 9 years ago

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.

pe3 commented 9 years ago

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

gaearon commented 9 years ago

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?

gaearon commented 9 years ago

I think I get what you mean now.

pe3 commented 9 years ago

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

gaearon commented 9 years ago

I understand the problem now. Looking into it.

gaearon commented 9 years ago

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

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);
gaearon commented 9 years ago

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.

tomsdev commented 9 years ago

Would https://github.com/erikras/multireducer/ help?

magnusjt commented 9 years ago

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.

ccorcos commented 9 years ago

@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....

ccorcos commented 9 years ago

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

gaearon commented 9 years ago

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

ccorcos commented 9 years ago

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

yelouafi commented 8 years ago

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

gaearon commented 8 years ago

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.

gaearon commented 8 years ago

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

elado commented 8 years ago

@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?

gaearon commented 8 years ago

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.

elado commented 8 years ago

@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>
    )
  }
}
ccorcos commented 8 years ago

@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

elado commented 8 years ago

@ccorcos

to wrap it in a list again

wrap what exactly?

elado commented 8 years ago

@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.

ccorcos commented 8 years ago

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.

elado commented 8 years ago

@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.

ccorcos commented 8 years ago

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

ghost commented 8 years ago

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?

gaearon commented 8 years ago

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

ghost commented 8 years ago

Aha.

dnutels commented 8 years ago

@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.

gaearon commented 8 years ago

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.

dnutels commented 8 years ago

Thank you.

iazel commented 8 years ago

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!

eloytoro commented 8 years ago

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?

gaearon commented 8 years ago

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

eloytoro commented 8 years ago

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)

gaearon commented 8 years ago

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

deevus commented 8 years ago

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.

markerikson commented 8 years ago

@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.

deevus commented 8 years ago

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

eloytoro commented 8 years ago

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

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.

crisu83 commented 8 years ago

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

jeffhtli commented 7 years ago

I think mutilreducer is a great implementation

eloytoro commented 7 years ago

@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