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

use-collection

v1.0.4

Published

Takes the pain out of managing stateful objects and arrays within your React components.

Downloads

1

Readme

🤹‍♂️ useCollection

Unit Tests NPM Package

Takes the pain out of modifying stateful objects and arrays within your React components.

Protip: Use Faker Enhanced to generate mock data for your useCollection hooks.

Table of Contents

Why useCollection?

Section coming soon...

Actions

Note that if you are using TypeScript you can pass the following generics:

  • I: The unique identifier type
  • O: the expected shape of the objects inside the collection (the specified identifier key is added automatically)

Add

  (
    newValues: O | O[], 
    target: number | 'start' | 'end' | { before: I } | { after: I }
  ) => void

Description coming soon...

Remove

  (
    target: number | { id: I } | (id: I, value: O) => void;
    amount?: number
  ) => void

Description coming soon...

Update

  (
    target: number | { id: I }, 
    changes: Partial<O>,
  ) => void;

Description coming soon...

Move

  (
    from: number | { id: I },
    to: number | { before: I } | { after: I }
  ) => void;

Description coming soon...

Reorder

  (
    command: { id: I, direction: 'ascending' | 'descending' } | (a: [I, O], b: [I, O]) => 0 | 1 | -1;
  ) => void

Description coming soon...

Replace

  (newValues:? O[]) => void;

Description coming soon...

Configuration

useCollection<I extends string | number | symbol, O extends Record<I, O>>(
  /**
   * The values that the collection starts with when intialisied.
   * 
   * @example [{ id: 'c', value: 30 }, { id: 'e', value: 50 }]
   * @example { c: { value: 30 }, e: { value: 50 }}
   * 
   * @default []
   */
  collection: O[] | Record<I, O>,

  /**
   * Additional configuration options that can 
   * (optionally) be passed to the collection during initialisation.
   */
  config?: {
    /**
     * Determines whether the collection is returned to the client-side 
     * as an array or object.
     * 
     * @default 'object'
     */
    transformer: 'object' | 'array' | (id, value) => any; 

    /**
     * Designates the unique identifier key that is used in the collection.
     * 
     * @default 'id'
     */
    identifier?: string;

    /**
     * Automatically creates an UUID as an identifier when an 
     * item is added without the identifier key.
     * 
     * @default false
     */
    autoId?: boolean;

    /**
     * A handler that catches the error thrown when you are adding an item 
     * that already exists in the collection. The returned object 
     * from the function is then used instead.
     * 
     * Note that if `null` is returned nothing happens.
     * 
     * @example: (type, value, id) => {
     *  if (type === 'exists') {
     *    return {
     *       ...value,
     *       id: `${id}-${new Date().getTime()})`
     *    }
     *  }
     * }
     */
    handleDuplicate: (value?: O, id?: I) => O | null
    

    /**
     * A handler that catches the error thrown when you are trying to
     * update/remove and item that does not exist. The returned id is 
     * then used instead.
     * 
     * Note that if `null` is returned then nothing happens.
     * 
     * @example: (type, value, id) => {
     *  if (type === 'remove') {
     *    return /\w+/(?=\-)/i.replace('');
     *  }
     */
    handleMissing?: (type: 'remove' | 'update' type value?: O, id?: I) => O | null
  }
)

Basic example

import { useCollection } from 'useCollection';

const startingCollection = [{ id: 'c', value: 30 }, { id: 'e', value: 50 }];
const [collection, collectionActions] = useCollection(startingCollection) /* { c: { id: 'c', value: 30 }, e: { id: 'e', value: 50 } } */


collectionActions.add({ id: 'g', value: 70 }, 'end'); 

/* 
 * { 
 *   c: { id: 'c', value: 30 }, 
 *   e: { id: 'e', value: 50 }, 
 *   g: { id: 'g', value: 70 }
 * } 
 */


collectionActions.add(
  [{ id: 'a', value: 10 }, { id: 'h', value: 80 }], 
  'start'
); 

/* 
 * { 
 *   a: { id: "a", value: 10 },
 *   h: { id: "h", value: 80 },
 *   c: { id: "c", value: 30 },
 *   e: { id: "e", value: 50 },
 *   g: { id: "g", value: 70 },
 * } 
 */


collectionActions.add(
  { id: 'b', value: 20 }, 
  1
); 

/* 
 * { 
 *   a: { id: "a", value: 10 },
 *   b: { id: 'b', value: 20 },
 *   h: { id: "h", value: 80 },
 *   c: { id: "c", value: 30 },
 *   e: { id: "e", value: 50 },
 *   g: { id: "g", value: 70 },
 * } 
 */


collectionActions.add(
  { id: 'd', value: 40 }, 
  { before: 'e' }
); 

/* 
 * { 
 *   a: { id: "a", value: 10 },
 *   b: { id: "b", value: 20 },
 *   h: { id: "h", value: 80 },
 *   c: { id: "c", value: 30 }, 
 *   d: { id: "d", value: 40 }, 
 *   e: { id: "e", value: 50 }, 
 *   g: { id: "g", value: 70 }
 * } 
 */


collectionActions.add(
  { id: 'f', value: 60 }, 
  { after: 'e' }
);

/* 
 * { 
 * { 
 *   a: { id: "a", value: 10 },
 *   b: { id: "b", value: 20 },
 *   h: { id: "h", value: 80 },
 *   c: { id: "c", value: 30 }, 
 *   d: { id: "d", value: 40 }, 
 *   e: { id: "e", value: 50 }, 
 *   f: { id: "f", value: 60 }, 
 *   g: { id: "g", value: 70 }
 * } 
 * } 
 */


collectionActions.reorder({ key: 'id', direction: 'ascending' });

/* 
 * { 
 *   a: { id: "a", value: 10 },
 *   b: { id: 'b', value: 20 },
 *   c: { id: "c", value: 30 },
 *   d: { id: "d", value: 40 },
 *   e: { id: "e", value: 50 },
 *   f: { id: "f", value: 60 },
 *   g: { id: "g", value: 70 },
 *   h: { id: "h", value: 80 },
 * } 
 */


collectionActions.reorder('value', 'descending');

/* 
 * { 
 *   h: { id: "h", value: 80 },
 *   g: { id: "g", value: 70 },
 *   f: { id: "f", value: 60 },
 *   e: { id: "e", value: 50 },
 *   d: { id: "d", value: 40 },
 *   c: { id: "c", value: 30 },
 *   b: { id: 'b', value: 20 },
 *   a: { id: "a", value: 10 },
 * } 
 */


collectionActions.reorder((a, b) => {
  if (a.id === 'e') {
    return -1;
  }

  if (a.values.value % 20 === 0) {
    return -1;
  }

  return 0;
}));

/* 
 * { 
 *   e: { id: "e", value: 50 },
 *   h: { id: "h", value: 80 },
 *   f: { id: "f", value: 60 },
 *   d: { id: "d", value: 40 },
 *   b: { id: "b", value: 20 },
 *   g: { id: "g", value: 70 },
 *   c: { id: "c", value: 30 },
 *   a: { id: "a", value: 10 },
 * } 
 */


collectionActions.move({ id: 'e'}, 4)

/* 
 * { 
 *   e: { id: "e", value: 50 },
 *   f: { id: "f", value: 60 },
 *   d: { id: "d", value: 40 },
 *   b: { id: "b", value: 20 },
 *   h: { id: "h", value: 80 },
 *   g: { id: "g", value: 70 },
 *   c: { id: "c", value: 30 },
 *   a: { id: "a", value: 10 },
 * } 
 */


collectionActions.move(2, { before: 'f' }); 

/* 
 * { 
 *   e: { value: 50 }
 *   d: { value: 40 }, 
 *   f: { value: 60 }, 
 *   b: { value: 20 }, 
 *   h: { value: 80 }, 
 *   g: { value: 70 }
 *   c: { value: 30 },
 *   a: { value: 10 },
 * } 
 */


collectionActions.remove(1);

/* 
 * { 
 *   e: { value: 50 }
 *   f: { value: 60 }, 
 *   b: { value: 20 }, 
 *   h: { value: 80 }, 
 *   g: { value: 70 }
 *   c: { value: 30 },
 *   a: { value: 10 },
 * } 
 */


collectionActions.remove({ id: 'c' });

/* 
 * { 
 *   e: { value: 50 }
 *   f: { value: 60 }, 
 *   b: { value: 20 }, 
 *   h: { value: 80 }, 
 *   g: { value: 70 },
 *   a: { value: 10 },
 * } 
 */


collectionActions.remove(([ , value ]) => value % 20 === 0, 1); 

/* 
 * {
 *  c: { value: 30 },
 *  d: { value: 40 },
 *  e: { value: 50 },
 *  g: { value: 70 }
 * }
 */


collectionActions.remove(
  ([ id, value ]) => {
    if (id === 70) {
      return false;
    }

    if (value % 2 === 0) {
      return false;
    }

    return true;
  },
);

/* 
 * {
 *  d: { value: 40 },
 *  g: { value: 70 }
 * }
 */


collectionActions.update(
  1, 
  { value: 99, additional: 'Hello World!' },
); 

/* 
 * {
 *  d: { value: 40 },
 *  g: { value: 99, additional: 'Hello World!' }
 * }
 */


collectionActions.update(
  { id: 'g' }, 
  { value: 3 },
); 

/* 
 * {
 *  d: { value: 40 },
 *  g: { value: 3, additional: 'Hello World!' }
 * }
 */


collectionActions.update(
  0
  { anotherAdditional: 'Lorem Ipsum' },
); 

/* 
 * {
 *  d: { value: 40, anotherAdditional: 'Lorem Ipsum' },
 *  g: { value: 99, additional: 'Hello World!' }
 * }
 */

collectionActions.replace()

/*
 * {}
 */


collectionActions.replace({ john { age: 6 }, sarah: { age: 32 }); 

/* 
 * {
 *  d: { value: 40, anotherAdditional: 'Lorem Ipsum' },
 *  g: { value: 99, additional: 'Hello World!' }
 * }
 */


collectionActions.replace(startingCollection);

/* 
 * {
 *  c: { value: 30 },
 *  e: { value: 50 }
 * }
 */

TypeScript example

import { useCollection, Array, Object, Actions, Props } from 'useCollection';

interface Item {
  /**
   * A unique id (UUID) that is assigned to a specific item in this collection.
   *
   * @example '668d6dd0-e154-4114-ad84-6bb2f6b5581b'
   */
  id: string;

  /**
   * The title assigned to a specific user alert. This is a short summary of what
   * you are trying to communicate and should not be longer than about 6 words in
   * length. Rather use the `message` value to provide more context.
   *
   * @example 'Access denied'
   * @example 'Redirecting to homepage'
   * @example 'Image uploaded'
   * @example 'Something went wrong!'
   */
  title: string;

  /**
   * The nature of the alert. This influences the manner in which the alert is
   * displayed when consumed by an `<Alerts />` component.
   *
   * - `'info'` should generally be used to provide the user with
   *   additional/non-critical information. For example if they entered an
   *   incorrect password several times an info alert might appear that says
   *   something along the lines of 'If you have forgotten or are unsure what your
   *   login details are you can contact [email protected] for assistance.'
   *
   * - `'warning'` is surfaced to a user when an anticipated/pre-defined error
   *   state is triggered. For example when a user enters an incorrect value or if
   *   they do not have permission to perform a specific task. Warnings can either
   *   block a user from performing and action or allow them to proceed as long as
   *   they are aware of the implications.
   *
   * - `'error'` should only be reserved for unexpected fail-states or errors that
   *   are not considered as part of the expected user flow. These usually have a
   *   technical aspect to them and advise users to try again and/or contact the
   *   support team. A common example where an error alert would be used is if the
   *   server returns a `404` HTTP status.
   *
   * - `'success'` should be used to indicate actions that have been performed
   *   successfully. According to the principles of [an optimistic
   *   UI](smashingmagazine.com/2016/11/true-lies-of-optimistic-user-interfaces/)
   *   the UI can update accordingly while the action is busy resolving, but a
   *   success alert can only fire once the response actually resolves, cementing
   *   the notion that the action actually did succeed (while not
   *   hampering/blocking the user-flow).
   *
   * - `'resolving'` should be used when you want to indicate to a user that an
   *   action is being performed but the end-state of the action is still unknown.
   *   For example you can surface a resolving alert while data is being uploaded.
   *   Once you know whether the data has been uploaded successfully (or failed)
   *   you can update the type accordingly. Note that when consumed by the
   *   `<Alerts />` component it will display a spinner instead of an icon in the
   *   alert.
   */
  type: 'info' | 'warning' | 'error' | 'success' | 'resolving';

  /**
   * An optional message that can be included alongside the alert's `title`. This
   * can provide more context on what exactly happened or provide
   * instructions/next steps to the user.
   *
   * @example 'Please try again. If the issue persists please contact our support
   * team.'
   *
   * @example 'The information provided is incorrect, please confirm that you have
   * entered the correct email and password.'
   *
   * @example 'One of the fields are empty. Please add all required data before
   * proceeding.'
   *
   * @example 'To access this information again later please visit your account
   * section.'
   */
  message: string | null;
}

const [collection, collectionActions] = useCollection<string, Item>([], { autoId: true })