Handy Reducer Patterns

A set of strategies for scaling your application

The Big Reducer

Great for smaller apps, and learning projects!

theBigReducer.js
import { app, effects } from './ferp';
const createItem = (title) => ({ title, done: false });
const arraySplitAtIndex = (index, length, array) => {
const before = array.slice(0, index);
const after = array.slice(index + length);
return {
before,
target: array.slice(index, index + length),
after,
};
};
const arrayMoveLeft = (index, array) => {
if (index <= 0) return array;
const { before, target, after } = arraySplitAtIndex(index - 1, 2, array);
return [...before, ...target.reverse(), ...after];
};
const arrayMoveRight = (index, array) => arrayMoveLeft(index + 1, array);
const messages = {
add: 'add',
done: 'done',
moveUp: 'moveUp',
moveDown: 'moveDown',
};
const addItemEffect = (title) => ({
type: messages.add,
item: createItem(title),
});
const doneItemEffect = (title) => ({
type: messages.done,
title,
});
const moveUpItemEffect = (title) => ({
type: messages.moveUp,
title,
});
const moveDownItemEffect = (title) => ({
type: messages.moveDown,
title,
});
const logItemsEffect = ({ items }) => {
console.log(' Done Title');
console.log('-------------------');
items.forEach((item) => {
const marker = item.done ? 'X' : ' ';
console.log(` [${marker}] ${item.title}`);
});
return effects.none();
};
const initialState = {
items: [],
};
const itemIndexFromStateUsingTitle = (title, state) => state.items.findIndex(item => item.title === title);
const reduceByType = (typeFunctions, fallback) => (message, previousState) => {
const next = typeFunctions[message.type];
if (next) {
return next(message, previousState);
}
return fallback(message, previousState);
};
app({
init: [
initialState,
effects.batch([
addItemEffect('First'),
addItemEffect('Second'),
addItemEffect('Third'),
addItemEffect('Fourth'),
addItemEffect('Fifth'),
moveUpItemEffect('Second'),
moveDownItemEffect('Third'),
moveUpItemEffect('Third'),
moveUpItemEffect('Third'),
moveUpItemEffect('Third'),
doneItemEffect('Second'),
doneItemEffect('Fifth'),
{},
]),
],
update: reduceByType({
add: ({ item }, state) => [{ items: state.items.concat(item) }, effects.none()],
done: ({ title }, state) => [{ items: state.items.map((item) => ({ ...item, done: item.title === title ? true : item.done })) }, effects.none()],
moveUp: ({ title }, state) => [{ items: arrayMoveLeft(itemIndexFromStateUsingTitle(title, state), state.items) }, effects.none()],
moveDown: ({ title }, state) => [{ items: arrayMoveRight(itemIndexFromStateUsingTitle(title, state), state.items) }, effects.none()],
}, (_, state) => [state, logItemsEffect(state)]),
});

Nesting Reducers

Top-Level Effects Only

Reasonable for scaling smaller apps, but not for when nested states produce side-effects.

topLevelEffects.js
const { app, effects } = require('./ferp.js');
const createItem = (title) => ({ title, done: false });
const arraySplitAtIndex = (index, length, array) => {
const before = array.slice(0, index);
const after = array.slice(index + length);
return {
before,
target: array.slice(index, index + length),
after,
};
};
const arrayMoveLeft = (index, array) => {
if (index <= 0) return array;
const { before, target, after } = arraySplitAtIndex(index - 1, 2, array);
return [...before, ...target.reverse(), ...after];
};
const arrayMoveRight = (index, array) => arrayMoveLeft(index + 1, array);
const messages = {
add: 'add',
done: 'done',
moveUp: 'moveUp',
moveDown: 'moveDown',
};
const addItemEffect = (title) => ({
type: messages.add,
item: createItem(title),
});
const doneItemEffect = (title) => ({
type: messages.done,
title,
});
const moveUpItemEffect = (title) => ({
type: messages.moveUp,
title,
});
const moveDownItemEffect = (title) => ({
type: messages.moveDown,
title,
});
const logItemsEffect = ({ items }) => {
console.log(' Done Title');
console.log('-------------------');
items.forEach((item) => {
const marker = item.done ? 'X' : ' ';
console.log(` [${marker}] ${item.title}`);
});
return effects.none();
};
const initialState = {
items: [],
};
const itemIndexFromStateUsingTitle = (title, state) => (
state.items.findIndex(item => item.title === title)
);
const itemReducerNoEffect = (message, item) => {
if (message.title !== item.title) return item;
switch (message.type) {
case messages.done:
return {
...item,
done: true,
};
default:
return item;
}
};
app({
init: [
initialState,
effects.batch([
addItemEffect('First'),
addItemEffect('Second'),
addItemEffect('Third'),
addItemEffect('Fourth'),
addItemEffect('Fifth'),
moveUpItemEffect('Second'),
moveDownItemEffect('Third'),
moveUpItemEffect('Third'),
moveUpItemEffect('Third'),
moveUpItemEffect('Third'),
doneItemEffect('Second'),
doneItemEffect('Fifth'),
{},
]),
],
update: (message, previousState) => {
const reduceOverItems = (items) => items.map(item => itemReducerNoEffect(message, item));
switch (message.type) {
case messages.add:
return [
{
items: reduceOverItems(previousState.items.concat(message.item)),
},
effects.none(),
];
case messages.moveUp:
return [
{
items: reduceOverItems(
arrayMoveLeft(
itemIndexFromStateUsingTitle(message.title, previousState),
previousState.items,
),
),
},
effects.none(),
];
case messages.moveDown:
return [
{
items: reduceOverItems(
arrayMoveRight(
itemIndexFromStateUsingTitle(message.title, previousState),
previousState.items,
),
),
},
effects.none(),
];
case messages.done:
return [
{ items: reduceOverItems(previousState.items) },
effects.none(),
];
default: {
const state = { items: reduceOverItems(previousState.items) };
return [
state,
logItemsEffect(state),
];
}
}
},
});

Nested Effects with combineReducers

Great for deeply nested states that produce side-effects!

nestedEffects.js
import { app, effects, util } from 'ferp';
const createItem = (title) => ({ title, done: false });
const arraySplitAtIndex = (index, length, array) => {
const before = array.slice(0, index);
const after = array.slice(index + length);
return {
before,
target: array.slice(index, index + length),
after,
};
};
const arrayMoveLeft = (index, array) => {
if (index <= 0) return array;
const { before, target, after } = arraySplitAtIndex(index - 1, 2, array);
return [...before, ...target.reverse(), ...after];
};
const arrayMoveRight = (index, array) => arrayMoveLeft(index + 1, array);
const messages = {
add: 'add',
done: 'done',
moveUp: 'moveUp',
moveDown: 'moveDown',
};
const addItemEffect = (title) => ({
type: messages.add,
item: createItem(title),
});
const doneItemEffect = (title) => ({
type: messages.done,
title,
});
const moveUpItemEffect = (title) => ({
type: messages.moveUp,
title,
});
const moveDownItemEffect = (title) => ({
type: messages.moveDown,
title,
});
const logItemsEffect = (items) => {
console.log(' Done Title');
console.log('-------------------');
items.forEach((item) => {
const marker = item.done ? 'X' : ' ';
console.log(` [${marker}] ${item.title}`);
});
return effects.none();
};
const initialState = {
items: [],
};
const itemIndexUsingTitle = (title, items) => (
items.findIndex(item => item.title === title)
);
const itemReducer = (message, item) => {
if (message.title !== item.title) return [item, effects.none()];
switch (message.type) {
case messages.done:
return [
{
...item,
done: true,
},
effects.none(),
];
default:
return [item, effects.none()];
}
};
const itemsReducer = (message, items) => {
const reduceOverItems = (nextItems) => util.combineReducers(nextItems.map(item => itemReducer(message, item)));
switch (message.type) {
case messages.add:
return reduceOverItems(items.concat(message.item));
case messages.moveUp:
return reduceOverItems(
arrayMoveLeft(
itemIndexUsingTitle(message.title, items),
items,
),
);
case messages.moveDown:
return reduceOverItems(
arrayMoveRight(
itemIndexUsingTitle(message.title, items),
items,
),
);
case messages.done:
return reduceOverItems(items);
default:
return [
items,
logItemsEffect(items),
];
}
};
app({
init: [
initialState,
effects.batch([
addItemEffect('First'),
addItemEffect('Second'),
addItemEffect('Third'),
addItemEffect('Fourth'),
addItemEffect('Fifth'),
moveUpItemEffect('Second'),
moveDownItemEffect('Third'),
moveUpItemEffect('Third'),
moveUpItemEffect('Third'),
moveUpItemEffect('Third'),
doneItemEffect('Second'),
doneItemEffect('Fifth'),
{},
]),
],
update: (message, previousState) => util.combineReducers({
items: itemsReducer(message, previousState.items),
}),
});