feature-redux is your feature-u integration point to redux!
It promotes the reducerAspect
(a feature-u plugin) that
facilitates redux integration to your features.
Backdrop:
feature-u is a utility that facilitates feature-based project organization for your react project. It helps organize your project by individual features. feature-u is extendable. It operates under an open plugin architecture where Aspects integrate feature-u to other framework/utilities that match your specific run-time stack.
Overview:
feature-redux configures redux through the reducerAspect
(which is supplied to feature-u's launchApp()
). This
extends feature-u's Feature
object by adding support for the
Feature.reducer
property, referencing feature-based reducers.
A feature-based reducer is simply a normal reducer that manages the
feature's slice of the the overall appState. The only thing different
is it must be embellished with slicedReducer()
, which provides
instructions on where to insert it in the overall top-level appState.
Only reducers are of interest to feature-redux because that is what is needed to configure redux. All other redux items (actions, selectors, etc.) are internal implementation details of the feature.
It is important to understand that you continue to use redux the same way you always have. It's just that now you are dealing with a smaller context ... within the boundaries of your feature!
As an aside, because feature-redux manages redux, it also provides an integration point to other Aspects that need to inject their redux middleware.
Let's see how this all works together ...
-
peerDependencies ... you should already have these, because this is our integration point (but just in case):
npm install --save feature-u npm install --save react npm install --save redux npm install --save react-redux
-
the main event:
npm install --save feature-redux
-
Within your mainline, register the feature-redux
reducerAspect
to feature-u'slaunchApp()
(see:**1**
below).src/app.js
import {launchApp} from 'feature-u'; import {reducerAspect} from 'feature-redux'; // **1** import features from './feature'; export default launchApp({ aspects: [ reducerAspect, // **1** ... other Aspects here ], features, registerRootAppElm(rootAppElm) { ReactDOM.render(rootAppElm, getElementById('myAppRoot')); } });
-
Within each feature that maintains state, simply register the feature's reducer through the
Feature.reducer
property (using feature-u'screateFeature()
) ... see:**2**
below.Because the state of each feature is combined into one overall appState, the feature reducer must identify it's root location, through the
slicedReducer()
function. This slice can optionally reference a federated namespace corresponding to the desired target shape.src/feature/myXyzFeature/index.js
import {createFeature} from 'feature-u'; import {slicedReducer} from 'feature-redux'; import myXyzFeatureReducer from './state'; export default createFeature({ name: 'myXyzFeature', reducer: slicedReducer('my.feature.state.location', myXyzFeatureReducer), // **2** ... snip snip (other aspect properties here) });
Well that was easy!! At this point redux is completely setup
for your application! The redux store has been created and is
accessible through the standard redux connect()
function.
In the nutshell, that's a highlight of most everything you need to know to use feature-redux! Go forth and compute!
feature-redux automatically configures redux by creating the store
(accumulating feature reducers across your app), and making the
store available through the standard redux connect()
function,
(by injecting the standard redux <Provider>
component at the root
of your app).
It is important to understand that your interface to redux has not changed in any way. In other words, you continue to use redux the same way you always have. It's just that now you are dealing with a smaller context ... within the boundaries of your feature!
With that said, as always, you should strive to keep each feature isolated, so it is truly plug-and-play. Working with redux, involves interacting with actions, reducers, and selectors. Let's take a closer look at some Feature-Based Best Practices in regard to redux usage.
Within the redux framework, actions are the basic building blocks that facilitate application activity.
-
Actions follow a pre-defined convention that promote an action type and a type-specific payload.
-
Actions are dispatched throughout the system (both UI components, and logic modules).
-
Actions are monitored by reducers (which in turn change state), and trigger UI changes.
-
Actions can also be monitored by logic modules (when using redux-logic), that implement a variety of app-level logic ... things like asynchronously gathering server resources, and even dispatching other actions.
In general, actions are considered to be an internal detail of the feature, and therefore are NOT managed by feature-u. In other words, each feature will define and use it's own set of actions.
This allows you to manage your actions however you wish. Best practices prescribe that actions be created by action creators (functions that return actions). It is common to manage your action creators and action types through a library like action-u or redux-actions.
There are a small number of cases where feature-based actions may need to be promoted outside of a feature's boundary. Say, for example, featureA's reducer needs to monitor one of featureB's actions, or one of featureB's logic modules needs to dispatch a featureA action.
When this happens the publicFace feature-u aspect can be used for this promotion.
Please note that in consideration of feature encapsulation, best practices would strive to minimize the public promotion of actions outside the feature boundary.
featureA.action1
featureA.action2
One characteristic that actions must adhere to is: action types must be unique across the entire app, because they are interpreted at an app-level scope.
This uniqueness is the responsibility of your implementation, because feature-u is not involved. This may simply naturally happen in your implementation.
If however, you wish to systematically insure this uniqueness, the simplest thing to do is to prefix all your action types with the feature name (feature-u guarantees all feature names are unique). This has the added benefit of readily associating dispatched action flows to the feature they belong to.
Note: Ideally you could use the Feature.name
as the
single-source-of-truth, however importing feature from your action
modules is problematic (the Feature
object will most likely not be
fully resolved during in-line code expansion). As a result, a best
practice is to import a separate featureName
constant (that
simply holds the name). Here is an example using action-u (see:
**3**
below):
src/feature/featureA/featureName.js
export default 'featureA'; // **3**
src/feature/featureA/actions.js
import {generateActions} from 'action-u';
import featureName from './featureName';
export default generateActions.root({
[featureName]: { // **3** prefix all actions with our feature name, guaranteeing uniqueness app-wide!
action1: { // actions.action1(): Action
actionMeta: {},
},
action2: { // actions.action2(): Action
actionMeta: {},
},
etc...
},
});
This example results in actions of type:
Within the redux framework, reducers monitor actions, changing appState, which in turn triggers UI changes.
Each feature (that maintains state), defines it's own reducer, maintaining it's slice of the overall appState.
While these reducers are truly opaque assets (internal to the feature), they are of interest to feature-redux to the extent that they are needed to configure redux.
Each feature that maintains state, simply registers it's reducer
through the Feature.reducer
property (using feature-u's
createFeature()
).
Because reducers may require access to feature-u's App
object
during code expansion, this property can also be a feature-u
managedExpansion()
callback (a function that returns the reducer)
... please refer to feature-u's discussion of Managed Code
Expansion.
Because feature-redux must combine the reducers from all features
into one overall appState, it requires that each reducer be
embellished through the slicedReducer()
function.
This merely injects a slice property on the reducer function (interpreted by feature-redux), specifying the location of the reducer within the top-level appState tree.
This slice can optionally reference a federated namespace corresponding to the desired target shape.
As an example, consider the following definition (see: **4**
):
const currentView = createFeature({
name: 'currentView',
reducer: slicedReducer('view.currentView', currentViewReducer), // **4**
...
});
const fooBar = createFeature({
name: 'fooBar',
reducer: slicedReducer('view.fooBar', fooBarReducer), // **4**
...
});
Which yields the following overall appState:
appState: {
view: {
currentView {
... state managed by currentViewReducer
},
fooBar: {
... state managed by fooBarReducer
},
},
}
You are free to use any root location for your feature's state (i.e. the slice). In many cases, this is dictated by the overall state shape.
However, depending on the context, it is often useful to use the
featureName
somewhere within this definition. This best
practice has the added benefit of readily associating each slice of
state to the feature they belong to.
We can refine the example above, by programmatically injecting the
featureName
. This yields the same result as before, but benefits
from the single-source-of-truth principle.
import featureName from './featureName';
import reducer from './reducer';
const currentView = createFeature({
name: featureName,
reducer: slicedReducer(`view.${featureName}`, reducer),
...
});
Selectors are a redux best practice which encapsulates both the state shape location and the business logic interpretation of that state.
Selectors should be used to encapsulate all your state. Most selectors are promoted/used internally within the feature (defined in close proximity to your reducers).
While feature-redux does not directly manage anything about selectors, a feature may wish to promote some of it's selectors using the publicFace feature-u aspect.
Please note that in consideration of feature encapsulation, best practices would strive to minimize the public promotion of feature state (and selectors) outside the feature boundary.
Another benefit of slicedReducer()
is that not only does it
embellish the reducer with a slice
property (interpreted by
feature-redux), it also injects a selector that returns the
slicedState root, given the appState:
reducer.getSlicedState(appState): slicedState
In our case the slicedState is one in the same as the featureState, so as a best practice it can be used in all your selectors to further encapsulate this detail (employing another single-source-of-truth principle).
Here is an example (see: **5**
and **6**
):
// **5** DEFINITION
/** Our feature state root (a single-source-of-truth) */
const getFeatureState = (appState) => reducer.getSlicedState(appState);
// **6** USAGE
/** Is device ready to run app */
export const isDeviceReady = (appState) => getFeatureState(appState).status === 'READY';
... more selectors
The primary accomplishment of feature-redux is the creation (and configuration) of the redux store. The Aspect Interface to this process (i.e. the inputs and outputs) are documented here.
-
Primary Input:
The primary input to feature-redux is the set of reducers that make up the overall app reducer. This is specified by each of your features (that maintain state) through the
Feature.reducer
property, containing aslicedReducer()
that manages the state of that corresponding feature. -
Middleware Integration:
Because feature-redux manages redux, other Aspects can promote their redux middleware through feature-redux's
Aspect.getReduxMiddleware()
API (an "aspect cross-communication mechanism"). As an example, the feature-redux-logic Aspect integrates redux-logic.
-
Primary Output:
The primary way in which feature-redux exposes redux to your app is by injecting the standard redux
<Provider>
component at the root of your application DOM. This enables app component access to the redux store (along with it'sdispatch()
andgetState()
) through the standard reduxconnect()
function. -
Middleware Features:
Because feature-redux allows other aspects to inject their redux middleware, whatever that middleware exposes is made available. As an example, the feature-redux-logic Aspect injects redux-logic.
-
Other:
For good measure, feature-redux promotes the redux store through the
Aspect.getReduxStore()
method. This provides direct access to the store to any external process that needs it.
When feature-redux detects that no reducers have been specified by any of your features, it will (by default) throw the following exception:
***ERROR*** feature-redux found NO reducers within your features
... did you forget to register Feature.reducer aspects in your features?
(please refer to the feature-redux docs to see how to override this behavior).
Most likely this should in fact be considered an error (for example you neglected to specify the reducer aspects within your features). The reasoning is: why would you not specify any reducers if your using redux?
You can change this behavior through the following configuration:
reducerAspect.config.allowNoReducers$ = true;
With this option enabled, when no reducers are found, redux will be configured with an identity reducer (accompanied with a WARNING logging probe).
You can also specify your own reducer function in place of the true
value, which will be used ONLY in the scenario where no reducers were
specified by your features.
-
Within your mainline, register the feature-redux
reducerAspect
to feature-u'slaunchApp()
. -
Within each feature that maintains state, simply register the feature's reducer through the
Feature.reducer
property (using feature-u'screateFeature()
).Because the state of each feature is combined into one overall appState, the feature reducer must identify it's root location, through the
slicedReducer()
function. This slice can optionally reference a federated namespace corresponding to the desired target shape.
The reducerAspect
is the feature-u plugin that facilitates
redux integration to your features.
To use this aspect:
Please refer to the Usage section for examples of this process.
-
slice: string
The location of the managed state within the overall top-level appState tree. This can be a federated namespace (delimited by dots). Example:
'views.currentView'
-
reducer: reducerFn
The redux reducer function to be embellished with the slice specification.
API: slicedReducer(slice, reducer): reducer
Embellish the supplied reducer with a slice property - a specification (interpreted by feature-redux) as to the location of the reducer within the top-level appState tree.
Please refer to the Sliced Reducers section for a complete description with examples.
Note: slicedReducer()
should always wrap the the outer
function passed to createFeature()
, even when managedExpansion()
is used. This gives your app code access to the embellished
getSlicedState()
selector, even prior to expansion occurring (used
as a single-source-of-truth in your selector definitions).
Parameters:
Return: reducerFn
the supplied reducer, embellished with both the slice and a convenience selector:
reducer.slice: slice
reducer.getSlicedState(appState): slicedState