🥒 Define and share reusable slices of Redux.
yarn add redux-definitions
TLDR - Define and share reusable slices of Redux.
Common reducer patterns always get recycled, write them once and then never repeat yourself again! Use definitions to automatically generate namespaced reducers, actions, and selectors for a predefined slice of functionality.
The example below implements the Redux code necessary for a basic shopping experience in 12 lines of code. This is achieved by leveraging an existing Collection definition and creating a custom Cart definition.
import { createReducers, createDefinition, Definitions } from 'redux-definitions'
const { Collection } = Definitions
const Cart = createDefinition({
defaultState: [],
reducers: {
addItem: (state, { payload }) => state.push(payload),
clearItems: () => [],
},
selectors: {
getTotal: (state) => state.reduce((total, item) => total + item.price, 0)
}
})
const { actions, reducers, selectors } = createReducers({
shopping: {
items: Collection,
cart: Cart
}
})
BONUS
Use the browser console to call actions and selectors against the running Redux application.
Inspired by lessons learned building countless UIs, Redux Definitions is a library that abstracts common Redux reducer patterns into reusable definitions that can be shared and used to generate standardized and logically related actions, reducers, and selectors.
Redux Definitions is 100% compatible with any existing Redux-based project.
To help organizations scale development, maintainability, and velocity on Redux-based projects.
yarn add redux-definitions
As shown above your core application state is described using a library of reducer definitions. createReducers
takes these definitions and creates reducers for you with corresponding actions and selectors.
import { createReducers, Definitions } from 'redux-definitions'
const { Collection, Flag, Field, Index } = Definitions
const { actions, reducers, selectors } = createReducers({
todoList: {
todos: Collection,
completedIds: Index,
selectedIds: Index
},
todoEditor: {
isEditing: Flag,
editingId: Field
}
})
Each top-level key in the
createReducers
schema generates a separate reducer.
Action creator functions are returned from createReducers
calls as actions
. The reducer definition determines what actions are available. For example a Collection
has actions create
, upsert
, remove
, set
, reset
, clear
. Learn more about what actions are available and what their expected payloads look like the reducer definitions section.
const { todoList, todoEditor } = actions
todoList.todos.create({ id: '1', message: 'Do thee laundry' })
// { type: 'todos/create', payload: { id: '1', message: 'Do thee laundry' } }
todosList.todos.update({ id: '1', message: 'Do the laundry' })
// { type: 'todos/update', payload: { id: '1', message: 'Do the laundry' } }
todoEditor.editingId.set('1')
// { type: 'todoEditor/editingId/set', payload: '1' }
todoList.todos.remove('1')
// { type: 'todos/remove', payload: '1' }
todoList.selectedIds.add('1')
// { type: 'selected/add', payload: '1' } }
todoList.selectedIds.clear()
// { type: 'selected/clear' }
Selectors are also returned from createReducers
. For example a Collection
has all
, find
, ids
, and count
:
const { todoList, todoEditor } = selectors
todoList.todos.all(state) // returns a collection of todos
todoList.todos.find(state, { id }) // returns a todo with matching `id`
todoList.todos.ids(state) // returns an array of ids
These selectors are perfect for feeding into Reselect
When in dev-mode Redux Definitions automatically provides a REPL-like experience in the browser console for dispatching pre-bound actions and selectors. Actions and selectors from all createReducers
calls are available in the REPL.
For your convenience unlike normal actions and selectors, calls to actions and selectors in the browser console are pre-bound to
store.dispatch
andstore.getState
. Remember, only in the console!
To setup the REPL, import startRepl
and call it on your project's store
object.
import { startRepl } from 'redux-definitions'
...
const store = createStore(rootReducer, initialState, applyMiddleware(..))
startRepl(store)
Note: when server-side rendering this call will be a no-op.
All reducer definitions accept initialState
values.
import { createReducers, Definitions } from 'redux-definitions'
const { Collection, Flag, Field } = Definitions
const { reducers } = createReducers({
todoList: {
todos: Collection({
initialState: [{ id: '1', message: 'Do the laundry' }]
}),
completedIds: Index({
initialState: ['1']
}),
selectedIds: Index
},
todoEditor: {
isEditing: Flag({
initialState: true
}),
editingId: Field({
initialState: 'fooId'
})
}
})
Redux Definitions provides an assortment of reducer definitions that can be found by importing the Definitions
object. Reducer definitions aim to be low-level enough to be generic but high level enough to abstract state patterns common to all applications.
Field creates a simple reducer that stores any value and comes with action types that set and clear.
set(payload: any)
Sets Field to payload value.
clear(void)
Clears current Field state.
get(state: {}): any
Returns the Field state.
isSet(state: {}): boolean
Returns a boolean specifying whether a Field value is set.
Record creates a reducer that stores an object. The object can be set or updated (similar to setState).
set(payload: {})
Sets Record state to payload object.
update(payload: {})
Merges payload object into the current Record state.
clear(void)
Clears Record and sets it to an empty object.
get(state: {}, keys?: string|string[]): {}
Returns the Record object, optionally takes keys to return a subset of the Record.
keys(state: {}): string[]
Returns an array of the Record's existing key names.
Flag creates a reducer that stores a boolean value and comes with actions for setting and toggling.
set(payload?: boolean)
Sets Flag to true or to the optional payload's boolean value.
unset(void)
Sets Flag to false.
toggle(void)
Toggles Flag value.
get(state: {}): boolean
Returns the current Flag value.
Collection creates a reducer that stores Entities
. Entities
are objects with id
properties. Entities can take any form as long as they at least have an id.
Id = string
Entity = { id: Id }
set(payload: Entity[])
Takes an array of entities. Resets entire Collection to the payload of entities.
reset(void)
Resets the entire Collection to empty.
create(payload: Entity)
Takes an Entity and adds it to the Collection. Warning will be logged if an entity with the id
already exists.
update(payload: Entity)
Takes an Entity and updates it in the Collection. Entity is not added unless an entity with the id
already exist.
upsert(payload: Entity)
Takes an Entity and updates it in the Collection. The entity will be added if an entity with the id
does not exist.
remove(payload: Id|Id[])
Takes an Id and removes any existing entity with the id
.
all(state: {}): Entity[]
Returns array of entities.
ids(state: {}): Id[]
Returns array of ids.
find(state: {}, params: { id: Id }): Entity
Returns entity that matches id parameter.
count(state: {}) => number
Returns the number of entities in the Collection.
get(state: {}) => Normalized
Returns the full underlying data structure which takes the form: { ids, entities }
where ids
is an array of unique id
keys and entities
is an id
-based lookup map.of ids and entities.
Index creates a reducer that stores a unique set of ids. Ids can be added, removed, and toggled. An Index is perfect for
Id = string
set(payload: Id[])
Takes an array of identifiers. Resets entire Index to the payload.
reset(void)
Resets entire Index to empty.
toggle(payload: Id)
Takes an identifier and toggles its presence in the Index.
add(payload: Id|Id[])
Takes an identifier(s) and ensures its presence in the Index.
remove(payload: Id|Id[])
Takes an identifier(s) and ensures its removal from the Index.
get(state: {}): Ids[]
Returns the entire Index array.
includes(state: {}, { id: Id }): boolean
Takes id parameter and checks whether the identifier is present in the Index, returns boolean value.
count(state: {}) => number
Returns the number of Ids in the Index.
Create new reducer definitions with the createDefinition
function. The resulting object is a valid definition that can be used.
import { createDefinition } from 'redux-definitions'
const SpecialField = createDefinition({
defaultState: 'morty',
reducers: {
set: (state, { payload }) => payload,
clear: () => undefined,
},
selectors: {
get: (state) => state
}
})
export { SpecialField }
import { createReducers } from 'redux-definitions'
import { SpecialField } from './specialField'
const { reducers } = createReducers({
people: {
rick: SpecialField
}
})
Redux Definitions is written in TypeScript. Due to the generative nature of the library fully typed actions, reducers, and selectors have proven difficult to implement. The goal is to get to the point where all actions and reducers have fully typed payloads. ✨Contributions from anyone with ideas on how to achieve this are very appreciated! Feel free to open a Github issue or start a conversation on Spectrum with any thoughts or ideas.
PRs with other examples are appreciated!
Coming soon!
Please check out the Contributing page to learn how to get involved.