Closed Diggsey closed 7 years ago
Ah... not exactly sure what you're trying to propose here. What "subtree mixin" are you referring to?
There are definitely a number of existing packages that implement various approaches to per-component state stored in Redux, as well as dynamically adding and removing reducer logic. See https://github.com/markerikson/redux-ecosystem-links/blob/master/component-state.md and https://github.com/markerikson/redux-ecosystem-links/blob/master/reducers.md for some examples.
You may also be interested in reading through the Structuring Reducers section of the Redux docs, as well as the "Redux Techniques#Encapsulation" section of my React/Redux links list.
I actually have a (not great) implementation of this called DynamicSubtreeMixin
that I was working on internally some time ago. I intended to put it in the distribution, but it never met my quality standards. In particular, I was never able to solve the core design problems to my satisfaction:
(typeName: String) -> ReduxComponentDescriptor
provided as the DynamicSubtreeComponent
is mounted?Making the root store rebuild its reducer. I talked to some people in the Redux community when I was designing this, and they advise me (correctly, I think) that people will be using all kinds of different Redux libraries and it would be better if redux-components didn't "take over" the Redux tree or prevent the usage of other libraries. That is the reason for the somewhat ugly "mounter" business.
Any dynamic tree configuration requires the ability to ask the root store to mount a new reducer, which either requires the user to write their own mounter, or to have the root (and all the nodes between the root and the DynamicSubtree
) be driven by ReduxComponents.
I like the latter approach a lot, because it really makes the whole thing easier for a user to work with. However, it would make some tree configurations impossible as well. It doesn't prevent working with other libraries, but it does mean you have to hang them on some node beneath the root, rather than at the root.
At any rate, I'm open to thoughts and PRs on this. A good implementation would be a very useful addition.
@wcjohnson
The classic object-serialization problem: the need for a type map.
Once nice solution would be to make the map generic over the component type, so it already knows in advance what component type it will be instantiating for values. This avoids the need to encode the component type in actions at all, but means you can't have components of different types in the same map (probably not great practice anyway...)
Time travel
As you say, I think it's do-able, if maybe difficult.
Making the root store rebuild its reducer.
I don't think this is necessary. If you look at how combineReducers
is implemented, it should be possible to copy that but give the child reducer list inner mutability. As long as it only gets mutated via actions sent to the parent reducer (add/remove) it should maintain the invariants that redux expects, even though it's outside the state object. The root reducer need never change.
This could be a problem as it's extra state that's not part of the store. It could be solved by having the add
/remove
actions only modify the state, and then automatically mount/unmount components in response to changes in the state compared to the current set of child components.
Oh dear. I apologize. I had forgotten I had subscribed to this repo, and just popped open the notification link without checking what repo it was in. I assumed it was in the main Redux repo. Sorry for the confusion!
@Diggsey
This could be a problem as it's extra state that's not part of the store.
Yes, I think this is the long and the short of it. And there's no way to do this without creating state that's not a part of the store, because fundamentally, a reducer is a function and a function can't be state.
However, as you pointed out, if the state that's not a part of the store is a pure function of the state that is, and we can guarantee it gets synced with the store, that should still be OK.
So here are my thoughts right now:
getReducer()
is now passed the local state
as an argument. When first mounting, this will be the initial state. So:getReducer: (state) => (state,action) => nextState
@@REPLACE_REDUCER
, will be added.@@REPLACE_REDUCER
action, it will hot swap the "real" reducer by calling getReducer()
.So, advantages:
getReducer()
is only receiving information that already is in the store.Disadvantages:
@@REPLACE_REDUCER
purely?Things that need to be tested intensively:
Can you see anything I don't?
@markerikson No problem! If you're here, I'd welcome any thoughts.
OK, so if I implement the changes I just described, then as far as I can see, Map just looks like this:
Map = (typeMap) ->
createClass {
verbs: [ 'ADD', 'REMOVE' ]
getReducer: (myState) ->
reducers = transformValues(myState.__metadata, (x) -> typeMap(x) )
combineReducers(myState.__metadata)
add: (key, typeKey) ->
@store.dispatch({ type: @ADD, key, typeKey })
@store.dispatch({ type: "@@REPLACE_REDUCER" })
remove: (key, typeKey) ->
@store.dispatch({ type: @REMOVE, key, typeKey })
@store.dispatch({ type: "@@REPLACE_REDUCER" })
}
And if the @@REPLACE_REDUCER
stuff works with time travel, so should Map
!
OK, unless anyone can think of a major problem here, I think I will try to implement this in the 0.2 branch.
@Diggsey
I just published (what I believe to be) a fully correct implementation of Map. It tests successfully under store rehydration and time travel scenarios.
Check out redux-components-map@0.0.1.
You will need redux-components@0.2.1 to use redux-components-map.
Currently the subtree mixin allows you to have a predetermined component structure, which remains static throughout the application lifetime. This directly maps onto the redux
combineReducers
function.It would be possible to implement a new combinator, say
combineReducersMap
, which as well as delegating actions to sub-reducers, also implementsADD
andREMOVE
actions itself. These actions would allow dynamically changing the set of child components and reducers.The sequence of actions would be:
ADD
action received, with a key and component typeThis could then be encapsulated inside a
Map
component, which implements add and remove action creators.Unfortunately it would be difficult to do the same thing for a
List
type, since the way this is currently built, each component is aware of its full path, and so it would be difficult to update all of the paths. Maybe with acomponentMove
event or similar, it would be possible...The motivation is to be able to dynamically change the layout of stores - for example, you might have allow logging into multiple accounts at the same time, like google does, in which case you can put the vast majority of your application state inside a component which gets instantiated once per logged-in account.