Hart Redux provides a set of modules that each aim to simplify and accelerate Redux development. These modules work equally well independently or mixed and matched. The philosophy behind these modules is to maintain flexibility and interoperability. If your project requires more and more custom code, you can remove/replace every Hart Redux component gracefully; as such, these modules are excellent for jump-starting your development without committment.
npm install @hart/hart-redux
yarn add @hart/hart-redux
The ActionTypes module generates a nested map of action type strings for your Action Creators and Reducers to reference. The map consists of top-level "operations" with sub-maps of each of the "actions".
import { ActionTypes } from '@hart/hart-redux';
//pass in a unique model name
const actionTypes = ActionTypes("Foo");
{
CREATE: {
REQUEST: 'FOO_CREATE_REQUEST',
SUCCESS: 'FOO_CREATE_SUCCESS',
ERROR: 'FOO_CREATE_ERROR'
},
READ: {
REQUEST: 'FOO_READ_REQUEST',
SUCCESS: 'FOO_READ_SUCCESS',
ERROR: 'FOO_READ_ERROR',
}
...
}
//Now you can access or match your structured set of actions
const action = {
type: actionTypes.CREATE.REQUEST,
payload: ...
}
switch(action.type){
case actionTypes.CREATE.REQUEST:
...
case actionTypes.CREATE.SUCCESS:
...
case actionTypes.CREATE.ERROR:
...
default:
...
}
Action types strings are created with the following pattern: MODELNAME_OPERATION_ACTION
The module, by default, generates using the values ["CREATE", "READ", "UPDATE", "DELETE"] for operation names and ["REQUEST", "SUCCESS", "ERROR"] for action names. These values can be overriden.
import ActionTypes, { Defaults } from '@hart/hart-redux/ActionTypes';
const actionTypes = ActionTypes("Foo", [...Defaults.operations, "UPSERT"], ["REQUEST", "SUCCESS", "SORTA_WORKED"]);
{
CREATE: {
REQUEST: 'FOO_CREATE_SUCCESS',
SUCCESS: 'FOO_CREATE_SUCCESS',
SORTA_WORKED: 'FOO_CREATE_SORTA_WORKED',
},
...
UPSERT: {
REQUEST: 'FOO_UPSERT_REQUEST',
SUCCESS: 'FOO_UPSERT_SUCCESS',
SORTA_WORKED: 'FOO_UPSERT_SORTA_WORKED',
}
}
If you intend to use this module with the Normalized module, it is recommended to append to the defaults rather than remove/replace. Normalized expects the default structure and while you may remap the defaults in Normalized, it may not yield intended results.
Default operations that correspond to Operation/Action combinations that Normalized Reducers/Selectors expect.
Operation that attempts to create an object with a resulting id.
Operation that attempts to retrieve an object with a specific id.
Operation that attempts to update an object with a specific id.
Operation that attempts to delete an object with a specific id.
Default actions that correspond to Operation/Action combinations that Normalized Reducers/Selectors expect.
Action that attempts an Operation that will return a result that subsequently would be used for a SUCCESS or ERROR action.
Action to dispatch the result of REQUEST action's result.
Action to dispatch when a REQUEST action fails.
The Normalized module aims to simplify the common use-case of handling collections of objects with ids in your redux store.
Normalized generates Reducers and Selectors for common (CRUD) operations. It does so using a map of ActionTypes. The ActionTypes module generates all of the neccesary ActionTypes and by pairing the two modules you can focus on writing the logic of your Action creators. The Normalized module is flexible and allows for renaming for reducers/selectors as well as working along side custom reducers.
const actionTypes = {
CREATE: {
REQUEST: 'FOO_CREATE_REQUEST',
SUCCESS: 'FOO_CREATE_SUCCESS',
ERROR: 'FOO_CREATE_ERROR'
},
READ: {
REQUEST: 'FOO_READ_REQUEST',
SUCCESS: 'FOO_READ_SUCCESS',
ERROR: 'FOO_READ_ERROR',
}
...
}
const normalized = Normalized(actionTypes);
The resulting object has reducers and selectors mapped to the actionTypes you supplied. With this result you can create a fully functioning Redux store!
const normalized = {
reducers: {
allIds,
byIds,
areObjectsLoading,
hasObjects,
objectCreating,
objectUpdating,
objectDeleting,
pageIds,
pages
},
selectors: {
getObjectIds,
getObjects,
getObjectById,
getObjectsByIds,
areObjectsLoading,
hasObjects,
isObjectCreating,
isObjectUpdating,
isObjectDeleting,
getObjectIdsByPage,
getPageMetadata
}
}
Reducer that stores an array of object ids. Uses READ, CREATE, and DELETE, SUCCESS actions to add and remove ids from the store.
Reducer that stores a map of all of this collection's objects. Uses READ, CREATE, UPDATE, and DELETE, SUCCESS actions to add, update, and remove objects from the store.
Reducer stores the status of READ operations.
Reducers that stores an object monitoring READ operations.
Reducer stores the status of CREATE operations.
Reducer stores the status of UPDATE operations.
Reducer stores the status of DELETE operations.
Reducer that stores arrays of object ids mapped to page ids from READ.SUCCESS results.
Reducer that stores metadata from the last READ.SUCCESS result.
Returns an array of all object ids. Not in any particular order.
Returns an array of all objects in this collection.
Returns a specific object with the specified id.
Returns an array of objects corresponding to the array of ids specified.
Returns true if a READ.REQUEST is still outstanding.
Returns an object { hasObjects, error }.
hasObjects will return true/false if there are objects in this collection AND READ.SUCCESS has returned; otherwise, hasObjects will be null and in the case of READ.ERROR, error will hold the last response's error.
Returns true if a CREATE.REQUEST is still outstanding.
Returns true if a UPDATE.REQUEST is still outstanding.
Returns true if a DELETE.REQUEST is still outstanding.
Returns an array of object ids for a given page index.
Returns an object with metadata from the last paged response of READ.SUCCESS.
{
itemsPerPage: int,
totalItems: int,
totalPages: int
}
If you would like to supply custom Reducer and/or Selector names, Normalized can handle this for you while maintaining all expected functionality.
In this example we selectively rename some Reducers and some Selectors.
All Reducer and Selector names are required when overriding; so, feel free to follow this approach if you intend to only rename/use some of the Reducers/Selectors.
import Normalized, { Defaults } from '@hart/hart-redux/Normalized';
const reducerNames = Object.assign({}, Defaults.reducerNames, {
areObjectsLoading: "areFoosLoading",
hasObjects: "hasFoos",
objectCreating: "fooCreating",
objectUpdating: "fooUpdating",
objectDeleting: "fooDeleting"
});
const selectorNames = Object.assign({}, Defaults.selectorNames, {
getObjectIds: "getFooIds",
getObjects: "getFoos",
getObjectById: "getFooById",
areObjectsLoading: "areFoosLoading",
hasObjects: "hasFoos",
isObjectCreating: "isFooCreating",
isObjectUpdating: "isFooUpdating",
isObjectDeleting: "isFooDeleting",
});
const normalized = Normalized(actionTypes, reducerNames, selectorNames);
const normalized = {
reducers: {
allIds,
byIds,
areFoosLoading,
hasFoos,
fooCreating,
fooUpdating,
fooDeleting,
pageIds,
pages
},
selectors: {
getFooIds,
getFoos,
getFooById,
getObjectsByIds,
areFoosLoading,
hasFoos,
isFooCreating,
isFooUpdating,
isFooDeleting,
getObjectIdsByPage,
getPageMetadata
}
}
The RequestAction module makes it simple to define action creators for long promise based operations. This makes it very simple to wire up an api with conditional logic for running the operation and dispatch actions before the request and once it succeeds or errors.
import { ActionTypes, Normalized, RequestAction } from '@hart/hart-redux';
const ActionTypes = ActionTypes("foos");
const normalized = Normalized(actionTypes);
const { selectors } = normalized;
const api = {
getFoo: fooId => Promise.resolve({
data: [{ id: fooId, name: "Foo!" }]
})
}
const loadFoos = RequestAction({
shouldSkip: (getState, ...params) => selectors.isObjectLoading(getState()),
operation: ActionTypes.READ,
promise: (getState, fooId) => api.getFoo(fooId)
});
//Hart-Redux will load the foo(s) into state if you loaded normalized.reducers into your Redux Store
//selectors.getObjectById(state, fooId) === { id: fooId, name: "Foo!" }
import { RequestAction } from '@hart/hart-redux';
const ActionTypes = {
READ: {
REQUEST: 'FOO_READ_REQUEST',
SUCCESS: 'FOO_READ_SUCCESS',
ERROR: 'FOO_READ_ERROR',
}
}
const Selectors = {
shouldLoad: (state, ...params) => { ... }
}
const api = {
getFoo: fooId => Promise.resolve({
data: [{ id: fooId, name: "Foo!" }]
})
}
const loadFoos = RequestAction({
shouldSkip: (getState, ...params) => Selectors.shouldLoad(getState(), ...params),
operation: ActionTypes.READ,
promise: (getState, fooId) => api.returnFoos(fooId)
});
await loadFoos(1234);
//State will reflect the outcome of ActionTypes.READ.SUCCESS with the {data} returned from the api.getFoo action
The Namespace module applies a namespace to your Selectors so that you can easily organize your top-level modules into your Redux Store without each module having knowledge of the overall store's structure.
import { Namespace } from '@hart/hart-redux';
or
import { applyNamespace, applyNamespaceToAll } from '@hart/hart-redux/Namespace';
const state = {
person: {
name: "Tester McTesterson",
age: 30
}
}
const getName = state => state.name;
const getAge = state => state.age;
const getPersonName = applyNamespace("person", getName);
const selectors = applyNamespaceToAll("person", {getName, getAge});
const name = getPersonName(state); // "Tester McTesterson"
const age = selectors.getAge(state); // 30
The ModuleConfig module presents a builder pattern for assembling your Reducers, Selectors, ActionTypes, and Action Creators into a single packaged module using a Namespace. This can optionally also remap your Action Creators with ActionTypes and Selectors to reduce interdepedency between the sources that create Action Creators, Reducers, and Selectors.
This example returns a function that accepts a Namespace before creating the Redux module. This pattern is useful to keep organization of the store out of the individual redux modules.
import { ModuleConfig } from '@hart/hart-redux';
export default function(namespace){
return new ModuleConfig(namespace)
.reducers(reducers)
.selectors(selectors)
.actionTypes(actionTypes)
.actions(actions)
.module();
}
import { ModuleConfig, ActionTypes, Normalized, RequestAction } from '@hart/hart-redux';
const actionTypes = ActionTypes("bars");
const normalized = Normalized(actionTypes);
const actions = {
loadBar: (ActionTypes, Selectors) => RequestAction({
shouldSkip: (getState, ...params) => Selectors.isObjectLoading(getState()),
operation: ActionTypes.READ,
promise: (getState, bar) => Promise.resolve({data: bar})
})
}
export default function(namespace){
return new ModuleConfig(namespace)
.reducers(normalized.reducers)
.selectors(normalized.selectors)
.actionTypes(actionTypes)
.buildActions(actions)
.module();
}
Each function, except module(), returns the ModuleConfig object for chaining.
Accepts a map of selector functions, maps each to the Namespace, and finally adds each into the ModuleConfig.
Can be called multiple times and successive calls will add new keys and replace same-keyed values.
Accepts a single reducer function. Use when you have a single or combined reducer.
Successive calls will overwrite the reducer assigned to the ModuleConfig.
If both reducer() and reducers() are called, the reducer() call will prevail.
Accepts a map of reducer functions and adds each into the ModuleConfig.
Can be called multiple times and successive calls will add new keys and replace same-keyed values.
If both reducer() and reducers() are called, the reducer() call will prevail.
Accepts a map of ActionTypes and adds each into the ModuleConfig.
Can be called multiple times and successive calls will add new keys and replace same-keyed values.
Accepts a map of Action/Creator functions and adds each into the ModuleConfig.
Can be called multiple times and successive calls will add new keys and replace same-keyed values.
Accepts a map of Action/Creator functions, maps each expecting the signature below, and finally adds each resulting function into the ModuleConfig.
Can be called multiple times and successive calls will add new keys and replace same-keyed values.
(ActionTypes, Selectors) => fn
Returns the assembled Redux module.
MakeStore is a utility function to combine initialState, middleware, enhancers, and a reducer into a Redux store.
import { combineReducers } from 'redux';
import { MakeStore } from '@hart/hart-redux';
const store = MakeStore({
reducer: combineReducers({ reducer1, reducer2 }),
enhancers: [ enhancer1, enhancer2 ],
middleware: [ middlewareFn1, middleWareFn2 ],
initialState: {}
});
The StoreConfig module presents a builder pattern for assembling a Redux store. When combined with modules made with ModuleConfig, a Redux store can be created in a very readable and terse manner.
import { StoreConfig } from '@hart/hart-redux';
const storeConfig = new StoreConfig({ initialState: {} });
import thunkMiddleware from 'redux-thunk';
import Foos from './Foos'; //Module assembled using ModuleConfig
storeConfig
.addEnhancer(enhancerFunction)
.addMiddleware(thunkMiddleware)
.addModule(Foos("Foos")); //Passing "Foos" as the Namespace for the Foos modules
export const store = storeConfig.getStore();
The StoreConfig constructor accepts an object with the following options.
Map of reducer functions keyed by their Namespace.
Array of enhancer functions.
Array of middleware functions
Object to represent the initial state of the Redux store.
Override the default 'redux' combineReducers function when using getStore().
Function to call for each instance of addModule called.
(namespace, module) => {}
Each function, except get() and getStore(), returns the StoreConfig object for chaining.
Adds a reducer using namespace.
Combines and adds the resulting combined reducer using namespace.
Adds an entire Redux module to this store.
A Redux module is defined as an object following this pattern.
Modules created with ModuleConfig follow this pattern.
const module = {
reducer: fn, //reducer or reducers will be added using the module's namespace.
reducers: {}, // passing both reducer and reducers will result in an Error.
namespace: "", //required
... any additional properties //optional
}
Adds an enhancer function into the config.
Adds a middleware function into config.
get() returns a copy of the config object if you intend to create a Redux store without getStore().
getStore() creates and returns a new Redux Store using the MakeStore utility. This will use 'combineReducers' from the 'redux' library to combine reducers added to this config. If does not fit your use case, you can instead use get() to get the final configuration; or pass in _combineReducers in the StoreConfig constructor.