npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@emab/maction

v1.0.1

Published

A set of utility functions aimed to help you create well typed actions in React for `redux`. This also helps to remove boilerplate code and keep your store and actions typed without having to completely restructure.

Downloads

3

Readme

Maction

A set of utility functions aimed to help you create well typed actions in React for redux. This also helps to remove boilerplate code and keep your store and actions typed without having to completely restructure.

Inspiration for this comes from this blog post written by Lenz Weber, an author of redux-toolkit.

Usage

createMaction

Creates a matchable action creator.

import {createMaction} from "@emab/maction";

const increment = createMaction((value: number) => ({
    type: "INCREMENT",
    value
}))

That is it! 🎉

You don't need to define an interface or type for the action - or create an enum to record the action types. Instead, the action creator itself can be used to match the action in the reducer. It is important that the action type is unique as you'd expect (a function called createPrefix is provided to help with that).

// Component
dispatch(increment(10));

// Reducer
const reducer = (state: State, action: AnyAction): State => {
    // Use the action creator to match the action
    if (increment.matches(action)) {
        // This is now strongly typed
        return {...state, value: action.value}
    }

    return state;
}

createPrefix

A simple utility function to create namespaced action types.

import {createMaction, createPrefix} from "@emab/maction";

const withPrefix = createPrefix("COUNTER_");

const increment = createMaction((value: number) => ({
    type: withPrefix("INCREMENT"),
    value
}))

increment(10).type === "COUNTER_INCREMENT"

matchableReducerFactory

An optional utility function that can help keep your split off reducers typed in line with actions.

It returns a function that takes a matchable action as its first parameter, and a reducer function as the second. If a generic for state has been provided, you should get full type safety here. This means if you change your action creator you'll get TS warnings here without having to go digging too far!

State type should be provided as a generic argument to the factory.

import {matchableReducerFactory} from "@emab/maction";

const reduceForAction = matchableReducerFactory<State>();

// Here `increment` was created using `createMaction`
// The types for `state` and `action` will be correctly typed here
const incrementReducer = reduceForAction(increment, (state, action) => ({
    ...state,
    value: action.value
}))

const reducer = (state: State, action: AnyAction): State => {
    if (increment.matches(action)) {
        return incrementReducer(state, action);
    }
}

Comparing boilerplate

Let's look at how this can replace your existing actions. This format may be familiar:

// actions.ts
export enum ActionType {
    INCREMENT = "INCREMENT",
    DECREMENT = "DECREMENT",
    RESET = "RESET"
}

type IncrementAction = {
    type: ActionType.INCREMENT,
    value: number
}

export const incrementAction = (value: number): IncrementAction => ({
    type: ActionType.INCREMENT,
    value
})

type DecrementAction = {
    type: ActionType.DECREMENT,
    value: number
}

export const decrementAction = (value: number): DecrementAction => ({
    type: ActionType.DECREMENT,
    value
})

type ResetAction = {
    type: ActionType.RESET
}

const resetAction = (): ResetAction => ({
    type: ActionType.RESET
})

export type Action = IncrementAction | DecrementAction | ResetAction;

// reducer.ts
const incrementReducer = (state: State, action: IncrementAction): State => ({
    ...state,
    value: state.value + action.value
})

const decrementReducer = (state: State, action: DecrementAction): State => ({
    ...state,
    value: state.value - action.value
})

const reducer = (state: State, action: Action): State => {
    switch (action.type) {
        case ActionType.INCREMENT:
            return incrementReducer(state, action);
        case ActionType.DECREMENT:
        case ActionType.RESET:
            return {
                ...state,
                value: 0
            }
        default:
            return state;
    }
} 

I've used this pattern many times - it feels safe and familiar. There is a lot of boilerplate code (looking at you, actions.ts!) but it comes with type safety, and you deal with each action specifically. As pointed out in the blog post mentioned earlier, this isn't really a great approach as we've typed action as Action in our reducer despite the fact that anything could be sent as an action, not just actions from the union type we defined.

Now the same code using maction:

// actions.ts
import {createMaction} from "@emab/maction";

export const incrementAction = createMaction((value: number) => ({
    type: "INCREMENT",
    value
}))

export const decrementAction = createMaction((value: number) => ({
    type: "DECREMENT",
    value
}))

export const resetAction = createMaction(() => ({
    type: "RESET"
}));

// reducer.ts
import {matchableReducerFactory} from "maction";
import {AnyAction} from "redux";

const reduceForAction = matchableReducerFactory<State>();

const incrementReducer = reduceForAction(incrementAction, (state, action) => ({
    ...state,
    value: state.value + action.value
}))

const decrementReducer = reduceForAction(decrementAction, (state, action) => ({
    ...state,
    value: state.value - action.value
}))

const reducer = (state: State, action: AnyAction): State => {
    if (incrementAction.matches(action)) {
        return incrementReducer(state, action);
    }
    if (decrementAction.matches(action)) {
        return decrementReducer(state, action);
    }
    if (resetAction.matches(action)) {
        return {
            ...state, value: 0
        }
    }

    return state;
} 

Overall I think this pattern is much neater. You avoid a tonne of boilerplate, and it's much easier to add and remove actions.