0.2.0 • Published 5 years ago

mini-actions v0.2.0

Weekly downloads
2
License
MIT
Repository
github
Last release
5 years ago

mini-actions

Helper utilities for a typesafe Redux experience.

CircleCI codecov dependencies Status devDependencies Status

Installation

$ npm install mini-actions

Usage

Create actions and reducers which respond to specified actions via the createAction, createReducer, and combineReducer functions.

import * as actions from "mini-actions";

const moveX = actions.createAction("move x", (value: number) => value);
const moveY = actions.createAction("move y", (value: number) => value);
const unrelatedAction = actions.createAction("this can't be passed to the reducer");

const posX = actions.createReducer(0).when(moveX, (state, { payload }) => state + payload);
const posY = actions.createReducer(0).when(moveY, (state, { payload }) => state + payload);
const position = actions.combineReducers({ x: posX, y: posY });

const result = position({ x: 3, y: 4 }, moveX(5)); // => { x: 8, y: 4 };
// the following line will yield a type error
// const result = position({ x: 0, y: 0}, unrelatedAction());

Additionally, the when and otherwise helpers will create well-typed functions which respond to specific actions created via this library. This may be useful in custom middlewares, which often translate actions into imperative behavior.

import { createAction, handle } from "mini-actions";
/* global store */

const apiSuccess = createAction("api success", (response: APIResponse) => response);
const performAPIRequest = createAction(
    "api request",
    (url: URL, headers?: Headers) => ({ url, headers }),
    () => ({
        onSuccess: apiSuccess,
    }));
const handler = handle.when(performAPIRequest, action => {
    fetch(action.payload.url.toString(), { headers: action.payload.headers }).
        then(response => response.json()).
        then(json => store.dispatch(action.meta.onSuccess(json)));
});

handler(performAPIRequest(new URL("https://api.thecatapi.com/v1/images/search?size=full")));

API

createAction

function createAction<Type extends string, Payload, Metadata, Args = [Payload]>(
    type: Type,
    toPayload?: (...args: Args) => Payload,
    toMeta?: (...args: Args) => Metadata
): ActionCreator<Type, Payload, Metadata, Args>;

Creates a new ActionCreator with the associated type and payload + metadata definitions. The ActionCreator is an enhanced function meant to be used with createReducer and handle to ensure types are correctly propagated through your reducers and action handlers.

The type argument must be a string literal, and cannot be correctly inferred at compile time unless a type cast is used.

Both toPayload and toMeta are optional, and if omitted, will result in the created actions not having the respective properties payload and meta. Additionally, these arguments must take the same arguments to ensure the action creator signature stays consistent.

Examples

export const moveBy = createAction("my action type", (x: number, y: number) => ({ x, y }));

const action = moveBy(1, 2); // { type: "my action type", payload: { x: 1, y: 2 }}
store.dispatch(action);

export const timeTrackedAction = createAction("the metadata is important", null, () => ({ time: Date.now() }));
const action = timeTrackedAction(); // { type: "the metadata is important", meta: { time: 1234567890} }

// incorrect action definition
const prefix = "@my-actions";
export const userDidSomething(`${prefix}/user did something`); // Type is inferred to `string` instead of the literal "@my-actions/user did something"

createReducer

function createReducer<State>(initialState: State): ReducerCreator<S, never>;

Creates a new reducer seeded with the provided initial state, which is used when the reducer receives an undefined first argument. This reducer handles no actions; to define transformations predicated on the actions received, use the when method of ReducerCreator.

combineReducers

function combineReducers<ReducerMap extends { [property: string]: Reducer }>(
    reducers: ReducerMap,
): Reducer<{ [property in keyof ReducerMap]: ReturnType<Reducer[property]>}, Parameters<ReducerMap[keyof ReducerMap]>[1]>

Combines the provided reducers into a tree, where the shape is defined by the key:value mapping provided in the call to combineReducers and the set of actions handled is the union of all actions handled by each reducer in the mapping. Similar to the combineReducers function provided by Redux, with modifications to make use of the enhanced action creators and reducers for type safety.

Usage

import { createReducer, createAction } from "mini-actions";

const moveX = createAction("move x", (value: number) => value);
const moveY = createAction("move y", (value: number) => value);
const unrelatedAction = createAction("unrelated");

const xReducer: Reducer<number, ReturnType<moveX>> =
    createReducer(0).when(moveX, (state, { payload }) => state + payload);
const yReducer: Reducer<number, ReturnType<moveY>> =
    createReducer(0).when(moveY, (state, { payload }) => state + payload);
const positionReducer: Reducer<{x: number, y: number}, ReturnType<moveX> | ReturnType<moveY>> = combineReducers({
    x: xReducer,
    y: yReducer,
});

const moved = [moveX(3), moveY(4)].reduce(positionReducer, undefined); // { x: 3, y: 4 }
// the following line is a compile-time error because none of the individual reducers
// define a transform for the provided action
// const _ = positionReducer(moved, unrelatedAction);

handle

export const handle = {
    when: <Type extends string, Payload, Metadata, ReturnValue>(
        actionCreator: ActionCreator<Type, Payload, Metadata, any>,
        fn: (action: Action<Type, Payload, Metadata>) => ReturnValue,
    ) => ActionHandler<((action: Action<Type, Payload, Metadata>) => ReturnValue), never>;
    otherwise: <ReturnValue>(
        fn: (action: Action<string, unknown, unknown>) => ReturnValue,
    ) => ActionHandler<never, ((action: Action<string, unknown, unknown>) => ReturnValue)>;
};

The handle object allows defining custom functions which use the enhanced action creators to create a type-safe function accepting a union of any of the provided actions, and returns the result of those functions. At this time, only functions of arity 1 are able to be created.

While the top level object itself only provides helper methods for creating basic functions capable of handling a single action, the returned ActionHandler type offers the same methods to build a function capable of handling multiple actions.

The when method adds a specific Action handler that only runs when that Action is received, and the parameterized return type of this function is added to the set of overloads on the returned function.

The otherwise method replaces the existing fallback action handler, which receives any action not specifically handled with a generalized string for its type field and unknown for both the payload and meta field.

Like the when method on ReducerCreators, these methods return new functions with the provided signature, so that logic may be shared between various handlers.

Usage

import { createAction, handle } from "mini-actions";

const trackThisAction = createAction("user clicked a button", (button: HTMLButtonElement) => button.getAttribute("id"));
const otherAction = createAction("this affects a different part of your program");

const handler = handle.when(trackThisAction, ({ payload }) => {
    navigator.sendBeacon("https://your.telemetry.endpoint", {
        event: "button click",
        data: {
            id: payload.id,
        },
    });

    return true;
}).otherwise(() => false);

const didTrack = handler(trackThisAction(document.getElementById("form-submit"))); // true
const didntTrack = handler(otherAction()); // false

ReducerCreator.when

declare reducerCreator: ReducerCreator<State, Actions>;

reducerCreator.when = <Type extends string, Payload, Metadata>(
    actionCreator: ActionCreator<Type, Payload, Metadata, any>,
    reducer: Reducer<State, Action<Type, Payload, Metadata>>,
): ReducerCreator<State, Actions | Action<Type, Payload, Metadata>>;

Defines a new transformation applied when the reducer creator is passed actions created by the provided action creator. This operation returns a new reducer creator without in-place modification of the original object, allowing reducers to share logic without interfering with other definitions.

Usage

import { createAction, createReducer } from "mini-actions";

const moveX = createAction("move x", (by: number) => by);
const moveY = createAction("move y", (by: number) => by);
const unhandled = createAction("a different action");
const reducer = createReducer({ x: 0, y: 0}).
    when(moveX, (state, { payload }) => ({ x: state.x + payload, y: state.y })).
    when(moveY, (state, { payload }) => ({ x: state.x, y: state.y + payload }));

const movedX = reducer(undefined, moveX(3)); // { x: 3, y: 0 }
const movedY = reducer(movedX, moveY(-4)); // { x: 3, y: -4 }
// the following line is a compile-time error because this reducer does not have
// a transformation defined for the provided action
// const _ = reducer(movedY, unhandled());