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

horseshoes

v0.23.1

Published

A suite of helpers, and model definitions for a normalized Redux/Saga stack

Downloads

17

Readme

Horseshoes for Redux and Sagas

A suite of helpers, and model definitions for a normalized Redux and Saga stack.

Getting started

Note: This module is a work in progress and may frequently have breaking changes. It is, however, currently being used effectively by Unicorn across a myriad of applications (SPA, Electron, etc) so the library is stable enough for production use. Just note that while in alpha, you should use specific versions.

Install: yarn add horseshoes

WHY Horseshoes? CRUD operations with Redux in practice

Since redux is very open and un-opinionated, states can look very different between developers and projects. As such, we've aimed to normalize state with a specific schema defined by our Resources type. As an example, all of these are valid approaches for state management, but none of them offer both flexibility and rigidity without careful planning and type definitions:

Typical practice with Arrays: Below is a typical setup seen in many tutorials across the web. The reasoning behind it makes sense: javascript as a functional language has a lot of benefits. However, data should almost never be stored in any meaningful way as an Array. It's inefficient and despite the saving of verbosity later with method chaining, your reducers and actions suffer the consequences.

const rootState = {
  tasks: [
    { id: 0, name: 'my first task', complete: false },
    { id: 1, name: 'my second task', complete: false },
    { id: 2, name: 'my completed task', complete: true }
  ]
}

// This is approach is nice, because it's easy to filter and sort arrays
const completedTasks = (tasks: Task[]): Task[] => {
  return tasks.filter(t => t.complete)
}

// Updating states with this approach, however, is slow and verbose
const taskReducer = (state, { type, payload }) => {
  switch (type) {
    // Is this immutable? Depends on if you know about accidental mutations
    // Lots of room for bad practices here: indexOf, array mutations, push/pop
    // Not to mention, this is super inefficient when dealing with large arrays
    case TASK.UPDATE :
      let nextState = state.filter(t => t.id !== payload.id)
      nextState.push(payload)
      return nextState

    // Oh no! More mutations and side effects introduced that aren't obvious!
    case TASK.DELETE :
      return state.filter(t => t.id !== payload.id)

    default :
      return state
  }
}

Typical practice with an Object

const rootState = {
  tasks: {
    // Did you remember to set your id twice: s the object key, and in the Task payload?
    // If not, how do you pass around your id's in a meaningful way?
    'first': { id: 'first', name: 'my first task', complete: false },
    'second': { id: 'second', name: 'my second task', complete: false }
  }
}

// Is your `Tasks` interface an object vs the Task[] array you would like to return?
// Did you have to refactor your type definitions because of naming collisions?
const completedTasks = (tasks: Tasks): Task[] => {
  // Did you remember to polyfill Object.entries?
  // Did you remember to do a guard for hasOwnProperty?
  return Object.entries(tasks).map(taskArr => taskArr[1])
}

const taskReducer = (state, { type, payload }) => {
  let newState = { ...state }

  // Better than storing arrays, but still lots of room for accidental mutations
  // Did you bootstrap your config with source mapping support to trace down improper implementation?
  // How do you trace and duplicate errors that users encounter in production?
  switch (type) {
    case TASK.UPDATE :
      newState[payload.id] = payload
      break

    case TASK.DELETE :
      delete newState[payload.id]
      break

    default :
      return state
  }

  return newState
}

The Horseshoes approach

Taking the best from the above two approaches, we stored the data with normalization, sanity checks, and immutability. Leveraging immer and lodash along the way. Thus your data is stored like you would find in a typical database, but you have many helpers and methods at your disposal. Oh, and it's strongly typed the whole time. No more any!

Extend the Resources interface Horseshoes was designed to give you some rigidity while assuming a few small things like: isLoading, errors, and errorTrace. It also stores your data by keys (functional FTW) and data (OOP FTW) giving you a normalized state no matter what your programming paradigm.

import { Resources, Resource } from 'horseshoes'

interface Task extends Resource {
  name: string
  complete: boolean

  // inherited from Resource
  key: string
}

interface Tasks extends Resources<Task> {
  lastFetchedAt: Date

  // inherited from Resources, used to debug implementation errors, regardless of where you have a proper source map setup.
  error: boolean
  errorTrace: {
    [key: string]: string
  }
  // normalized state so that you can fetch keys or the entire object from storage and glue them any way you want
  keys: string[]
  data: {
    [key: string]: Task
  }
}

Init Resources from an option rehydrated state Note the typing and convenience of getting an initial or default state.

const rootState = {
  tasks: initResources<Task>(rehydratedTasks)
}

Typed actions Loose assumptions made with an optional payload? property for all CRUD actions.

const updateTask = (payload: Task): ResourceAction<Task> => {
  type: TASK.UPDATE,
  payload
}

Simpler Reducers with Resource CRUD helpers With the small assumptions made above and in the library, we can then offer up a standardized way of performing CRUD ops:

const defaultTask = {
  complete: false
}

const taskReducer = (state, { type, payload }) => {
  // One guard statement, immutability provided under the hood.
  if (!type || !payload) return state

  switch (type) {
    case TASK.UPDATE :
      // We can optionally pass any default properties to inherit from
      return updateResource<Task>(state, payload, defaultTask)

    case TASK.DELETE :
      return deleteResource<Task>(state, payload)

    default :
      return state
  }
}

toArray helper

import { toArray } from 'horseshoes'

const completedTasks = toArray<Task>(tasks).filter(t => t.complete) // returns Task[]

Read more

Check out the API Docs for now. We're still working on the project and documentation.