-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
Comments
I agree this is a good example. |
Yes, definitely possible. 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 |
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 |
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. |
It would be great to have a have a general list reducer + component who can do a lists of lists of counters. |
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 |
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.
There may be non-awkward solutions to this, but I don't see them yet. Here's the code: components/Counter.jsimport 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.jsimport 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.jsexport 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.jsexport 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.jsimport { 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.jsimport { 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.jsimport { combineReducers } from 'redux';
import counter from './counter';
import list from './list'
const counterList = list(counter);
const rootReducer = combineReducers({
counterList
});
export default rootReducer; containers/App.jsimport { 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. |
Would |
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 In this ticket, I come up with a way for nesting the types: 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: And here it is after: 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 👍 |
@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 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 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 |
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 |
@ccorcos 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 |
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 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. |
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 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 |
How would middleware know whether to interpret action as is, or look for nested actions? |
Aha. |
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, I would be happy if someone give me his insight about this experiment, especially about the Thanks! |
Just want to clarify that none of these solutions aim to handle an initial state. |
Why would that be problematic? I imagine you could call the child reducers with |
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 |
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: 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 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 An action with namespace of 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. |
I might be a bit late to the game but I wrote some generic-purpose reducers which might help: |
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 |
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.
The text was updated successfully, but these errors were encountered: