Store
The Store package is a kernel that exposes an API using the storeHelper to manage the Redux's state.
Store Helper
The storeHelper is responsible for the application state management. It's a wrapper around Redux, which exposes the required methods to handle the state of the application.
You should be familiar with the Redux documentation.
Method |
Description |
---|---|
getState |
Obtains the Redux store state |
registerReducer |
Registers a plugin reducer |
dispatch |
Dispatches an action |
handleAction |
Handles an action |
handleReducedAction |
Handles an action after the reducer |
Usage
The store helper is a wrapper that enables Redux capacities. It uses a Redux store under the hood. Redux philosophy consists of three key concepts:
-
The state
-
The reducer
-
The actions
getState
The state is an object that contains all the application state. It is the single source of truth.
Use getState to obtain the state.
storeHelper.getState(): State

getState returns the root state.
StatePlugin.js
export default class StatePlugin {
constructor({ storeHelper }) {
this._storeHelper = storeHelper;
}
getState() {
return this._storeHelper.getState();
}
}
spec.js
import { addPlugin, useKernels } from '@orion/core/test-utils';
import { coreKernel } from '@orion/core';
import StatePlugin from './StatePlugin.js';
import pkg from './package.json';
test('get state returns the root state', () => {
useKernels(coreKernel);
const statePlugin = addPlugin(pkg, StatePlugin);
const state = statePlugin.getState();
expect(state).toMatchObject({ plugins: {} });
});
package.json
{
"name": "state-plugin",
"version": "1.0.0"
}
registerReducer
By default, the state for plugins is empty. In Redux, reducers are functions that update the state. These reducers receive the old state, and they have to compute a new state. Before writing a reducer, you must understand that they are:
-
They are pure functions.
-
They cannot modify the state; they make a copy instead.
-
They cannot modify any global variable.
-
It returns the state.
Redux uses reducers to execute actions and notify views of the new values. It automatically decides when to call reducers.
Register your plugin reducers with registerReducer. The first thing that you have to do with a reducer is set up the initial state. The typical form to set it up is by using the default argument value syntax.

registerReducer adds a new reducer to the Redux store.
CounterPlugin.js
export default class CounterPlugin {
constructor({ storeHelper }) {
storeHelper.registerReducer('counter', this._countReducer);
}
_countReducer(state = 0) {
return state;
}
}
spec.js
import { addPlugin, getHelper, useKernels } from '@orion/core/test-utils';
import { coreKernel } from '@orion/core';
import CounterPlugin from './CounterPlugin.js';
import pkg from './package.json';
test('register reducer adds a new reducer to the redux store', () => {
useKernels(coreKernel);
const counterPlugin = addPlugin(pkg, CounterPlugin);
const state = getHelper(counterPlugin, 'storeHelper').getState();
expect(state).toMatchObject({ plugins: { counter: 0 } });
});
package.json
{
"name": "counter-plugin",
"version": "1.0.0"
}
dispatch
The Redux store executes all actions in the system. In Redux, actions are JSON objects that have at least one property called type, which is a string. Actions may have more related properties for that action type.
{
"type": "counter/INCREMENT",
"amount": 1
}
Use dispatch to request redux to execute one of these actions.

The dispatcher executes a JSON action with type in the reducer.
IncrementPlugin.js
export default class IncrementPlugin {
constructor({ storeHelper }) {
this._storeHelper = storeHelper;
}
dispatchIncrement(amount) {
this._storeHelper.dispatch({
type: 'counter-plugin/INCREMENT',
amount,
});
}
}
CounterPlugin.js
export default class CounterPlugin {
constructor({ storeHelper }) {
storeHelper.registerReducer('counter', this._countReducer);
}
_countReducer(state = 0, action) {
switch (action.type) {
case 'counter-plugin/INCREMENT': {
const { amount } = action;
const nextState = state + amount;
return nextState;
}
default: {
return state;
}
}
}
}
spec.js
import { addPlugin, getHelper, useKernels } from '@orion/core/test-utils';
import { coreKernel } from '@orion/core';
import CounterPlugin from './CounterPlugin.js';
import IncrementPlugin from './IncrementPlugin.js';
import counterPkg from './counter.json';
import incrementPkg from './increment.json';
import { testMessage } from './testHelper.js';
test(testMessage, () => {
useKernels(coreKernel);
const counterPlugin = addPlugin(counterPkg, CounterPlugin);
const incrementPlugin = addPlugin(incrementPkg, IncrementPlugin);
incrementPlugin.dispatchIncrement(3);
const state = getHelper(counterPlugin, 'storeHelper').getState();
expect(state).toMatchObject({ plugins: { counter: 3 } });
});
increment.json
{
"name": "increment-plugin",
"version": "1.0.0",
"pluginDependencies": {
"counter-plugin": "^1.0.0"
},
"orion": {
"storeHelper": {
"actionTypes": {
"consumes": {
"counter-plugin": ["counter-plugin/INCREMENT"]
}
}
}
}
}
counter.json
{
"name": "counter-plugin",
"version": "1.0.0",
"orion": {
"storeHelper": {
"actionTypes": {
"produces": ["counter-plugin/INCREMENT"]
}
}
}
}
testHelper.js
export const testMessage = 'the dispatcher executes a JSON action with type in the reducer';
Reducers compute dispatched actions. Besides the state, reducers receive the current action as a second argument. By convention, the reducer starts with a switch statement that looks for the value of the action type. This switch always has a default case that returns the state without modification; it is because the reducer must ignore unknown actions. If the action type is relevant, it adds a case statement in which it computes the next state.
handleAction
Adds a listener of actions, which triggers a callback (handle) when a condition (test) is satisfied.
Different plugins can be handling the same action, so we use chain of responsibility pattern with a priority. The plugins with a lower priority will be handled first.
Usage
handleAction(test: String, handle: HandleFunction, position?: ActionHandlerPosition): void
-
test String
Condition to determine if the action should be handled or not.
-
handle HandleFunction
The callback function which will handle the action.
-
position ActionHandlerPosition [OPTIONAL]
The object with the position of the action to be handled.
-
HandleFunction: ({action, replaceAction, stopPropagation, preventDefault, done }) => Promise?
Function that handles an action and returns a promise if it must wait to the completion of a promise. Look examples for usage.
-
ActionHandlerPosition: { name: string, before: string, after: string }
Object that defines the position of the action to be handled in relation to an existing one.
Returns
-
Function
A function to stop handling the action.
Warning: Using handleAction with a function as test argument can negatively affect the performance of the application, if not used carefully.
The recommendation is to avoid its use unless there is no alternative. Only use it when the action to be handled does not depend on a type which can be matched by a String or a RegExp.
For example, instead of:
handleAction(
(action) => action.type === 'INCREMENT' && action.count > 2,
() => { ... },
)
You can use:
handleAction(
'INCREMENT',
(action) => {
if (action.count > 2) { ... }
},
)
When handling an action the handled action is retrieved:
storeHelper.handleAction('INCREMENT', ({ action }) => {
console.log('Log: ', action), { name: 'incrementAction' };
});

handlerAction listener triggers when condition is meet.
IncrementPlugin.js
export default class IncrementPlugin {
constructor({ storeHelper }) {
this._storeHelper = storeHelper;
}
dispatchIncrement() {
this._storeHelper.dispatch({
type: 'counter-plugin/INCREMENT',
});
}
}
CounterPlugin.js
export default class CounterPlugin {
constructor({ storeHelper }) {
this.count = 0;
storeHelper.handleAction('counter-plugin/INCREMENT', this._incrementAction, {
name: 'counter',
});
}
_incrementAction = () => {
this.count++;
};
}
spec.js
import { addPlugin, getHelper, useKernels } from '@orion/core/test-utils';
import { coreKernel } from '@orion/core';
import CounterPlugin from './CounterPlugin.js';
import IncrementPlugin from './IncrementPlugin.js';
import counterPkg from './counter.json';
import incrementPkg from './increment.json';
import { testMessage } from './testHelper.js';
test(testMessage, () => {
useKernels(coreKernel);
const counterPlugin = addPlugin(counterPkg, CounterPlugin);
const incrementPlugin = addPlugin(incrementPkg, IncrementPlugin);
incrementPlugin.dispatchIncrement();
expect(counterPlugin).toHaveProperty('count', 1);
});
increment.json
{
"name": "increment-plugin",
"version": "1.0.0",
"pluginDependencies": {
"counter-plugin": "^1.0.0"
},
"orion": {
"storeHelper": {
"actionTypes": {
"consumes": {
"counter-plugin": ["counter-plugin/INCREMENT"]
}
}
}
}
}
counter.json
{
"name": "counter-plugin",
"version": "1.0.0",
"orion": {
"storeHelper": {
"actionTypes": {
"produces": ["counter-plugin/INCREMENT"]
},
"actionHandlers": {
"produces": ["counter"]
}
}
}
}
testHelper.js
export const testMessage = 'handlerAction listener triggers when condition is meet';
When handling an action, by default, the handler respects the priority of the plugin, but a priority for the handlers can be specified with the position parameter: package.json
{
"name": "pos-web-plugin-example",
"version": "0.0.1",
"orion": {
"storeHelper": {
"actionHandlers": {
"produces": ["someAction"]
}
}
}
}
storeHelper.handleAction('someAction', ({ action }) => console.log('Hello ', action), {
name: 'someAction',
});
storeHelper.handleAction('anotherAction', ({ action }) => console.log(action, ' goes before'), {
before: 'someAction',
});
// this logs: anotherAction goes before, Hello someAction

handlerAction listener triggers according its priority.
spec.js
import { addPlugin, getHelper, useKernels } from '@orion/core/test-utils';
import { coreKernel } from '@orion/core';
import LoggerPlugin from './LoggerPlugin.js';
import IncrementPlugin from './IncrementPlugin.js';
import loggerPkg from './logger.json';
import incrementPkg from './increment.json';
import { testMessage } from './testHelper.js';
test(testMessage, () => {
useKernels(coreKernel);
const incrementPlugin = addPlugin(incrementPkg, IncrementPlugin);
const loggerPlugin = addPlugin(loggerPkg, LoggerPlugin);
incrementPlugin.dispatchIncrement();
expect(loggerPlugin).toHaveProperty('log', [
'HANDLER-TWO executed',
'HANDLER-ONE executed',
'HANDLER-THREE executed',
]);
});
IncrementPlugin.js
export default class IncrementPlugin {
constructor({ storeHelper }) {
this._storeHelper = storeHelper;
}
dispatchIncrement() {
this._storeHelper.dispatch({
type: 'increment-plugin/INCREMENT',
});
}
}
LoggerPlugin.js
export default class LoggerPlugin {
constructor({ storeHelper }) {
this.log = [];
storeHelper.handleAction(
'increment-plugin/INCREMENT',
this._logAction('HANDLER-ONE executed'),
{
name: 'logger',
},
);
storeHelper.handleAction(
'increment-plugin/INCREMENT',
this._logAction('HANDLER-TWO executed'),
{
before: 'logger',
},
);
storeHelper.handleAction(
'increment-plugin/INCREMENT',
this._logAction('HANDLER-THREE executed'),
{
after: 'logger',
},
);
}
_logAction(message) {
return () => this.log.push(message);
}
}
increment.json
{
"name": "increment-plugin",
"version": "1.0.0",
"orion": {
"storeHelper": {
"actionTypes": {
"produces": ["increment-plugin/INCREMENT"]
}
}
}
}
logger.json
{
"name": "logger-plugin",
"version": "1.0.0",
"pluginDependencies": {
"increment-plugin": "^1.0.0"
},
"orion": {
"storeHelper": {
"actionTypes": {
"consumes": {
"increment-plugin": ["increment-plugin/INCREMENT"]
}
},
"actionHandlers": {
"produces": ["logger"]
}
}
}
}
testHelper.js
export const testMessage = 'handlerAction listener triggers according its priority';
Action handlers can be asynchronous:
storeHelper.handleAction('INCREMENT', async ({ action }) => {
await delay(1000); // Will delay the flow of the action 1 second
});

Replacing an action by a different one (other handlers will receive the modified action):
storeHelper.handleAction('INCREMENT', ({ replaceAction }) => {
replaceAction({ type: 'DIFFERENT-ACTION' });
});
Preventing the action to be handled by others (it will arrive reducers anyway, modifying state):
storeHelper.handleAction('INCREMENT', ({ stopPropagation }) => {
stopPropagation();
});
Preventing the action to be handled by reducers (it will arrive others "handleActions" anyway):
storeHelper.handleAction('INCREMENT', ({ preventDefault }) => {
preventDefault();
});
Preventing the action to be handled by reducers or others:
storeHelper.handleAction('INCREMENT', ({ preventDefault, stopPropagation }) => {
preventDefault();
stopPropagation();
});
Asynchronous action handlers can be resolved when desired:
storeHelper.handleAction('INCREMENT', async ({ done }) => {
done(); // The action will be able to continue its flow
await delay(1000); // As we invoke "done()" before it will NOT delay the flow of the action 1 second
console.log('After 1 second'); // But we can continue doing things without stop the action's flow
});

Stop handling action, for example after handling the action five times.
let count = 0;
const stopHandlingAction = storeHelper.handleAction('INCREMENT', () => {
count += 1;
console.log(`Incremented ${count} times`);
if (count === 5) {
stopHandlingAction();
}
});
handleReducedAction
Use this method to handle an action (due to its type) after the state has been modified. In other words, after the action has passed through the reducers.
Useful to react to the state changes.
These kind of handlers can not be skipped.
Usage
handleReducedAction(test: String, handle: Function): void
-
test String
The type of the action which determines if the action should be handled or not.
-
handle Function
The callback to be invoked when the action is handled.
Returns
-
Function
A function to stop handling the reduced action.

storeHelper.handleReducedAction('MY_ACTION_TYPE', ({ action }) => {
console.log('Action: ', action);
});
If you need the store use withFactory:
storeHelper.handleReducedAction(
'MY_ACTION_TYPE',
withFactory(({ storeHelper }) => ({ action }) => {
console.log('Action: ', action);
console.log('Result State: ', storeHelper.getState());
}),
);
Stop handling a reduced action, for example, after handling the action five times.
let count = 0;
const stopHandlingAction = storeHelper.handleReducedAction('INCREMENT', () => {
count += 1;
console.log(`Incremented has modified the state ${count} times`);
if (count === 5) {
stopHandlingAction();
}
});