yarn add react-redux-gen
# or
npm install react-redux-genPeer dependency: redux (>= 4). Async REST/GraphQL thunks use axios (bundled dependency).
Generate Redux action names, plain action creators, async thunks, and reducers from naming conventions.
v1 is written in TypeScript, supports REST, GraphQL, and WebSocket via a pluggable adapter layer, and uses named lifecycle access (requested / success / error) instead of array indices.
New to Redux? Start with the official Redux docs.
As apps grow, action/reducer boilerplate repeats. If your API follows REST (or GraphQL / WS) conventions, you can generate the Redux surface once and stay consistent.
import { createClient, genPlainActions, genReducer } from 'react-redux-gen'
// 1) Choose a protocol
const client = createClient({
protocol: 'REST',
baseUrl: 'https://api.example.com/users',
headers: { Authorization: `Bearer ${token}` },
})
// 2) Generate async thunks + reducer
const userAsync = client.genAsyncActions('user')
const userReducer = client.genReducer('user', {
data: null,
error: false,
completed: true,
})
// 3) Dispatch
dispatch(userAsync.list())
dispatch(userAsync.create({ name: 'Ada' }))
dispatch(userAsync.fetch(1))
dispatch(userAsync.update(1, { name: 'Ada Lovelace' }))
dispatch(userAsync.delete(1))const actions = genPlainActions('user', ['action'])
// Prefer named keys:
actions.action.requested()
actions.action.success(data)
actions.action.error(error)
// Action type strings:
const names = client.genActionNames('user')
names.create.requested // 'CREATE_USER_REQUESTED'
names.create.success // 'CREATE_USER_SUCCESS'
names.create.error // 'CREATE_USER_ERROR'Migration from v0:
actions.x[0|1|2]→actions.x.requested|success|error.
These work with any protocol (they only care about naming):
import {
genActionNames,
genPlainActions,
genAsyncActions,
genReducer,
} from 'react-redux-gen'Returns named action type strings.
| Parameter | Type | Default |
|---|---|---|
entity |
string |
required |
types |
string[] |
['create','update','delete','list','fetch'] |
states |
string[] |
['REQUESTED','SUCCESS','ERROR'] |
genActionNames('user')
// {
// create: { requested: 'CREATE_USER_REQUESTED', success: '...', error: '...' },
// update: { ... },
// ...
// }Returns named action creators.
const actions = genPlainActions('user')
actions.create.requested()
// { type: 'CREATE_USER_REQUESTED', completed: false }
actions.create.success({ id: 1 })
// { type: 'CREATE_USER_SUCCESS', completed: true, error: false, data: { id: 1 } }
actions.create.error(err)
// { type: 'CREATE_USER_ERROR', completed: true, error: err }Convenience wrapper for REST (same as createClient({ protocol: 'REST', baseUrl: url })).
const userAsync = genAsyncActions('user', 'https://api.example.com/users', {
Authorization: 'Bearer …',
})
dispatch(userAsync.create({ name: 'Ada' }))Standard CRUD reducer for default types × default states.
const user = genReducer('user', { data: {}, error: false, completed: true })Initialize once with the protocol your project uses. Adapters share a common base class so more transports can be added later.
import { createClient } from 'react-redux-gen'
createClient({ protocol: 'REST', baseUrl: '…' })
createClient({ protocol: 'GRAPHQL', endpoint: '…' })
createClient({ protocol: 'WEBSOCKET', url: '…' })Every adapter implements:
interface Adapter {
readonly protocol: 'REST' | 'GRAPHQL' | 'WEBSOCKET'
genActionNames(entity, types?, states?)
genPlainActions(entity, types?, states?)
genAsyncActions(entity, options?)
genReducer(entity, initialState)
}const rest = createClient({
protocol: 'REST',
baseUrl: 'https://api.example.com/users',
headers: { 'Content-Type': 'application/json' },
})
const asyncActions = rest.genAsyncActions('user')
// optional resource URL override:
rest.genAsyncActions('user', 'https://api.example.com/v2/users')| Op | HTTP | URL |
|---|---|---|
| list | GET | baseUrl |
| fetch | GET | baseUrl/:id |
| create | POST | baseUrl |
| update | PUT | baseUrl/:id |
| delete | DELETE | baseUrl/:id |
Generates standard list / get queries and create / update / delete mutations.
const gql = createClient({
protocol: 'GRAPHQL',
endpoint: 'https://api.example.com/graphql',
selection: 'id name email',
})
const asyncActions = gql.genAsyncActions('user')
// Override documents when needed:
gql.genAsyncActions('user', {
operations: {
list: `query { users { id name } }`,
},
})Sends JSON envelopes and waits for a correlated response (requestId / matching type).
const ws = createClient({
protocol: 'WEBSOCKET',
url: 'wss://api.example.com/ws',
messageTypePrefix: 'app',
// Node / tests: inject a socket factory
createSocket: (url) => new WebSocket(url),
})
const asyncActions = ws.genAsyncActions('message')
dispatch(asyncActions.create({ text: 'hello' }))Envelope:
{ "type": "app.message.create", "payload": { "text": "hello" }, "requestId": "…" }Use plain generators when the flow is not standard CRUD (login, multi-step, etc.):
import axios from 'axios'
import { genPlainActions, genActionNames } from 'react-redux-gen'
const loginActions = genPlainActions('user', ['logged', 'login', 'logout', 'register'])
const loginActionNames = genActionNames('user', ['logged', 'login', 'logout', 'register'])
const login = (user) => (dispatch) => {
dispatch(loginActions.login.requested())
return axios
.post('/authorize/local', user)
.then((res) => dispatch(loginActions.login.success(res.data)))
.catch((err) => dispatch(loginActions.login.error(err)))
}Hand-written reducer:
switch (action.type) {
case loginActionNames.login.requested:
return { ...state, completed: action.completed }
case loginActionNames.login.success:
return { ...state, completed: action.completed, data: action.data, error: false }
case loginActionNames.login.error:
return { ...state, completed: action.completed, error: action.error }
default:
return state
}Full runnable demos: examples/ (REST, GraphQL, WebSocket, custom auth).
src/
types.ts # shared public types
index.ts # public API
constants/ # default CRUD types & states
shared/ # pure helpers (one function per file)
actions/ # genActionNames, genPlainActions, genAsyncActions
reducers/ # genReducer
adapters/
types.ts
base-adapter.ts # abstract Adapter
create-client.ts # factory
rest/
graphql/
websocket/
Design rules:
- Types live in
types.ts(or adapter-localtypes.ts) - One function (or class) per file
- Shared pure logic under
shared/ - Protocol code isolated under
adapters/<name>/
# Node 18+ recommended
npm install
npm test
npm run test:coverage # threshold: 90%
npm run build
npm run typecheck| Script | Description |
|---|---|
npm test |
Jest unit tests |
npm run test:coverage |
Coverage report (fails under 90%) |
npm run build |
Emit CJS + ESM + .d.ts via tsup |
npm run dev |
Watch build |
npm run typecheck |
tsc --noEmit |
npm run build
cd examples && npm install
npm run rest # or graphql | websocket | custom | allSee examples/README.md.
Adapters let the library support additional transports (gRPC-web, Firebase, tRPC, …) without mixing concerns.
src/adapters/myprotocol/
types.ts # protocol-specific options & message types
myprotocol-adapter.ts # class extending BaseAdapter
# one helper function per file, e.g.:
build-request.ts
create-myprotocol-thunk.ts
// src/adapters/myprotocol/myprotocol-adapter.ts
import { BaseAdapter } from '../base-adapter'
import type { AsyncActionsMap } from '../../types'
import type { MyProtocolAdapterOptions } from './types'
import { createMyProtocolThunk } from './create-myprotocol-thunk'
export class MyProtocolAdapter extends BaseAdapter {
readonly protocol = 'MYPROTOCOL' as const
// store options…
constructor(options: MyProtocolAdapterOptions) {
super(options)
// …
}
genAsyncActions(entity: string, options?: unknown): AsyncActionsMap {
const plain = this.genPlainActions(entity)
const actions: AsyncActionsMap = {}
Object.keys(plain).forEach((key) => {
actions[key] = createMyProtocolThunk(
key,
entity,
/* transport deps */,
plain[key]
)
})
return actions
}
}BaseAdapter already implements genActionNames, genPlainActions, and genReducer. You only implement async I/O.
Each async action must:
dispatch(plain.requested())- Perform the transport call
dispatch(plain.success(data))ordispatch(plain.error(error))- Return a
Promiseso callers canawait dispatch(...)
Use helpers from src/shared/ for action objects when useful; reuse createStateActionCreator / named maps rather than inventing parallel shapes.
- Extend
Protocolinsrc/types.ts(or keep protocol as a string on the class only). - Add options to the
CreateClientOptionsunion insrc/adapters/types.ts. - Handle the case in
src/adapters/create-client.ts. - Export the adapter + types from
src/index.ts.
- Put public types in
src/types.tsor export adaptertypes.tsfrom the package entry. - Keep internal request/response shapes in the adapter folder’s
types.ts. - Prefer explicit interfaces over
any.
Add tests/myprotocol-adapter.test.ts covering:
- Success path for each operation you support
- Error path
- Edge cases (timeouts, malformed payloads, missing config)
createClient({ protocol: 'MYPROTOCOL', … })wiring
Keep global coverage ≥ 90% (npm run test:coverage).
- Add
examples/src/myprotocol/with a runnable offline demo. - Document it in
examples/README.md. - Add a short section to this README under Protocol adapters.
- List any new peer dependencies in
package.jsonand this install section.
-
src/adapters/<name>/with one function/class per file - Extends
BaseAdapter, implementsgenAsyncActions - Registered in
createClient - Exported from
src/index.ts - Unit tests + coverage ≥ 90%
- Example project + README notes
- No protocol-specific logic leaked into
shared/or other adapters
createClient(options)
│
┌─────────────┼─────────────┐
▼ ▼ ▼
RestAdapter GraphQLAdapter WebSocketAdapter
│ │ │
└─────────────┴─────────────┘
│
BaseAdapter
genActionNames / genPlainActions / genReducer
│
shared helpers + actions/ + reducers/
Ahorta — https://ahorta.io
Real-world actions/reducers: ahorta-client
MIT — see LICENSE
Maintained by Alexandre Magno
