Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
pe3 opened this issue Sep 30, 2015 · 50 comments
Closed

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

pe3 opened this issue Sep 30, 2015 · 50 comments
Labels

Comments

@pe3
Copy link

pe3 commented Sep 30, 2015

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.

@pe3 pe3 changed the title How to create editor-like functionality? How to create editor-like dynamic functionality? Sep 30, 2015
@pe3 pe3 changed the title How to create editor-like dynamic functionality? How to create editor-like functionality? Sep 30, 2015
@gaearon
Copy link
Contributor

gaearon commented Sep 30, 2015

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

@gaearon
Copy link
Contributor

gaearon commented Sep 30, 2015

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
Copy link
Author

pe3 commented Sep 30, 2015

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

@pe3
Copy link
Author

pe3 commented Oct 1, 2015

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
Copy link
Contributor

gaearon commented Oct 1, 2015

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
Copy link
Author

pe3 commented Oct 2, 2015

@gaearon how should the list action get its index?

@Zeikko
Copy link

Zeikko commented Oct 2, 2015

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: Zeikko@6a22288

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

@pe3
Copy link
Author

pe3 commented Oct 3, 2015

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

@gaearon
Copy link
Contributor

gaearon commented Oct 3, 2015

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
Copy link
Contributor

gaearon commented Oct 3, 2015

I think I get what you mean now.

@pe3
Copy link
Author

pe3 commented Oct 3, 2015

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

@gaearon
Copy link
Contributor

gaearon commented Oct 3, 2015

I understand the problem now. Looking into it.

@gaearon
Copy link
Contributor

gaearon commented Oct 3, 2015

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

@gaearon gaearon changed the title How to create editor-like functionality? How to create a generic list as a reducer and component enhancer? Oct 3, 2015
@gaearon
Copy link
Contributor

gaearon commented Oct 3, 2015

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
Copy link

tomsdev commented Oct 3, 2015

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

@magnusjt
Copy link

magnusjt commented Oct 4, 2015

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
Copy link

ccorcos commented Oct 15, 2015

@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:
#897

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

@ccorcos
Copy link

ccorcos commented Oct 15, 2015

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
Copy link
Contributor

gaearon commented Oct 15, 2015

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

@ccorcos
Copy link

ccorcos commented Oct 15, 2015

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

@yelouafi
Copy link

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
Copy link
Contributor

gaearon commented Nov 18, 2015

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 gaearon closed this as completed Nov 18, 2015
@elado
Copy link

elado commented Jan 22, 2016

@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
Copy link

ccorcos commented Jan 22, 2016

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
Copy link

elado commented Jan 28, 2016

@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
Copy link

ccorcos commented Jan 28, 2016

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
Copy link

ghost commented Jan 28, 2016

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
Copy link
Contributor

gaearon commented Jan 28, 2016

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

@ghost
Copy link

ghost commented Jan 28, 2016

Aha.

@dnutels
Copy link

dnutels commented Feb 20, 2016

@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
Copy link
Contributor

gaearon commented Feb 20, 2016

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
Copy link

dnutels commented Feb 20, 2016

Thank you.

@iazel
Copy link

iazel commented Apr 8, 2016

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

List of Counters

Thanks!

@eloytoro
Copy link

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
Copy link
Contributor

gaearon commented Apr 14, 2016

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

@eloytoro
Copy link

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
Copy link
Contributor

gaearon commented Apr 14, 2016

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
Copy link

deevus commented May 16, 2016

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
Copy link
Contributor

@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
Copy link

deevus commented May 16, 2016

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

@eloytoro
Copy link

eloytoro commented May 16, 2016

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 #822 (comment))

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.

@crisu83
Copy link

crisu83 commented Jul 1, 2016

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
Copy link

jeffhtli commented Nov 9, 2016

I think mutilreducer is a great implementation

@eloytoro
Copy link

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests