ioof-holdings/redux-subspace

RFC: Components sharing state between subspaces

mpeyper opened this issue · 4 comments

We recently had a requirement come up where two related components needed to share state with each other. Both components came from the same micro-frontend package and shared the same reducer and actions. The problem was that one of the components was nested inside another subspace. The component hierarchy was something like:

  • app
    • summary
      • component1
    • component2

redux-subspace is quite good (IMHO) at encapsulating the the summary state from the component2 state, so consequently the resulting state structure was:

  • appState
    • summaryState
      • componentState
    • componentState

with the store being created like:

const summaryReducer = combineReducers({
  // other reducers
  componentState: namespaced('component')(componentReducer)
})

const reducer = combineReducers({
  summary: namespaced('summary')(summaryReducer),
  // other reducers
  componentState: namespaced('component')(componentReducer)
})

const store = createStore(reducer /*, ... */)

Note: component1 and component2 are expecting to share the state in the same reducer, which was not occurring.

The hack solution we used to get around this was to wrap component2 in a subspace to fake out the summary layer. The simplified version looked something like:

<App>
  <SubspaceProvider mapState={state => state.summaryState} namespace="summary">
    <Summary>
      {/* lots of other stuff */}
      <SubspaceProvider mapState={state => state.componentState} namespace="component">
        <Component1 />
      </SubspaceProvider>
    </Summary>
  </SubspaceProvider>
  {/* lots of other stuff */}
  <SubspaceProvider mapState={state => state.summaryState} namespace="summary">
    <SubspaceProvider mapState={state => state.componentState} namespace="component">
      <Component2 />
    </SubspaceProvider>
  </SubspaceProvider>
</App>

and the store is created as:

const summaryReducer = combineReducers({
  // other reducers
  componentState: namespaced('component')(componentReducer)
})

const reducer = combineReducers({
  summary: namespaced('summary')(summaryReducer),
  // other reducers
})

const store = createStore(reducer /*, ... */)

This worked and we were able to move on, but it bothers me that we had to fake a layer of the state to allow the components to work as intended. We were also lucky that we were actually able to push component2's state down a layer to make it work. I can quite easily see a situation where both components are in entirely different levels in the tree, e.g.

  • app
    • header
      • component1
    • summary
      • component2

Our solution could not work in this scenario.

As soon as this occurred, @jpeyper and I started spit-balling how redux-subspace could help out in scenarios like this and we came up with this.

The idea is that you could flag a subspace as transparent which will behave as a normal subspace for any component mounted and action dispatched within it, but if a subspace created from it it will use it's parent's state and namespace instead of it's own, as if it doesn't exist to the nested subspaces.

Effectively, it's just following React's advice for these types of scenarios and allowing the common state to be lifted up to to the common ancestor.

Going back to the above example, it would change to:

<App>
 <SubspaceProvider mapState={state => state.summaryState} namespace="summary" transparent={true}>
   <Summary>
     {/* lots of other stuff */}
     <SubspaceProvider mapState={state => state.componentState} namespace="component">
       <Component1 />
     </SubspaceProvider>
   </Summary>
 </SubspaceProvider>
 {/* lots of other stuff */}
 <SubspaceProvider mapState={state => state.componentState} namespace="component">
   <Component2 />
 </SubspaceProvider>
</App>

and the store could now be created as:

const summaryReducer = combineReducers({
  // other reducers
})

const reducer = combineReducers({
  summary: namespaced('summary')(summaryReducer),
  // other reducers
  componentState: namespaced('component')(componentReducer)
})

const store = createStore(reducer /*, ... */)

I did a quick proof of concept locally and it can be done, so now I have some questions:

  1. Do we want this?

  2. Is transparent the correct term for this?
    It came about because a child subspace would look straight it to it's parent when resolving state and namespaces

  3. Should it be all or nothing?
    Under the current proposal, transparent={true} would result in all nested subspaces ignoring the it. Instead, we could use an array of namespaces (regex?) to give more control, e.g.:

    <App>
      <SubspaceProvider mapState={state => state.summaryState} namespace="summary" transparentTo={['component' /*, ... */]}>
        <Summary>
          {/* lots of other stuff */}
          <SubspaceProvider mapState={state => state.componentState} namespace="component">
            <Component1 />
          </SubspaceProvider>
        </Summary>
      </SubspaceProvider>
      {/* lots of other stuff */}
      <SubspaceProvider mapState={state => state.componentState} namespace="component">
        <Component2 />
      </SubspaceProvider>
    </App>

    If we allow regex, perhaps transparent={true} could be used as an alias for transparentTo={[/.*/]}.

I'm interested in hearing any thoughts on the above, alternative solutions and answers to my questions.

Cheers.

I ran into other issue when using nested react-router-config's object and <Route/>'s with react-redux-subspace's subspaced hoc.

My action namespaces for nested components, like:

  • A_1
    • B_1
    • C_1
  • A_2
    • B_2
    • C_2

NOTE: A_1 and A_2 are using the same subspaced reducer structure. Same goes for other capital letters.

They went berserk for B_1 to something like: namespaceA_2/namespaceA_1/namespaceB_1/ACTION, while I expected namespaceA_1/namespaceB_1/ACTION. other routes are also weird.

For now I was forced to make my custom redux middleware to correct action namespaces to solve my issue, but creating one was very painful, I'll try to describe it in a new issue at some point.

We definitely need more power over nested namespaces.

Hi @tlenex,

Something sounds up there. The actions should only every get a namespace of a subspace that they pass through, so I'm not sure how you'd end up with a namespace of namespaceA_2/namespaceA_1/namespaceB_1/ACTION given the component structure described.

I'm not familiar with react-router-config. Can you share some code or set up an example in codesandbox.io to show what you are seeing?

@mpeyper sure, I'll try to prepare short version this weekend

Closing due to inactivity. Happy to repoen if there is more discussion to be had.