-
Notifications
You must be signed in to change notification settings - Fork 33
Dynamic namespaces #94
Comments
Hi @Thom1729, Thanks for taking the time to reach out to us, I'll do my best to answer your questions and give you some insights.
Just to make sure I understand correctly, you have a Component you want to mount and a reducer you want to use in the store an variable number of time, but once included you want them to operate indecently from one another
This was the original brief when we set out to build redux-subspace. The different action types is not quite right, in that the whole point of the namespacing was that multiple sub-applications were using the same action type for different outcomes, so we needed to isolate them away from each other.
I have heard this request before in our other libraries (which build upon redux-subspace)
It seems to be a common desire to want a dynamic subspaces, so I'm happy to consider and discuss it here.
If you know what that array of namespace/component/reducer combinations up front then you can do something like this. Essentially the idea is to build the If you don't know up front, then there is nothing currently in redux-subspace to accomodate this. You could potentially do something with the redux-dynostore The other idea I've had is similar to your suggestion of allowing subspaces to accept an Something like: const App = (
<div>
<SubspaceProvider namespace={'subApp'} instance={1}>
<SubApp />
</SubspaceProvider>
<SubspaceProvider namespace={'subApp'} instance={2}>
<SubApp />
</SubspaceProvider>
</div>
)
const reducer = combineReducers({
subApp: namespaced('subApp', { instanced: true })(subAppReducer)
} My only reservation on something like this is that namespaces are optional. I'm not sure how it would work or be set up if the a namespace was not provided. Personally, I think using redux-dynostore is the better approach (it's literally a library for doing dynamic things with redux), but it does add more complexity to the store setup and the more packages into the bundle. |
Thanks for the reply.
That is exactly it. For reference, this is the approach I'm working with, inspired by redux-subspace: import PropTypes from 'prop-types';
class StoreModifier extends React.PureComponent {
static propTypes = {
mapState: PropTypes.func,
mapAction: PropTypes.func,
};
static contextTypes = {
store: PropTypes.object.isRequired,
};
static childContextTypes = {
store: PropTypes.object,
};
getState = () => {
const { store } = this.context;
const { mapState = x=>x } = this.props;
return mapState(store.getState());
}
dispatch = (action) => {
const { store } = this.context;
const { mapAction = x=>x } = this.props;
return store.dispatch(mapAction(action))
}
getChildContext() {
const { store } = this.context;
return {
store: {
...store,
getState: this.getState,
dispatch: this.dispatch,
},
};
}
render() { return this.props.children; }
} The component: const Tabs => ({ tabs }) => <ul>
{tabs.map(tabData =>
<li key={tabData.id}>
<StoreModifier
mapState={state => selectTabState(tabData.id)}
mapAction={action => ({
...action,
context: {
tabID: tabData.id,
...action.context,
},
})}
>
<MySubApplication />
</StoreModifier>
</li>
)}
</ul>; The reducer: function tabsReducer(state = {}, action) {
const tabID = action.context.tabID;
if (tabID !== undefined) {
return {
...state,
[tabID]: subApplicationReducer(state[tabID], action),
};
} else {
return state;
}
} It's based on redux-subspace, though lacking most of the features and the general structure. It could use a couple of utility functions, such as an analogue of I think that redux-dynostore would certainly do the trick, but I'm not sure the complexity is warranted here. |
Haha, that looks very similar to redux-subspace v1 https://github.com/ioof-holdings/redux-subspace/blob/v1.0/src/SubspaceProvider.jsx, before we needed to care about middleware. I'm not sure what you are running with, but thunks are going to be pain for your I'm a bit short on time to consume all that right now, but I'll try to take a closer look soon. |
We're using saga, so no thunks. I'm not sure how to handle nonstandard action types generically. I see that the linked code has a special case for thunks. |
redux-saga was the catalyst for the v2 rewrite, and unfortunately for you, you're going to have to consider the following if you want to use them and do what you're doing: Anything that delays the dispatch of an action (i.e. most async middleware - thunk, saga, observable, etc.) are going to give you a hard time. The reason for this is that the middleware that handle those special actions had no idea about your special In the case of thunks, it was relatively easy to work around. The thunk is a function that expects In the case of sagas, this approach was not possible. You do not dispatch a saga. There is nothing in your action to indicate that it will trigger a saga. The saga's themselves are generators for plain objects that describe the effects the saga middleware should undertake. That makes them great from a testability perspective, but awful from an encapsulation perspective. To make it worse for our use case, when writing your saga, you don't even directly use So it goes something like this:
There are 2 things you can do about this... Well, 3, but one of them is give up, which I refused to do at the time (although given my time again?): So your 2 real options are:
There may be other options, but not any I could see (at the time or now). If you find any, please let me know. Option 1 is, by far, the easier approach. You have to remember to always use the context appropriately, but you can write helper functions to alleviate that. It get also get cumbersome if you have lots of sagas to deal worry about, but you can probably deal with it. There are a few triggers for when option 1 is not enough:
If none of the above is applicable, then option 1 is fine, perhaps even preferable. Depending on the size of your project and the number of devs you have, I would suggest that 1. is a worthy goal, as we have found it to be an incredibly liberating tool for teams to be able arbitrarily slice and dice out apps up into micro-frontends and build, test and deploy different parts of out apps completely independently from other teams, not to mention the cost savings of be able to reuse complex UIs across multiple apps. Option 2 is ultimately what we went with for redux subspace. Our approach to changing the
Perhaps there is more complexity here than you expected? Perhaps I've made a simple problem more complex than it needs to be? Only you will know what is right for your project. I assume that the tabs are not known at the time the store is created, otherwise this example I shared before should be a workable solution. |
Thank you for the in-depth reply.
That is fair to say. :-) That said, after thinking on your reply for a day, I think that it should be possible to use Option 1 while solving the listed difficulties and maintaining a similar API to react-subspace. A substore definition consists of the following four functions:
In my case, I want dynamic substores, so I'd define a function In pure redux, you use a reducer decorator like this: const tabsReducer = (state = [], action) => {
switch (action.type) {
case 'NEW_TAB':
return [...state, getInitialTabState(action)];
default:
return state.map(tab => decorateReducer(getTabSubstore(tab.id))(state, action));
}
} For For function* myApplicationSaga(initialTabIds) {
for (const tabId of initialTabIds) {
yield fork(tabSaga(tabId));
}
yield takeEvery('NEW_TAB', function* (action) {
yield* tabSaga(action.payload.tabId);
};
}
function tabSaga(tabId) {
return decorateSaga(getTabSubstore(tabId))(mySubapplicationSaga);
} For It should be reasonably easy to define useful shorthands in terms of these primitives. For instance, a substore could be defined using a namespace with a simple factory function: const createNamespacedSubstore = (namespace, mapState) => ({
mapState,
mapAction: action => ({
...action,
type: `${namespace}/${action.type}`,
}),
unmapAction: action => ({
...action,
type: action.type.replace(/^\w+\//, ''),
}),
filterAction: action => ({
...action,
type: action.startsWith(`${namespace}/`),
}),
}); My implementation of this approach is pretty rough, but it seems to be working well enough (including sagas). Specific thoughts:
|
Closing due to inactivity. Happy to repoen if there is more discussion to be had. |
Suppose that I have some original React/Redux app that already works. Then, I need to modify the application so that I can run many "instances" on the same page.
This package seems nearly ideally suited to that purpose -- but the namespacing seems designed for a fixed set of heterogeneous sub-applications with different action types rather than a homogeneous dynamic list of sub-applications sharing the same action types. In particular, redux-subspace accomplishes namespacing by changing the name of the action, whereas I would like to instead either "stuff" extra properties into the payload or (perhaps more cleanly) add an extra "context" property alongside the type and payload.
Is there a sensible way to do something like that within the context of react-subspace? If not, would it be reasonable to extend react-subspace to accommodate that use case, or does that fall outside the scope of what this package ought to do?
(I wish that there were a stable-ish "future" version of
react-redux
; that would make the whole thing easy!)The text was updated successfully, but these errors were encountered: