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

atomic-reducer

v0.2.0

Published

Encapsulates reducer design, reducing boilerplate

Downloads

1

Readme

⚛ Atomic Reducer

Build Status JavaScript Style Guide

Atomic Reducer encapsulates a common reducer pattern, reducing the amount of boilerplate you need to write.

Introduction

An Atomic Reducer looks like this:

{
  entities: {},
  order: [],
  selected: null,
  loading: false,
  error: null
}

The idea is that this reducer is the smallest atmoic state unit, to be composed with other atomic reducers to build more complex reducers which might handle multiple data types. The advantage of this is that each atomic reducer manages the data for a single entity, thereby avoiding bloated reducer logic.

Quick start

Install with npm or yarn.

npm install atomic-reducer
yarn add atomic-reducer

Define atomic reducer's in your project.

// github/reducer.js

// import `createReducer` into your reducer file
import { combineReducers } from 'redux'
import createReducer from 'atomic-reducer'

import * as actionTypes from './actions'

// Define each atomic reducer by passing action types
const username = createReducer(
  actionTypes.GET_USERNAMES_REQUEST,
  actionTypes.GET_USERNAMES_SUCCESS,
  actionTypes.GET_USERNAMES_FAILURE,
  actionTypes.SET_USERNAMES_ORDER,
  actionTypes.SET_USERNAMES_SELECTED
)

const repo = createReducer(
  actionTypes.GET_REPOS_REQUEST,
  actionTypes.GET_REPOS_SUCCESS,
  actionTypes.GET_REPOS_FAILURE,
  actionTypes.SET_REPOS_ORDER,
  actionTypes.SET_REPOS_SELECTED
)

// optionally compose them together using redux's `combineReducers`
export default combineReducers({ username, repo })

This file exports a reducer with the following shape.

{
  username: {
    entities: {},
    order: [],
    selected: null,
    loading: false,
    error: null
  },
  repo: {
    entities: {},
    order: [],
    selected: null,
    loading: false,
    error: null
  }
}

Dispatching the corresponding actions will update the relevent part of the state, taking data from action.payload. See usage for more information on the reducer logic and action format.

Why would I use this?

It's common for a reducer to manage several entity types. For example a GitHub reducer might manage a state like this:

// github reducer initial state
{
  data: {
    repos: {
      enitites: {},
      order: [],
      loading: false
    },
    usernames: {
      entities: {},
      order: [],
      loading: false
    },
    tags: {
      entities: {},
      order: [],
      loading: false
    },
    // ...maybe more, e.g. commits, followers...
  },
  loading: false,
  error: null
}

This can lead to bloated reducers, which have to deal with repeated logic across all these data types. As a result, it's not uncommon to see logic like this:

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case GET_REPOS_REQUEST:
      return {
        ...state,
        data: {
          ...state.data,
          repos: {
            ...state.data.repos,
            loading: true
          }
        }
      }
    case GET_REPOS_SUCCESS:
      return {
        ...state,
        data: {
          ...state.data,
          repos: {
            ...state.data.repos,
            entities: action.payload,
            loading: false
          }
        }
      }
    case GET_REPOS_FAILURE:
      return { /* ... */ }

    // ...and so on for each entity type...
    default: return state
  }
}

As you can imagine, this can easily lead to repetition. Furthermore, it becomes painful to manage nested state (10 lines just to set loading: true 😵).

With Atomic Reducer, the above can be re-written as:

import { combineReducers } from 'redux'
import createReducer from 'atomic-reducer'

const repos = createReducer(
  'GET_REPOS_REQUEST',
  'GET_REPOS_SUCCESS',
  'GET_REPOS_FAILURE'
)
const usernames = createReducer(
  'GET_USERNAMES_REQUEST',
  'GET_USERNAMES_SUCCESS',
  'GET_USERNAMES_FAILURE'
)
const tags = createReducer(
  'GET_TAGS_REQUEST',
  'GET_TAGS_SUCCESS',
  'GET_TAGS_FAILURE'
)

export default combineReducers({ repos, usernames, tags })

The logic for setting loading, error, entities, order and selected is all built in, we just need to pass in the action types.

Usage

Creating an atomic reducer

To create an atomic reducer, just pass the actions you want to use for that entity type to createReducer. You don't need to pass them all in, but you need to provide at least one (otherwise there won't be anyway to dispatch to the reducer).

As a list of arguments:

const reducer = createReducer(
  'REQUEST_ACTION',
  'SUCCESS_ACTION',
  'FAILURE_ACTION',
  'SET_ORDER_ACTION',
  'SET_SELECTED_ACTION'
)

You can also provide actions as an array of strings, with the same constraints as above:

const reducer = createReducer([
  'REQUEST_ACTION',
  'SUCCESS_ACTION',
  'FAILURE_ACTION',
  'SET_ORDER_ACTION',
  'SET_SELECTED_ACTION'
])

Finally, you can pass actions as an object, this is useful if you only want to provide actions for, say, setting the order. You must provide at least one of the following keys (other keys are ignored).

const reducer = createReducer({
  request: 'REQUEST_ACTION',
  success: 'SUCCESS_ACTION',
  failure: 'FAILURE_ACTION',
  setOrder: 'SET_ORDER_ACTION',
  setSelected: 'SET_SELECTED_ACTION'
})

Reducer logic

Each atomic reducer responds to the following events

| Event Name | Description | Expected action.payload | | ---------- | ----------- | ------------------------- | | request | Sets loading to true | - | | success | Merge action.payload with entities, sets loading to false | object<Key, Value> | | failure | Sets error to action.payload, sets loading to false | Error | | setOrder | Sets order to action.payload | array<Key> | | setSelected | Sets selected to action.payload | <Key> |

The exact implementation for each case is as follows.

request

No data from action.

case request:
  return {
    ...state,
    loading: true
  }

success

action.payload is merged with state.entities. Data is merged to prevent data loss.

case success:
  return {
    ...state,
    entities: {
      ...state.entities,
      ...action.payload
    },
    loading: false
  }

failure

action.payload is used to populate state.error.

case failure:
  return {
    ...state,
    loading: false,
    error: action.payload
  }

setOrder

action.payload sets the state.order.

case setOrder:
  return {
    ...state,
    order: action.payload
  }

setSelected

action.payload sets state.selected.

case setSelected:
  return {
    ...state,
    selected: action.payload
  }

Customisation

You can provide a custom configuration to generate a bespoke createReducer function.

Currently, the only supported configuration option is logic, which lets you define custom reducer logic for each event type.

logic

An object mapping events to a function that accepts the current state and should return the new state. See the redux documentation for good reducer practices.

Logic key names are:

  • request
  • success
  • failure
  • setOrder
  • setSelected
import { configureCreateReducer } from 'atomic-reducer'

const customCreateReducer = configureCreateReducer({
  logic: {
    setOrder: (state, action) => ({
      ...state,
      order: [...action.payload, ...state.order]
    })
  }
})

// use customCreateReducer instead of createReducer...

FAQ

Q. I want to set entities and order at the same time

A. This is a common pattern, particuarly when using normalizr where the entities and their order are returned together. With atomic reducer you'd need to do this in two actions.

For example:

const getUsers = () => (dispatch) => {
  dispatch({ type: 'GET_USERS_REQUEST' })
  return api({ url: '/users' })
    .then(res => normalize(res.data, [ user ]))
    .then(({ result, entities }) => {
      // (1) dispatch normalised data
      dispatch({
        type: 'GET_USERS_SUCCESS',
        payload: entities.users
      })
      // (2) dispatch order
      dispatch({
        type: 'SET_USERS_ORDER',
        payload: result
      })
    })
}

Although dispatching two actions might seem like more work, it is more explicit and gives you greater flexibility in general. If you hate doing this, then this pattern can easily be extracted to a helper function which dispatches the two actions for you in one call.

Next steps

TODO

Contributing

If you'd like to contribute then thats great! Please make a pull request against the master branch, and include as much detail about what your PR does as you can. PRs should add or update tests as necessary.