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

thunkless

v1.0.0-beta.8

Published

Thunkless middleware for Redux

Downloads

346

Readme

thunkless

Simple Redux middleware for async actions without thunks.

NPM Version NPM Downloads Build Status Coverage Status License: MIT

$ npm install thunkless

What is it

thunkless is a lightweight Redux middleware library for writing async actions with a simple declarative API. It is targeted at Redux apps that use the standard async action flow: dispatch start action to signal beginning of an event, wait till the promise is resolved or rejected, optionally dispatch more actions, i.e. side effects, finally dispatch a success or a failure action. thunkless supports blocking actions - preventing an action from being dispatched until another action is completed.

Motivation (aka What's wrong with Thunk)

Redux Thunk provides a dead simple approach to asynchronous actions. Its simplicity (11 lines of code) makes it a very nice solution for small-sized apps. Its 100% imperative API, however, results in a fair amount of custom logic inside of thunks, and that becomes a pain point for bigger-sized apps (>10-15 async actions). To ensure bug-free behavior, all of these actions need to be tested for every scenario, and debugging them can be tedious when logic becomes more complex. The key insight about this is that in most cases these actions follow a similar pattern: dispatch a start action -> resolve a promise -> dispatch side effects -> dispatch a success action, or catch an error and dispatch actions for failure scenarios. This results in a lot of logic duplication. thunkless abstracts this logic away and provides a simple declarative API for this common asynchronous pattern.

Usage

The simplest (not the most useful) usage example:

// Action
const confirmIdentity = name => ({
  promise: Promise.resolve('Valar dohaeris'),
  type: CONFIRM_IDENTITY,
  payload: 'Valar morghulis',
  meta: { name },
});

// Reducer
const reducer = (state, action) => {
  if (action.type !== CONFIRM_IDENTITY) return state;

  const { payload, meta: { name } } = action;
  if (payload === 'Valar morghulis') {
    return {
      ...state,
      [name]: { status: `Confirming ${name}'s identity...` },
    };
  } else if (payload === 'Valar dohaeris') {
    return {
      ...state,
      [name]: { status: `${name}'s identity has been confirmed.` },
    };
  } else {
    return {
      ...state,
      [name]: { status: `${name} is an impostor.` },
    };
  }
}

thunkless will first send CONFIRM_IDENTITY action with payload 'Valar morghulis', wait until the promise resolves and send another CONFIRM_IDENTITY action with the result of the promise ('Valar dohaeris'). Note that the actions will be sent down the middleware chain via next() function call instead of getting dispatched, so any middleware placed before thunkless WILL NOT process the action.

A more real-world example - authentication flow:

// Action
const login = (email, password) => ({
  /**
   * If the value of promise is an async function (or a function that returns a promise)
   * and statusSelector is supplied, the promise will only be created if statusSelector
   * does not return false or thunkless.ActionStatus.BUSY. Otherwise, the action will be blocked.
   */
  promise: () => sendLoginRequest(email, password),
  type: [ // Separate action type for start, success, and failure, is a good common practice.
    START_AUTH,
    AUTH_SUCCESS,
    AUTH_FAILURE,
  ],
  // statusSelector is required if a duplicate action should be blocked.
  statusSelector: state => state.auth.loginStatus,
  /**
   * Actions in chain will be dispatched if the promise is successfully resolved.
   * If one of them results in error, AUTH_FAILURE will be sent. Otherwise,
   * thunkless will send AUTH_SUCCESS after dispatching chain actions.
   */
  chain: ({ userData, isReturningUser }) => [
    { type: INIT_USER, payload: userData },
    isReturningUser && { type: SHOW_MESSAGE, payload: 'Welcome back!' },
  ],
  /**
   * Action with type SHOW_ERROR will be dispatched on error with error object and
   * this login action instance in its payload.
   */
  dispatchOnError: SHOW_ERROR,
  meta: { email },
});

// Reducers
import { ActionStatus } from 'thunkless';

const { BUSY, SUCCESS, FAILURE } = ActionStatus;

const authReducer = (state, action) => {
  switch (action.type) {
    case START_AUTH: return {
      ...state,
      loginStatus: BUSY
    }

    case AUTH_SUCCESS: return {
      ...state,
      loginStatus: SUCCESS,
      username: action.payload.userData.username,
    }

    case AUTH_FAILURE: return {
      ...state,
      loginStatus: FAILURE,
    }

    default: return state;
  }
}

const userReducer = (state, action) => {
  if (action.type !== INIT_USER) return state;

  const { payload: { userData } } = action;
  return {
    ...state,
    [userData.username]: userData,
  };
}

const errorReducer = (state, action) => {
  if (action.type !== SHOW_ERROR) return state;

  const { payload: { error, origin } } = action;

  if (error.message === 'Email Not Found') {
    const { meta: { email } } = origin;
    return {
      ...state,
      message: `User with email ${email} does not exist.`,
    }
  }

  return {
    ...state,
    message: error.message
  };
}

When to choose thunkless

thunkless is useful for medium- and big-sized apps that have many asynchronous actions that follow the same common pattern.

thunkless allows to block an action from being dispatched when another action is in progress. It does not, however, restore the state after an action has failed (e.g. an asynchronous action in the chain of another action). A library like redux-optimistic-ui can be used together with thunkless to enable that functionality.

thunkless is not a suitable solution for apps that need advanced action queueing capabilities or complex side effect patterns (yet even big-sized apps don't typically need them). If those are a must, Redux-Saga is an excellent choice.

Installation

npm install thunkless

Use applyMiddleware() to enable:

import { createStore, applyMiddleware } from 'redux';
import { middleware } from 'thunkless';
import rootReducer from './reducers/index';

const store = createStore(
  rootReducer,
  applyMiddleware(middleware)
);

TypeScript

createThunklessAction is just an identity function that helps to strongly type action objects in TypeScript.

import { createThunklessAction } from 'thunkless';

const login = (email, password) => createThunklessAction({
  promise: sendLoginRequest(email, password),
  type: [
    LOGIN_REQUEST,
    LOGIN_SUCCESS,
    LOGIN_FAILURE,
  ] as const,
  /**
   * Chain function parameter type will be inferred
   * from the promise result type.
   */
  chain: ({ userData, isReturningUser }) => [
    { type: INIT_USER, payload: userData },
    isReturningUser && { type: SHOW_MESSAGE, payload: 'Welcome back!' },
  ],
});

ReducibleThunklessAction type helper can be used to resolve action type properly in the reducer. Example usage:

import type { ReducibleThunklessAction } from 'thunkless';
import type { login, signup } from '../actions/auth';

const authReducer = (state, action: ReducibleThunklessAction<typeof login>|ReducibleThunklessAction<typeof signup>) => {
  switch (action.type) {
    case AUTH_SUCCESS: return {
      ...state,
      // username type will be inferred correctly
      username: action.payload.userData.username,
    }
  }
}

License

MIT