How to implement plugin-based architecture using redux

Viewed 1370

I am working on an web app where user can subscribe to available features and all those features are having certain effects on user actions. I am using redux thunk for async actions.

eg. user can select product to buy or sell from product list, this triggers a reducer called userIntention and after that createProductForm reducer is called which displays product form along with product information. User can subscribe to feature like recommendation, best deals, etc. which can modify certain parameters of product properties like tags, price. I want to implement this in such a way that if createProductForm is triggered that reducer will collect data from recommendation, best deals, etc dynamically and based on there results, it'll form user product form.

We want to keep multiple developers to work on different features without conflicting with other's features and codebase. So that when a new feature is developed, we don't have change core logic. But this has to happen without prior knowing which reducers to call for creatingProductForm, it could be none or many feature reducers.

It's like plugin, where subscribe activates some reducers and middlewares.

Which one is best for this case : reducer or middleware ? I've tried adding dynamic middlewares, dynamic reducers but both of them are having their own pitfalls.

What i've tried:

  1. After feature subscription, action called 'ENABLE_FEATURE' is dispatched to which feature reducers can listen register themselves dynamically. But i am finding it difficult to maintain which reducers to call. Is there any way to keep listing reducers in redux store.
  2. same tried with middlewares, but then dynamically removing one of the middlewares is too complicated.
  3. create my own utility outside redux, where registration, subscription of dynamic reducers is implemented, but then how to call reducers from outside react component since dispatch and store is unavailable.

We're using rekit studio for development.

EDIT This is my application architecture. As you can see, all features are developed independent of each other along with reducers, store, components. What we need is once f-core component is developed it's untouched even if we implement new features independent of it. Somewhat like a registry where features can register themselves so f-core comes to know about their existence. Then this features should've exposed some functions ( reducers ) which will be decided at runtime. Application architecture

/node_modules
package.json
package-lock.json
/public
/src
 - /common
     - configStore.js
     - history.js
     - rootReducer.js
     - routeConfig.js
 - /features
    - /f-core
        - component1.js
         - component1.less
         - index.js
         - route.js
         - /api-service
             - router.js
             - constants.js
             - service.js
         - /redux
             - actions.js
             - constants.js
             - reducer.js
             - initialState.js
             - reducer1.js
     - /f1
         - component1.js
         - component1.less
         - index.js
         - route.js
         - /api-service
             - router.js
             - constants.js
             - service.js
         - /redux
             - actions.js
             - constants.js
             - reducer.js
             - initialState.js
             - reducer1.js
     - /f2
        - component1.js
            - component1.less
         - index.js
         - route.js
         - /api-service
             - router.js
             - constants.js
             - service.js
         - /redux
             - actions.js
             - constants.js
             - reducer.js
             - initialState.js
             - reducer1.js
 - index.js
 - Root.js
1 Answers

One way to develop pluggable Redux substates in isolation is 1) write each substate with zero outside dependencies, and then 2) within the main app, use Redux-Saga to orchestrate action flows that cross pluggable substate boundaries.

1) Write each substate with zero dependencies by only referencing functions, variables, etc, within its own namespace/directory/plugin. Meaning:

  • Colocation of all related code such as reducers, action types, action creators, selectors, constants, utils
  • Reducers only handle colocated action types
  • Selectors only pull from colocated state

2) Use Redux-Saga to orchestrate action flows that cross substate boundaries. If reducers from separate plugins need to update state for the same event, have a saga listen to an app/non-plugin action and then dispatch separate plugin-specific actions.

Example:

import { CREATE_ITEM } from '../actions'; // i.e. src/actions
import { moduleA, moduleB } from '../modules';

export function* onCreateItem() {
    yield takeEvery(CREATE_ITEM, function* ({ item }) {
        yield put(moduleA.actionCreators.addNewItem(item));
        yield put(moduleB.actionCreators.setMostRecentlyCreatedItem(item.name));
    })
}

With this pattern, Redux-Saga becomes layer of indirection between application-specific actions and application-agnostic substates so that each to vary independently.

Lastly, structure selectors using the widely accepted delegation approach, which works well with the above pattern.

Considerations:

  • there will be more actions and sagas to keep track of
  • this is my opinionated approach, which I recommend based on experience with using it to scale redux apps
Related