Redux reducers without constants and dispatching
Deprecated — please use Redux Toolkit instead
Redaction is no longer maintained. This repository is preserved for historical reference only.
The problem Redaction set out to solve — eliminating boilerplate action constants and manual dispatch() calls — was later solved, correctly and with full TypeScript support, by Redux Toolkit (@reduxjs/toolkit) via its createSlice API. Redux Toolkit is now the official, recommended way to write Redux logic.
If you are starting a new project, do not adopt Redaction. Pick one of the following based on what you actually need:
| If you need… | Use |
|---|---|
| Redux with minimal boilerplate | Redux Toolkit (@reduxjs/toolkit) |
| Server state / data fetching | TanStack Query or RTK Query |
| Tiny client state, no Redux | Zustand |
| Atomic state model | Jotai |
| Proxy-based state | Valtio |
If you are already using Redaction in an existing project, the final published version is 5.0.4. It supports React 15 / 16 / 17 and react-redux 5 / 6 / 7 only. It will not work correctly with React 18+ concurrent rendering, and the README explicitly warns against using it with SSR. A migration to Redux Toolkit is straightforward because the mental model is nearly identical (see Migration below).
Why this library existed
In 2016, idiomatic Redux meant writing, for every action:
- A constant (
constants/todos.js) - An action creator (
actions/todos.js) - A
switchcase in a reducer (reducers/todos.js) - A
mapDispatchToPropswiring in every connected component
Redaction collapsed all four into a single reducer method. The action type was auto-derived from the reducer's key path (todos.addTodo), and dispatch was handled implicitly by a module-level dispatcher captured at createStore time.
This was a genuinely novel idea at the time. Three years later, Redux Toolkit's createSlice shipped the same core insight — "the reducer is the action creator" — with proper TypeScript inference, Immer-backed immutability, and first-class SSR support, which Redaction was never able to provide.
Historical overview
The content below reflects how Redaction was used in 2019. It is preserved for readers maintaining legacy codebases that still depend on the library.
Classic Redux approach (before Redaction)
constants/todos.js
const ADD_TODO = 'ADD_TODO' export { ADD_TODO }
reducers/todos.js
import { ADD_TODO } from 'constants/todos' const initialState = { todos: [] } export default (state = initialState, action) => { switch (action.type) { case ADD_TODO: return { ...state, todos: [ ...state.todos, action.payload ] } default: return state } }
actions/todos.js
import { ADD_TODO } from 'constants/todos' export const addTodo = (text) => (dispatch) => { dispatch({ type: ADD_TODO, payload: text }) }
App.js
import { connect } from 'react-redux' import { addTodo } from 'actions/todos' const App = ({ todos, addTodo }) => ( <div> { todos.map((text, index) => ( <div key={index}>{text}</div> )) } <button onClick={() => addTodo('new todo name')}>Add</button> </div> ) const mapStateToProps = (state) => ({ todos: state.todos, }) const mapDispatchToProps = (dispatch) => ({ addTodo: (text) => { dispatch(addTodo(text)) } }) export default connect(mapStateToProps, mapDispatchToProps)(App)
Same example with Redaction
reducers/todos.js
export const initialState = { todos: [] } export const addTodo = (state, payload) => ({ ...state, todos: [ ...state.todos, payload, ], })
actions/todos.js
import { reducers } from 'core/reducers' export const addTodo = (text) => { reducers.todos.addTodo(text) }
App.js
import actions from 'actions' import { connect } from 'redaction' const App = ({ todos }) => ( <div> { todos.map((text, index) => ( <div key={index}>{text}</div> )) } <button onClick={() => actions.addTodo('new todo name')}>Add</button> </div> ) export default connect({ todos: 'todos', })(App)
No constants. No explicit dispatch. That was the pitch.
Store wiring
core/store.js
import { createStore, combineReducers } from 'redaction' import reducers from 'reducers' const store = createStore({ reducers: { ...combineReducers(reducers), }, initialState: {}, }) export default store
core/reducers.js
import { wrapReducers } from 'redaction' import reducers from 'reducers' export default wrapReducers(reducers)
connect variants
import { connect } from 'redaction' // 1. Classic react-redux style connect(state => ({ todos: state.todos.list })) // 2. Dotted-string path connect({ todos: 'todos.list' }) // 3. Per-key selector functions connect({ todos: (state) => state.todos.list })
Migration to Redux Toolkit
The translation is almost mechanical. A Redaction reducer module:
// reducers/todos.js (Redaction) export const initialState = { list: [] } export const addTodo = (state, payload) => ({ ...state, list: [...state.list, payload], })
Becomes a Redux Toolkit slice:
// features/todos/todosSlice.ts (Redux Toolkit) import { createSlice, PayloadAction } from '@reduxjs/toolkit' const todosSlice = createSlice({ name: 'todos', initialState: { list: [] as string[] }, reducers: { addTodo: (state, action: PayloadAction<string>) => { state.list.push(action.payload) // Immer handles immutability }, }, }) export const { addTodo } = todosSlice.actions export default todosSlice.reducer
And on the component side, replace connect with hooks:
import { useSelector, useDispatch } from 'react-redux' import { addTodo } from './features/todos/todosSlice' const App = () => { const todos = useSelector((s: RootState) => s.todos.list) const dispatch = useDispatch() return ( <div> {todos.map((text, i) => <div key={i}>{text}</div>)} <button onClick={() => dispatch(addTodo('new todo name'))}>Add</button> </div> ) }
You gain: full TypeScript inference, working SSR, React 18 concurrent-safe subscriptions, Immer-based reducer ergonomics, and an actively maintained library.
Known limitations (why it was deprecated)
For anyone considering reviving Redaction or extracting ideas from it, these were the architectural dead-ends:
- Module-level singleton dispatcher.
wrapReducers.tsstoressavedDispatchin module scope. This makes SSR impossible (request state leaks across concurrent renders), breaks multi-store setups, and makes tests order-dependent. - No React 18 support.
connectis built on legacyreact-reduxinternals and does not useuseSyncExternalStore, so tearing under concurrent rendering is possible. - No hooks API. The TODO at the bottom of the old README ("Support React hooks") was never addressed.
- Weak types. The public
Reducerstype is{ [key: string]: any }, so the ergonomicreducers.todos.addTodo(payload)call site accepts anything.createSliceinfersPayloadAction<T>end-to-end. - Deprecated test stack. Tests depend on Enzyme +
enzyme-adapter-react-16, both abandoned. - Deprecated example ecosystem. The
examples/folder referencesredux-formandredux-auth-wrapper, both of which are themselves deprecated.
Status
- Last release:
5.0.4 - Maintenance: none
- Peer deps: React 15 / 16 / 17,
react-redux5 / 6 / 7, Redux 3 / 4 - Issues & PRs: the repository remains public for historical reference; new issues are unlikely to be addressed.
License
ISC — see LICENSE.
Author
Pavel Ivanov — grammka@gmail.com
