Reducers in Flambeau are differentiated from the standard Redux reducer by a number of modest features. One of these is the addition of props for reducers.
These props allow both the initial state and the state transformation during an action to be customized. Like React’s props, their use is encouraged to enable reducers to be less hard-coded and more reusable.
The initial state in Flambeau is determined by a declaration of the exported
function getInitialState
. The props are passed as the first argument.
export function getInitialState({ initialItems = [] }) {
return {
items: initialItems
};
}
Responding to actions is done in a similar manner to action creators: by declaring functions. The name of the function mirrors that of the action creator. The functions are declared in a vanilla JavaScript object, named the same as the action set.
The only difference from action creators is the current state is passed as the first argument.
export const TodoListActions = {
addTodo(state, { text }) {
return state.concat({ text });
}
}
Reducers can be easily composed within each other. This allows you to break your reducers into multiple pieces.
Forwarding actions is possible, especially easily done in bulk per action set. To forward an action, declare an action set as a function instead of an object. When an action from this set is dispatched, this function will be called with the following parameters.
isAction
: when the payload being dispatched is a standard action.isIntrospection
: when introspection into reducers is being requested.actionID
: the identifier of the action or introspection method.payload
: the payload being dispatched.props
: the props of this particular reducer.forwardTo()
: Call this to use another reducer on a subset of your state.
// TodoItemActions.js
export function changeText({ text, index }) {}
export function changeCompleted({ isCompleted, index }) {}
// TodoListReducer.js
import TodoItemReducer from './TodoItemReducer';
export function getInitialState() {
return {
items: []
};
}
export function TodoItemActions(state, { isAction, isIntrospection, payload, props, forwardTo }) {
if (isAction) {
const { index } = payload;
state = Object.assign({}, state, {
items: state.items.slice() // Make a copy of the entire array
});
state.items[index] = forwardTo({ responder: TodoItemReducer, initialState: state.items[index] });
return state;
}
}
// TodoItemReducer.js
export const TodoItemActions = {
changeText(item, { text, index }) {
return Object.assign({}, item, { text });
},
changeCompleted(item, { isCompleted, index }) {
return Object.assign({}, item, { isCompleted });
}
}
An application will often need different actions to be dispatched depending on the
store’s state. Differing from the normal method of directly checking the store
(getState()
in Redux), Flambeau introduces introspection methods, which
allow reducers to completely encapsulate its state from the outside world.
Say a todo list allows importing items from a URL online. The import action creator may want to only load data if it hasn’t been done already. Because action creators are stateless, this bit of information will be stored by a reducer somewhere.
Introspection methods allow a reducer to declare its preference, say whether to load a URL or not, whilst leaving the implementation details of the store’s state hidden from the action creator.
// TodoListActions.js
import fetch from 'isomorphic-fetch';
export function addTodosFromURL({ items, URL }) {}
function importTodosFromURL({ URL }, { currentActionSet }) {
fetch(URL)
.then(response => response.json())
.then(items => currentActionSet.addTodosFromURL({ items, URL }));
}
export function importTodosFromURLIfNeeded({ URL }, { currentActionSet }) {
if (!currentActionSet.consensus.hasImportedFromURL({ URL }).every()) {
// This function is not exported as a public action, instead used directly.
importTodosFromURL({ URL }, { currentActionSet });
}
}
export const introspection = {
hasImportedFromURL({ URL }) {}
};
// TodoListReducer.js
export function getInitialState({ initialItems = [] }) {
return {
items: initialItems,
importedURLs: {}
};
}
export const TodoListActions = {
addTodosFromURL(state, { items, URL }) {
// Other reducers might have returned false from hasImportedFromURL()
if (state.importedURLs[URL]) {
return;
}
return Object.assign({}, state, {
importedURLs: Object.assign({}, state.importedURLs, { [URL]: true }),
items: state.items.concat(items)
});
},
introspection: {
hasImportedFromURL(state, { URL }) {
return Boolean(
state.importedURLs[URL]
);
}
}
};
The consensus
property is part of every connected action set, including
those passed to action creators (currentActionSet
and allActionSets
), that
have introspection methods.
To use it, append your introspection identifier, and call it with the payload to pass to each reducer’s introspection method. Then call one of the following methods:
some([callback])
: likeArray.some
, returns true if callback returns true for any reducer’s result. If a callback is not passed, then the result is treated as a boolean.every([callback])
: likeArray.every
, returns true if callback returns true for every reducer’s result. If a callback is not passed, then the result is treated as a boolean.singleton()
: expects there to be only one reducer, returning its result. Throws an exception if zero or more than one reducer responded.reduce(callback[, initialValue])
: likeArray.reduce
, combines every reducer’s result using a callback passed the combined result so far, and the currently iterated reducer’s result for the introspection method.toArray()
: returns an array of results of every reducer for this introspection method.
if (currentActionSet.consensus.yourIntrospectionID({
yourPayloadProperties: true
}).some()) {
// If any reducer returned true.
}
if (currentActionSet.consensus.yourIntrospectionID({
yourPayloadProperties: true
}).every()) {
// If all reducers returned true.
}
const currentActionSet.consensus.yourIntrospectionID({
yourPayloadProperties: true
}).singleton();
// A single chosen reducer returned a value.
// Throws if no or multiple reducers returned a result.
// Great for configuration variables.
const combinedResult = currentActionSet.consensus.yourIntrospectionID({
yourPayloadProperties: true
}).reduce((combined, current) => {
// Reduce `combined` and `current`
return combined + current;
}, /* optional initialValue */ 0);