TypeScript FSA Utilities for Redux Thunk
typescript-fsa-redux-thunk is a utility library that bridges the gap between `typescript-fsa`'s strongly typed Flux Standard Action (FSA) creators and `redux-thunk` for handling asynchronous operations in Redux applications. It provides an `asyncFactory` to easily define typed async action creators, streamlining the process of dispatching `started`, `done`, and `failed` actions that conform to the FSA specification. The current stable version is 2.10.2. While the release cadence isn't explicitly stated, the project appears actively maintained with recent updates, though the latest npm publish was 5 years ago for 2.10.2. A key differentiator is its tight integration with `typescript-fsa`, allowing developers to leverage type safety throughout their async Redux action flows, including custom error types. It also includes `thunkToAction` for easier integration with `bindActionCreators`.
Common errors
-
Argument of type 'ThunkAction<any, any, any, AnyAction>' is not assignable to parameter of type 'AnyAction'. Property 'type' is missing in type 'ThunkAction' but required in type 'AnyAction'.
cause Attempting to dispatch a Redux Thunk action without the `redux-thunk` middleware being applied to the Redux store.fixEnsure `redux-thunk` middleware is correctly applied: `createStore(reducer, applyMiddleware(thunkMiddleware))`. -
Cannot find name 'fetch'. Do you need to install type definitions for a global API such as 'fetch'?
cause The `fetch` API is used in the quickstart example, but it's a browser API not natively available in Node.js, and its types might be missing in some TypeScript configurations.fixFor Node.js environments, install a polyfill like `isomorphic-fetch` (`npm install isomorphic-fetch @types/isomorphic-fetch`) and import it: `import 'isomorphic-fetch';`. -
Error: Reducer for action 'examples/Login_DONE' not found.
cause A `typescript-fsa` action (like `login.async.done`) was dispatched, but the corresponding `.case()` handler is missing or incorrectly named in the reducer.fixVerify that your `reducerWithInitialState` or similar reducer setup includes `.case()` for all `started`, `done`, and `failed` actions created by `asyncFactory`, with the correct action type string.
Warnings
- breaking Version 2.x introduces breaking changes from 1.x, particularly around the assumption of the result type for async actions. The API has been simplified, and the result type is no longer always assumed to be a Promise.
- gotcha `redux-thunk` middleware must be correctly applied to your Redux store for `typescript-fsa-redux-thunk`'s async actions to function. Dispatching a thunk without the middleware will lead to type errors or runtime issues.
- gotcha Properly typing the `dispatch` function within Redux thunks, especially when dispatching other thunks or specific actions, requires careful configuration using `ThunkMiddleware` or explicitly defining `AppDispatch`.
- gotcha This library relies on `typescript-fsa` for action creator factories. Ensure you are using `typescript-fsa` version 3.x or newer, as specified in peer dependencies, to avoid type conflicts or unexpected behavior.
- deprecated Redux-Thunk itself has undergone changes, particularly in Redux Thunk 3.0 (released with Redux 5.0 and RTK 2.0), where the default export was removed in favor of named exports `thunk` and `withExtraArgument`. While `typescript-fsa-redux-thunk` might abstract this, be aware if directly interacting with `redux-thunk`'s exports.
Install
-
npm install typescript-fsa-redux-thunk -
yarn add typescript-fsa-redux-thunk -
pnpm add typescript-fsa-redux-thunk
Imports
- asyncFactory
const asyncFactory = require('typescript-fsa-redux-thunk').asyncFactory;import { asyncFactory } from 'typescript-fsa-redux-thunk'; - thunkToAction
const thunkToAction = require('typescript-fsa-redux-thunk').thunkToAction;import { thunkToAction } from 'typescript-fsa-redux-thunk'; - create
import { create } from 'typescript-fsa';import actionCreatorFactory from 'typescript-fsa';
Quickstart
import 'isomorphic-fetch'; // For Node.js environments
import { createStore, applyMiddleware, AnyAction } from 'redux';
import thunkMiddleware, { ThunkMiddleware } from 'redux-thunk';
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import actionCreatorFactory from 'typescript-fsa';
import { asyncFactory } from 'typescript-fsa-redux-thunk';
interface LoginParams {
email: string;
password: string;
}
interface UserToken {
token: string;
}
class CustomError extends Error {}
interface State {
title: string;
userToken: UserToken;
loggingIn?: boolean;
error?: CustomError;
}
const create = actionCreatorFactory('examples');
const createAsync = asyncFactory<State>(create);
const changeTitle = create<string>('Change the title');
const login = createAsync<LoginParams, UserToken, CustomError>(
'Login',
async (params, dispatch) => {
const url = `https://reqres.in/api/login`;
const options: RequestInit = {
method: 'POST',
body: JSON.stringify(params),
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
};
const res = await fetch(url, options);
if (!res.ok) {
throw new CustomError(`Error ${res.status}: ${res.statusText}`);
}
dispatch(changeTitle('You are logged-in'));
return res.json();
},
);
const initial: State = {
title: 'Please login',
userToken: {
token: '',
},
};
const reducer = reducerWithInitialState(initial)
.case(changeTitle, (state, title) => ({
...state,
title,
}))
.case(login.async.started, (state) => ({
...state,
loggingIn: true,
error: undefined,
}))
.case(login.async.failed, (state, { error }) => ({
...state,
loggingIn: false,
error,
}))
.case(login.async.done, (state, { result: userToken }) => ({
...state,
userToken,
loggingIn: false,
error: undefined,
}));
(async () => {
const thunk: ThunkMiddleware<State, AnyAction> = thunkMiddleware;
const store = createStore(reducer, applyMiddleware(thunk));
console.log('Initial state:', store.getState().title);
try {
await store.dispatch(login({ email: 'eve.holt@reqres.in', password: 'cityslicka' }));
const { title, userToken } = store.getState();
console.log('Logged in state:', title, userToken);
} catch (err) {
console.error('Login failed:', err);
}
})();