I have a TypeScript project that uses React and Redux and I'm trying to add some middleware functions. I got started by implementing one from Redux's samples like so:
// ---- middleware.ts ----
export type MiddlewareFunction = (store: any) => (next: any) => (action: any) => any;
export class MyMiddleWare {
public static Logger: MiddlewareFunction = store => next => action => {
// Do stuff
return next(action);
}
}
// ---- main.ts ----
import * as MyMiddleware from "./middleware";
const createStoreWithMiddleware = Redux.applyMiddleware(MyMiddleWare.Logger)(Redux.createStore);
The above works just fine but since this is TypeScript I'd like to make it strongly-typed, ideally using the types defined by Redux so I don't have to reinvent and maintain my own. So, here are the relevant excerpts from my index.d.ts file for Redux:
// ---- index.d.ts from Redux ----
export interface Action {
type: any;
}
export interface Dispatch<S> {
<A extends Action>(action: A): A;
}
export interface MiddlewareAPI<S> {
dispatch: Dispatch<S>;
getState(): S;
}
export interface Middleware {
<S>(api: MiddlewareAPI<S>): (next: Dispatch<S>) => Dispatch<S>;
}
I'm trying to figure out how to bring those types into my Logger method but I'm not having much luck. It seems to me that something like this ought to work:
interface MyStore {
thing: string;
item: number;
}
interface MyAction extends Action {
note: string;
}
export class MyMiddleWare {
public static Logger: Middleware = (api: MiddlewareAPI<MyStore>) => (next: Dispatch<MyStore>) => (action: MyAction) => {
const currentState: MyStore = api.getState();
const newNote: string = action.note;
// Do stuff
return next(action);
};
}
but instead I get this error:
Error TS2322: Type '(api: MiddlewareAPI) => (next: Dispatch) => (action: Action) => Action' is not assignable to type 'Middleware'.
Types of parameters 'api' and 'api' are incompatible.
Type 'MiddlewareAPI' is not assignable to type 'MiddlewareAPI'.
Type 'S' is not assignable to type 'MyStore'.
I see the <S> generic declared in the type definition, but I've tried a lot of different combinations and I can't seem to figure out how to specify it as MyStore so that it is recognized as the generic type in the rest of the declarations. For example, according to the declaration api.getState() should return a MyStore object. Same thinking applies to the action type <A>, of course.
To apply a middleware in redux, we would need to require the applyMiddleware function from the redux library. import {createStore, applyMiddleware} from "redux"; In order to check that our middleware is hooked up correctly, we can start by adding a log to display a message when an action is dispatched.
As of React-Redux v8, React-Redux is fully written in TypeScript, and the types are included in the published package. The types also export some helpers to make it easier to write typesafe interfaces between your Redux store and your React components.
MyStore is not required.
export const Logger: Middleware = (api: MiddlewareAPI<void>) => (next: Dispatch<void>) => <A extends Action>(action: A) => { // Do stuff return next(action); };
or
export const Logger: Middleware = api => next => action => { // Do stuff return next(action); };
Have a Nice Dev
Here is my solution:
First is the middleware creator that accepts a todo function as input which is run as core logic for the middleware. The todo function accepts an object that encapsulates store(MiddlewareAPI<S>)
, next(Dispatch<S>)
, action(Action<S>)
as well as any other your custimized parameters.
Please be aware that I use as Middleware
to force the middleware creator to return a Middleware. This is the magic I use to get rid of the trouble.
import { MiddlewareAPI, Dispatch, Middleware } from 'redux';
import { Action } from 'redux-actions';
export interface MiddlewareTodoParams<S> {
store: MiddlewareAPI<S>;
next: Dispatch<S>;
action: Action<S>;
[otherProperty: string]: {};
}
export interface MiddlewareTodo<S> {
(params: MiddlewareTodoParams<S>): Action<S>;
}
// <S>(api: MiddlewareAPI<S>): (next: Dispatch<S>) => Dispatch<S>;
export const createMiddleware = <S>(
todo: MiddlewareTodo<S>,
...args: {}[]
): Middleware => {
return ((store: MiddlewareAPI<S>) => {
return (next: Dispatch<S>) => {
return action => {
console.log(store.getState(), action.type);
return todo({ store, next, action, ...args });
};
};
// Use as Middleware to force the result to be Middleware
}) as Middleware;
};
Second part is the definition of my todo function. In this example I write some token into cookie. It is just a POC for Middleware so I don't care about the XSS risk in my codes at all.
export type OAUTH2Token = {
header: {
alg: string;
typ: string;
};
payload?: {
sub: string;
name: string;
admin: boolean;
};
};
export const saveToken2Cookie: MiddlewareTodo<OAUTH2Token> = params => {
const { action, next } = params;
if (action.type === AUTH_UPDATE_COOKIE && action.payload !== undefined) {
cookie_set('token', JSON.stringify(action.payload));
}
return next(action);
};
Lastly, here is what it looks of my store configruation.
const store: Store<{}> = createStore(
rootReducer,
// applyMiddleware(thunk, oauth2TokenMiddleware(fetch))
applyMiddleware(thunk, createMiddleware<OAUTH2Token>(saveToken2Cookie))
);
I have a solution that goes like this:
export type StateType = { thing: string, item: number };
export type ActionType =
{ type: "MY_ACTION", note: string } |
{ type: "PUSH_ACTIVITIY", activity: string };
// Force cast of generic S to my StateType
// tslint:disable-next-line:no-any
function isApi<M>(m: any): m is MiddlewareAPI<StateType> {
return true;
}
export type MiddlewareFunction =
(api: MiddlewareAPI<StateType>, next: (action: ActionType) => ActionType, action: ActionType) => ActionType;
export function handleAction(f: MiddlewareFunction): Middleware {
return <S>(api: MiddlewareAPI<S>) => next => action => {
if (isApi(api)) {
// Force cast of generic A to my ActionType
const _action = (<ActionType>action);
const _next: (action: ActionType) => ActionType = a => {
// Force cast my ActionType to generic A
// tslint:disable-next-line:no-any
return next(<any>a);
};
// Force cast my ActionType to generic A
// tslint:disable-next-line:no-any
return f(api, _next, _action) as any;
} else {
return next(action);
}
};
}
With the handeAction
function I can now define middlewares:
// Log actions and state.thing before and after action dispatching
export function loggingMiddleware(): Middleware {
return handleAction((api, next, action) => {
console.log(" \nBEGIN ACTION DISPATCHING:");
console.log(`----- Action: ${JSON.stringify(action)}\n`);
const oldState = api.getState();
const retVal = next(action);
console.log(` \n----- Old thing: ${oldState.thing}`);
console.log(`----- New thing: ${api.getState().thing)}\n`);
console.log("END ACTION DISPATCHING\n");
return retVal;
});
}
// Another middleware...
export interface DataHub = { ... }:
export function dataHandlingMiddleware(datahub: DataHub): Middleware {
return handleAction((api, next, action) => {
switch (action.type) {
case "PUSH_ACTIVITY": {
handlePushActivities(action.activity, api, /* outer parameter */ datahub);
break;
}
default:
}
return next(action);
});
}
Please note that the middlewares can also require additional parameters like services etc (here: DataHub), that are passed in during setup. The store setup looks like this:
import {
Store, applyMiddleware, StoreCreator, StoreEnhancer,
createStore, combineReducers, Middleware, MiddlewareAPI
} from "redux";
const middlewares = [
dataHandlingMiddleware(datahub),
loggingMiddleware()];
const rootReducer = combineReducers<StateType>({ ... });
const initialState: StateType = {};
// Trick to enable Redux DevTools with TS: see https://www.npmjs.com/package/redux-ts
const devTool = (f: StoreCreator) => {
// tslint:disable-next-line:no-any
return ((window as any).__REDUX_DEVTOOLS_EXTENSION__) ? (window as any).__REDUX_DEVTOOLS_EXTENSION__ : f;
};
const middleware: StoreEnhancer<StateType> = applyMiddleware(...middlewares);
const store: Store<StateType> = middleware(devTool(createStore))(rootReducer, initialState);
Hope this helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With