Search code examples
redux-toolkit

Middleware to add time to all actions


Using Redux Toolkit, I have many action creators that have prepare callbacks adding a unixtime property to the payload. This property gets the Date.now() value. Since I'm continually adding more actions requiring a time, I'm thinking I'll instead add middleware like the following:

import { Middleware } from 'redux';
import { RootState } from '../sliceReducer'; // type of my app's Redux state

const unixtimeMiddleware: Middleware<{}, RootState> = () => (next) => (action) => {
    if (action && !action.meta) {
        action.meta = {};
    }
    action.meta.unixtime = Date.now();
    return next(action);
};

export default unixtimeMiddleware;

My question is:

  1. Is this the right type for the middleware?
  2. Possibly related to (1), is there a way to automatically have meta: { unixtime: number } added to all RTK action types, or do I need to create types that extend the built-in action types like:
import { PayloadAction } from '@reduxjs/toolkit';

/**
 * An extension of Redux Toolkit's PayloadAction which includes a .meta.unixtime property.
 *
 * @template P The type of the action's payload.
 * @template M Union to the action's base meta type { unixtime: number } (optional)
 * @template E The type of the action's error (optional)
 *
 * @public
 */
export type PayloadActionUnixtime<P, M = never, E = never> = PayloadAction<
    P,
    string,
    { unixtime: number } & ([M] extends [never] ? {} : M),
    E
>;

(Side note: I chose the name unixtime over something like timestamp to reduce the likelihood someone would think it is a string like YYYY-MM-DD HH-MM-SS)


Solution

  • This is what I've done for over a year:

    import { AnyAction, Middleware } from 'redux';
    import type { RootState } from '../index';
    
    type MetaProp = {
        [moreProps: string]: unknown;
    };
    type TimedMetaProp = MetaProp & { unixtime: number };
    
    // eslint-disable-next-line @typescript-eslint/ban-types
    export const actionAddMetaUnixtime = <T extends {}>(
        action: T
    ): T & { meta: TimedMetaProp } => {
        const unixtime = Date.now();
        const meta: TimedMetaProp = {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            ...(action && 'meta' in action ? (action as any).meta : {}),
            unixtime,
        };
        return {
            ...action,
            meta,
        };
    };
    
    // eslint-disable-next-line @typescript-eslint/ban-types
    const unixtimeMiddleware: Middleware<{}, RootState> =
        () => (next) => (action: AnyAction) => {
            const newAction =
                typeof action === 'function'
                    ? action // do nothing for thunk
                    : actionAddMetaUnixtime(action);
            return next(newAction);
        };
    
    export default unixtimeMiddleware;
    

    Then in reducers where I want to ensure that unixtime is set, I do:

    export const doFoo = (
        state: RootState,
        action: PayloadAction<FooProps>
    ): void => {
        assertActionHasUnixtime(action);
        // ... do foo stuff ...
    };
    

    Where assertActionHasUnixtime is defined as:

    import type { PayloadAction } from '@reduxjs/toolkit';
    
    /**
     * Assertion function that ensures that an action has `.meta.unixtime` present. All actions should
     * have this, since it is added by Redux middleware. This function ensures type-safety for reducers.
     *
     * Alternatively we could use an action type other than PayloadAction which includes meta.unixtime,
     * and that was done prior to enabling TypeScript `strictFunctionTypes`. However, with TypeScript
     * `strictFunctionTypes` this failed type checking. See the following Discord discussion with the
     * Redux/Redux-Toolkit maintainers where this assert method was suggested:
     *
     * https://discord.com/channels/102860784329052160/103538784460615680/1117937690164342824
     */
    export default function assertActionHasUnixtime(
        action: Record<string, unknown>
    ): asserts action is { meta: { unixtime: number } } {
        if (
            typeof action.meta === 'object' &&
            !!action.meta &&
            typeof (action.meta as Record<string, unknown>).unixtime === 'number'
        ) {
            return;
        }
        throw new Error(
            'missing .meta.unixtime in action: ' + JSON.stringify(action)
        );
    }