3.0.0 • Published 4 years ago

feature-redux v3.0.0

Weekly downloads
39
License
MIT
Repository
github
Last release
4 years ago

feature-redux (feature-u redux integration)

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.

Build Status Codacy Badge Codacy Badge Known Vulnerabilities NPM Version Badge

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

At a Glance

Install

  • 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

SideBar: Depending on how current your target browser is (i.e. it's JavaScript engine), you may need to polyfill your app (please refer to Potential Need for Polyfills).

Usage

  1. Within your mainline, register the feature-redux reducerAspect to feature-u's launchApp() (see: **1** below).

    src/app.js

    import {launchApp}           from 'feature-u';
    import {createReducerAspect} from 'feature-redux'; // **1**
    import features              from './feature';
    
    export default launchApp({
    
      aspects: [
        createReducerAspect(),                         // **1**
        ... other Aspects here
      ],
    
      features,
    
      registerRootAppElm(rootAppElm) {
        ReactDOM.render(rootAppElm,
                        getElementById('myAppRoot'));
      }
    });
  2. Within each feature that maintains state, simply register the feature's reducer through the Feature.reducer property (using feature-u's createFeature()) ... 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() HoC, or the newer Hooks API.

In the nutshell, that's a highlight of most everything you need to know to use feature-redux! Go forth and compute!

A Closer Look

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() HoC (or the newer Hooks API), (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.

Actions

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.

Public 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 feature-u fassets 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.

Action Uniqueness (Single Source of Truth)

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:

src/feature/timer/featureName.js

export default 'timer';

src/feature/timer/actions.js

import featureName  from './featureName';

// action type constants
export const TIMER_START  = `${featureName}.TIMER_START`;
export const TIMER_CANCEL = `${featureName}.TIMER_CANCEL`;
export const TIMER_RESET  = `${featureName}.TIMER_RESET`;
export const TIMER_END    = `${featureName}.TIMER_END`;

... snip snip

Reducers (state)

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 Fassets object during code expansion, this property can also be a feature-u expandWithFassets() callback (a function that returns the reducer) ... please refer to feature-u's discussion of Managed Code Expansion.

Sliced Reducers

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
    },
  },
}

State Root (Single Source of Truth)

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 (encapsulating state)

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

Public Selectors

While feature-redux does not directly manage anything about selectors, a feature may wish to promote some of it's selectors using the feature-u fassets 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.

Feature State Location (Single Source of Truth)

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

Interface Points

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.

Inputs

  1. 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 a slicedReducer() that manages the state of that corresponding feature.

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

  3. Enhancer Integration:

    Because feature-redux manages redux, other Aspects can promote their redux enhancer through feature-redux's Aspect.getReduxEnhancer() API (an "aspect cross-communication mechanism").

Exposure

  1. 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's dispatch() and getState()) through the standard redux connect() HoC, or the newer Hooks API.

  2. Application Life Cycle Hooks - namedParams:

    feature-redux promotes the redux getState/dispatch functions as namedParams of feature-u's Application Life Cycle Hooks.

  3. Redux Middleware:

    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.

  4. Redux Enhancer:

    Because feature-redux allows other aspects to inject their redux enhancer, whatever that enhancer exposes is made available.

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

    • Integration with Redux DevTools is automatically configured (when detected).

Error Conditions

  • NO REDUCERS:

    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 allowNoReducers parameter:

    createReducerAspect({allowNoReducers: true})
    • when true, and no reducers are found, redux will be configured with an identity reducer (accompanied with a WARNING logging probe).

    • you can also specify your own app-wide reducer function in place of the true value, which will be used ONLY in the scenario where no reducers were specified by your features.

API

reducerAspect: Aspect

API: createReducerAspect({name:'reducer',
                          initialState:undefined,
                          allowNoReducers:false}): reducerAspect

The reducerAspect is the feature-u plugin that facilitates redux integration to your features.

PARAMS: (Please Note: only named parameters are used)

  • name: The name of this reducer (defaults to 'reducer')

  • initialState: An optional pre-loaded state that will become the initial state of your store.

    Typically this is not needed, because your reducers will define the initial state. However, it can be useful to hydrate the state from the server (in universal apps), or to restore a previously serialized user session.

    When supplied, any validation/errors will be detected by redux, as it is passed directly to it's createStore().

  • allowNoReducers: A boolean -or- app-wide reducer function, that specifies what to do when no reducers are found in any or your features (defaults to false which throws an error) ... see: NO REDUCERS section of Error Conditions

USAGE:

  • Within your mainline, register the feature-redux reducerAspect to feature-u's launchApp().

  • Within each feature that maintains state, simply register the feature's reducer through the Feature.reducer property (using feature-u's createFeature()).

    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.

Please refer to the Usage section for examples of this process.

slicedReducer()

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 expandWithFassets() 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:

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

Return: reducerFn

the supplied reducer, embellished with both the slice and a convenience selector:

reducer.slice: slice
reducer.getSlicedState(appState): slicedState

Potential Need for Polyfills

The implementation of this library employs modern es2015+ JavaScript constructs. Even though the library distribution is transpiled to es5 (the least common denominator), polyfills may be required if you are using an antiquated JavaScript engine (such as the IE browser).

We take the approach that polyfills are the responsibility of the client app. This is a legitimate approach, as specified by the W3C Polyfill Findings (specifically Advice for library authors).

  • polyfills should only be introduced one time (during code expansion of the app)
  • a library should not pollute the global name space (by including polyfills at the library level)
  • a library should not needlessly increase it's bundle size (by including polyfills that are unneeded in a majority of target environments)

As it turns out, app-level polyfills are not hard to implement, with the advent of third-party utilities, such as babel:

If your target JavaScript engine is inadequate, it will generate native run-time errors, and you will need to address the polyfills. Unfortunately, in many cases these errors can be very obscure (even to seasoned developers). The following Babel Feature Request (if/when implemented) is intended to address this issue.