Get rid of your reducer boilerplate!
Zero hassle state management that's typed, flexible and scalable.
You've seen how even basic usage of Riduce provides zero hassle setup, scalable state management and typesafe action creators.
There's even more that you can do with Riduce:
- Bundle multiple actions into a single dispatch;
- Execute arbitrary reducer logic for extendability;
- Add custom reducers for reusability; and
- Control action types for debugging (e.g. Redux DevTools).
import { createStore } from 'redux'
import riduce from 'riduce'
const museumState = {
isOpen: false,
visitor: {
counter: 0,
guestbook: ['richard woz here']
}
}
const [reducer, actions] = riduce(museumState)
const { getState, dispatch } = createStore(reducer)
Riduce's actions
gives us access to lots of atomic action creators at any node on our state tree, e.g.
actions.isOpen.create.toggle()
actions.visitor.counter.create.increment(5)
actions.visitor.guestbook.create.push("LOL from js fan")
We can build a single complex action out of these atomic actions using bundle
:
import { bundle } from 'riduce'
const actionsBundle = bundle([
actions.isOpen.create.toggle(),
actions.visitor.counter.create.increment(5),
actions.visitor.guestbook.create.push("LOL from js fan")
])
dispatch(actionsBundle)
getState()
/*
{
isOpen: true,
visitor: {
counter: 5,
guestbook: [
'richard woz here',
'LOL from js fan'
]
}
}
*/
Sometimes the simple atomic action creators - update
, set
, clear
... - won't feel sufficient.
The general purpose do
can help with flexibility: it takes a callback of (leafState, treeState) => newLeafState
.
const pizzaShopState = {
stock: {
margherita: 10,
pepperoni: 20
},
isOpen: {
forEatIn: false,
forTakeOut: true
}
}
const [reducer, actions] = riduce(pizzaShopState)
const { getState, dispatch } = createStore(reducer)
const squareMargheritaStock = actions.stock.margherita.create.do(leafState => leafState ** 2)
dispatch(squareMargheritaStock)
getState().stock // => { margherita: 100, pepperoni: 20 }
const openIfSurplusStock = actions.isOpen.create.do(
(leafState, treeState) => {
const hasEnoughStock = treeState.stock.margherita > 10
return {
forEatIn: leafState.forEatIn || hasEnoughStock,
forTakeOut: leafState.forTakeOut || hasEnoughStock
}
}
)
getState().isOpen // => { forEatIn: true, forTakeOut: true }
For reusability, sometimes you might want to abstract out some custom reducer logic which can then be executed at arbitrary leaf state.
This can be done in two ways:
Shorthand 'riducers' are functions with the signature (leafState, action, treeState) => leafState
.
When you pass a dictionary of these to riduce
as a second argument, it automatically makes a corresponding action creator available.
By default, the action creator will take an optional single argument, that gets passed to your riducer logic as action.payload
.
(This behaviour can be changed in a longhand riducer.)
import riduce, { Action } from 'riduce'
import { createStore } from 'redux'
const restaurantState = {
tables: [
{ persons: 4, hasOrdered: false, hasPaid: false },
{ persons: 3, hasOrdered: true, hasPaid: false }
],
stock: {
ramen: {
beef: 5,
veg: 2
},
sushi: {
nigiri: 10,
sashimi: 4
}
}
}
/*
* Note: I'm typing in a slightly unorthodox way
* in the hope that this is more friendly for
* non-TypeScript users.
*
* (I suggest explicitly typing state.)
*/
type Table = typeof restaurantState['tables'][0]
const finishTable = (tableState: Table) => ({
...tableState,
hasOrdered: true,
hasPaid: true
})
/*
* Take an object with number values, and decrease each
* value by a given argument (the action payload).
*/
const decreaseValuesBy = (leafState: Record<string, number>, action: Action<number>) => {
const keys = Object.keys(leafState)
return keys.reduce((acc, key) => ({
...acc,
[key]: leafState[key] - action.payload
}), {})
}
const [reducer, actions] = riduce(restaurantState, {
finishTable,
decreaseValuesBy
})
const { getState, dispatch } = createStore(reducer)
dispatch(actions.tables[0].create.finishTable())
getState().tables[0] // => { persons: 4, haveOrdered: true, havePaid: true }
dispatch(actions.tables[1].create.finishTable())
getState().tables[1] // => { persons: 3, haveOrdered: true, havePaid: true }
// ❌ TypeError: (ts 2339) Property 'finishTable' does not exist on type...
actions.stock.ramen.create.finishTable()
// By default, the first argument passed becomes action.payload
dispatch(actions.stock.ramen.create.decreaseValuesBy(1))
getState().stock.ramen // => { beef: 4, veg: 1 }
dispatch(actions.stock.sushi.create.decreaseValuesBy(4))
getState().stock.sushi // => { nigiri: 6, sashimi: 0 }
// ❌ TypeError: (ts 2339) Property 'decreaseValuesBy' does not exist on type...
actions.tables.create.decreaseValuesBy()
Longhand riducer definitions benefit from being more strongly typed.
import riduce, { Riducer } from 'riduce'
import { createStore } from 'redux'
const bookstoreState = {
books: {
9780007925568: {
title: 'Moby Dick',
authorName: 'Herman Melville',
stock: 7
},
9780486280615: {
title: 'The Adventures of Huckleberry Finn',
authorName: 'Mark Twain',
stock: 10
},
9780764502231: {
title: 'JavaScript for Dummies',
authorName: 'Emily A. Vander Veer',
stock: 5
}
},
visitor: {
count: 2,
guestbook: []
}
}
type BookstoreState = typeof bookstoreState
interface BookReview {
id: keyof BookstoreState['books'],
stars: number,
comment?: string
}
const addBookReviews: Riducer<{
treeState: BookstoreState,
leafState: string[],
args: BookReview[],
payload: BookReview[]
}> = {
// pass all arguments through as payload
argsToPayload: (...reviews) => reviews,
// push into the string[] a formatted review for each book
reducer: (leafState, { payload: reviews = [] }, treeState) => {
return reviews.reduce((acc, { stars, id, comment = '' }) => ([
...acc,
`${stars} stars for ${treeState.books[id].title}! ${comment}`
]), leafState)
}
}
const [reducer, actions] = riduce(bookstoreState, { addBookReviews })
const { getState, dispatch } = createStore(reducer)
// ❌ TypeError: (ts 2339) Property 'addBookReviews' does not exist on type...
actions.create.addBookReviews([])
// ❌ TypeError: (ts 2332) Type 'string' is not assignable to type '9780007925568 | 9780486280615 | 9780764502231'
actions.visitor.guestbook.create.addBookReviews(
{ id: '9780007925568', stars: 4.5 }
)
dispatch(actions.visitor.guestbook.create.addBookReviews(
{ id: 9780007925568, stars: 4.5 },
{ id: 9780764502231, stars: 5, comment: 'so great!!' }
)
getState().visitor.guestbook
/*
[
'4.5 stars for Moby Dick! ',
'5 stars for JavaScript for Dummies! so great!!'
]
*/
Any time you are calling create
, you can pass an optional string argument to it. This will be the type
of any resulting action that gets created. (The riduce
reducer will still deal with it in the same way.)
If you are using bundle
, you can pass a second argument of a string to control the type instead.
import riduce, { bundle } from 'riduce'
import { createStore } from 'redux'
const initialState = {
counter: 0,
nums: [4]
}
const double = (leafState: number) => 2 * leafState
const [reducer, actions] = riduce(initialState, { double })
const { getState, dispatch } = createStore(reducer)
const incrementCounter = actions.counter.create('INCREMENTED_COUNTER').increment(5)
incrementCounter.type // => 'INCREMENTED_COUNTER'
dispatch(incrementCounter)
getState().counter // => 5
const doubleCounter = actions.counter.create('DOUBLED_COUNTER').double()
doubleCounter.type // => 'DOUBLED_COUNTER'
dispatch(doubleCounter)
getState().counter // => 10
const storeCountThenDouble = bundle([
actions.nums.create.do((leafState, treeState) => [...leafState, treeState.counter]),
doubleCounter // bundle accepts any Riduce actions
], 'STORED_AND_DOUBLED')
storeCountThenDouble.type // => 'STORED AND DOUBLED'
dispatch(storeCountThenDouble)
getState() // => { counter: 20, nums: [4, 10] }
You may wish to check out the following:
The basic usage of riduce is documented
Have fun adding it to your project!
npm install riduce