wcjohnson / redux-components

A component model for Redux state trees.
https://wcjohnson.github.io/redux-components
MIT License
4 stars 1 forks source link

Dynamic "Map" component and reducer combinator #4

Closed Diggsey closed 7 years ago

Diggsey commented 7 years ago

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 implements ADD and REMOVE actions itself. These actions would allow dynamically changing the set of child components and reducers.

The sequence of actions would be:

This 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 a componentMove 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.

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

wcjohnson commented 7 years ago

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:

At any rate, I'm open to thoughts and PRs on this. A good implementation would be a very useful addition.

Diggsey commented 7 years ago

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

markerikson commented 7 years ago

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!

wcjohnson commented 7 years ago

@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: (state) => (state,action) => nextState

So, advantages:

Disadvantages:

Things that need to be tested intensively:

Can you see anything I don't?

wcjohnson commented 7 years ago

@markerikson No problem! If you're here, I'd welcome any thoughts.

wcjohnson commented 7 years ago

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.

wcjohnson commented 7 years ago

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