@hvish/redux-thaga
v1.1.0
Published
redux middleware for redux-saga with redux-thunk capabilities
Readme
Redux thaga
This redux middleware enhances redux-saga with redux-thunk capabilites.
Requires
@reduxjs/toolkitv2+ andredux-sagav1+.
Usage
// file: reducer.ts
import {
PayloadAction,
Update,
createEntityAdapter,
createSlice,
EntityState,
} from '@reduxjs/toolkit';
import { call, takeLatest } from 'redux-saga/effects';
import { createThagaAction } from '@hvish/redux-thaga';
export interface Task {
id: string;
title: string;
isCompleted: boolean;
}
const taskAdapter = createEntityAdapter<Task>();
export const allTasksSelector = (state: { tasks: EntityState<Task> }) =>
taskAdapter.getSelectors().selectAll(state.tasks);
const taskApi = async () => {
const response = await fetch('/tasks.json');
const tasks: Task[] = await response.json();
return tasks;
};
export const fetchTasks = createThagaAction(
'fetchTasks',
function* fetchTasksWorker() {
// arguments: (actionPayload, action, ...restArgs)
const tasks = (yield call(taskApi)) as Task[];
return tasks;
},
);
export const { actions, reducer: tasksReducer } = createSlice({
name: 'tasks',
initialState: taskAdapter.getInitialState(),
reducers: {
addTask(state, action: PayloadAction<Task>) {
taskAdapter.addOne(state, action.payload);
},
updateTask(state, action: PayloadAction<Update<Task>>) {
taskAdapter.updateOne(state, action.payload);
},
},
extraReducers: (builder) => {
builder.addCase(fetchTasks.finished, (state, action) => {
taskAdapter.addMany(state, action.payload);
});
},
});
export function* tasksWorker() {
try {
yield takeLatest(fetchTasks, fetchTasks.worker);
} catch (error) {
console.log(error);
}
}// file: store.ts
import { configureStore } from '@reduxjs/toolkit';
import createSagaMiddleware from 'redux-saga';
import { createThagaMiddleware } from '@hvish/redux-thaga';
import { tasksWorker, tasksReducer } from './reducer';
const sagaMiddleware = createSagaMiddleware();
const thagaMiddleware = createThagaMiddleware();
export const store = configureStore({
reducer: { tasks: tasksReducer },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(sagaMiddleware, thagaMiddleware),
});
sagaMiddleware.run(tasksWorker);// file: App.tsx
import { useDispatch, useSelector } from 'react-redux';
import { Task, allTasksSelector, fetchTasks } from './reducer';
function App() {
const tasks = useSelector(allTasksSelector);
const dispatch = useDispatch();
const onClick = async () => {
try {
const tasks = await dispatch(fetchTasks());
console.log(tasks);
} catch (error) {
console.log('unable to fetch tasks');
}
};
return (
<div>
<button onClick={onClick}>Fetch Tasks</button>
{tasks.map((task) => (
<div key={task.id}>{task.title}</div>
))}
</div>
);
}
export default App;Typed dispatch
Importing anything from @hvish/redux-thaga augments Redux's Dispatch (and
redux-thunk's ThunkDispatch, which configureStore uses by default), so
dispatching a thaga action returns a fully typed ThagaPromise — no casts and
no store wiring required:
const dispatch = useDispatch();
const tasks = await dispatch(fetchTasks()); // inferred as Task[]
dispatch(fetchTasks()).cancel('user navigated away'); // ThagaPromise<Task[]>Typed hooks (useDispatch<AppDispatch>()) work the same way. Under the hood
the worker's return type rides along on the initiator action via the phantom
THAGA_RETURN property (see ThagaInitiatorAction) — nothing extra exists at
runtime. The augmentation needs redux and redux-thunk to be resolvable by
TypeScript; both are declared as peer dependencies, so a regular install
already satisfies this (including under pnpm and Yarn PnP).
API
createThagaMiddleware(options?)
Creates redux middleware that turns each thaga dispatch into a Promise.
Options:
getId?: () => string— correlation id generator.timeoutMs?: number— default per-action timeout. Per-actiontimeoutMs(set oncreateThagaAction) takes precedence. No timeout by default.
createThagaAction(type, worker, options?)
Creates a thaga action creator (extends redux-toolkit's createAction).
type— action type string.worker— generator called with(payload, action, ...extraArgs)when the action is dispatched (via your saga, e.g.takeLatest(action, action.worker)).options.timeoutMs?: number— per-action timeout in ms; overrides middleware default.
Action creator properties
type,match(),toString()— inherited fromcreateAction.worker— saga worker to run on dispatch.finished(result, initiator)— emitted when the worker resolves;payloadis the worker's return value.failed(initiator, error)— emitted when the worker throws;payloadis the serialized error.cancelled(initiator, reason?)— emitted on cancellation;payloadis the reason (orundefined).
Returned Promise
dispatch(thagaAction(payload)) returns a ThagaPromise<T>:
- Resolves with the worker's return value on
finished. - Rejects with
SerializedErroronfailed. - Rejects with
ThagaCancelledError(carrying.reason) oncancelled. - Rejects with
ThagaTimeoutErrorif a timeout elapses. .cancel(reason?)— dispatches acancelledaction with the same id and rejects the promise. The saga worker keeps running unless your saga handles cancellation (e.g. viatakeLatestorrace).
Exports
import {
createThagaAction,
createThagaMiddleware,
isThagaAction,
serializeError,
THAGA_RETURN,
ThagaCancelledError,
ThagaTimeoutError,
} from '@hvish/redux-thaga';
import type {
CreateThagaActionOptions,
CreateThagaMiddlewareOptions,
SerializedError,
ThagaInitiatorAction,
ThagaMetaData,
ThagaPromise,
} from '@hvish/redux-thaga';