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

@shrugsy/use-immer-state

v3.1.2

Published

Use immutable state with inbuilt time travel. Super-charged use-state with immer capabilities.

Downloads

46

Readme

use-immer-state

version build codecov size

A React hook that provides a supercharged version of the useState hook. Allows for writing easy immutable updates. Heavily inspired by @reduxjs/redux-toolkit's usage of immer.

codesandbox demo

Table of Contents

  1. Installation
  2. Features
  3. Usage
  4. Mutation Detection
  5. Re-exports
  6. Typescript Exports

Installation

npm i @shrugsy/use-immer-state

Within your React app:

import { useImmerState } from "@shrugsy/use-immer-state";

⬆ back to top

Features

  • Includes all functionality from useState.
  • When using the 'functional update' setter callback, updates can be written 'mutably', and the setter internally uses immer to produce the next immutable state.
  • Throws an error if a state mutation is detected between mutations to help fix bad habits (except in production mode).
  • Provides inbuilt time-travel history including 'checkpoints', 'goTo', 'goBack', 'goForward' and 'reset' functionality.
  • Full typescript support.

Note: If you're looking to be able to write 'mutable' draft updates for more complex state, I recommend either:

⬆ back to top

Usage

Basic Usage

At it's core, it can be used identically to the inbuilt useState hook.

e.g.

import { useImmerState } from "@shrugsy/use-immer-state";

const [user, setUser] = useImmerState({ id: 1, name: "john smith" });

function handleUpdateUser(newName) {
  // nothing special here, this is how you might do it with `useState` currently
  setState({ ...user, name: "Jane Doe" });
}

When using a callback to perform functional updates, behaviour is as follows:

  • New state is computed using the previous state (same as useState)
  • The updates within the callback can be written mutably, but internally produce the next immutable update, without mutating the state

e.g.

const [user, setUser] = useImmerState({ id: 1, name: "john smith" });

function handleUpdateUser(newName) {
  // the functional update notation allows writing the update mutably, and will internally produce an immutable update without mutating the actual state
  setState((prev) => {
    prev.name = "Jane Doe";
  });
}

The benefits shine more for nested updates that would be messy to write manually. e.g.

// given some initial state like so:
const initialState = [
  {
    todo: "Learn typescript",
    done: true,
  },
  {
    todo: "Try use-immer-state",
    done: false,
  },
  {
    todo: "Pat myself on the back",
    done: false,
  },
];
const [todos, setTodos] = useImmerState(initialState);

function handleToggleTodo(index, isDone) {
  setTodos((prevTodos) => {
    if (prevTodos[index]) prevTodos[index].done = isDone;
  });

Note: To achieve a similar effect with plain useState, the update would look more like this:

const [todos, setTodos] = useState(initialState);

function handleToggleTodo(index, isDone) {
  setTodos((prevTodos) => {
    return prevTodos.map((todo, idx) => {
      if (idx !== index) return todo;
      return { ...todo, done: isDone };
    });
  });
}

Note that the deeper the nested updates become, the larger the advantage will be to use this notation.

⬆ back to top

Advanced Usage

The tuple returned by useImmerState includes an optional third value; extraAPI like so:

const [state, setState, extraAPI] = useImmerState(initialState);

Note that you can name the value anything you like, or de-structure the values out directly.

  • state is the current state, similar to what you would receive from useState.

  • setState is the function to use when updating state, similar to what you would receive from useState.
    Key differences:

    • When providing a callback updater, updates can be written mutably, and are applied immutable behind the scenes
    • It accepts an optional second boolean argument to dictate whether that state update should contribute to the state 'history' object (defaults true).
  • extraAPI is an object that contains the following values:

Note: For the purposes of the table below, S refers to the type of initialState.

| Name | Type | Description | | ----------------- | ---------------------- | -------------------------------------------------------------------------------------- | | history | ReadOnlyArray<S> | (default [initialState]) An array of the state history | | stepNum | number | (default 0) The current step (index) within the state history | | isFirstStep | boolean | Whether the current step is the first step (i.e. if stepNum === 0) | | isLastStep | boolean | Whether the current step is the last step (i.e. if stepNum === history.length - 1) | | goTo | (step: number) => void | Change the current state to a particular step (index) within the state history | | goBack | () => void | Go to the previous step (index) within the state history | | goForward | () => void | Go to the next step (index) within the state history | | saveCheckpoint | () => void | Saves the current step (index) within the state history to a 'checkpoint' that can be restored later | | restoreCheckpoint | () => void | Restores the state to the saved 'checkpoint' if it is still valid | | checkpoint | number | (default 0) The step (index) within the state history for the saved checkpoint | | isCheckpointValid | boolean | (default true) Indicates whether the saved checkpoint is valid and accessible to restore. A checkpoint will be invalidated if the history gets overwritten such that it overwrites the saved checkpoint. History is overwritten when writing new state while at a step number besides the latest. | | reset | () => void | Resets state, history and checkpoint back to the initial state. |

Please try the codesandbox demo to see an example of the API in action.

⬆ back to top

Mutation detection

This library expects that mutating logic is only written using the functional update notation within a setState call. Any attempts to mutate the state outside of this are not supported.

If an uncontrolled mutation is detected, a MutationError will be thrown (a custom error type exported by this library), and the path detected will be logged to the console to highlight the detected mutation and assist with detecting the cause.

See this codesandbox example to view how the mutation is detected and shown in the console.

Mutation log output

Note:

This feature is disabled in production mode.

By default, immer freezes the state recursively after it has been used. This means that attempted mutations will not have an effect, but will not reliably be detected and throw an error for every setup/browser when the attempt is made.
What this means is that the mutation may only be detected in between the first and second state.
This library re-exports setAutoFreeze from immer which can help narrow down invalid mutation attempts, as calling setAutoFreeze(false) will prevent immer freezing the state, and allow the mutation detection from this library to reliably detect uncontrolled mutations occurring to a serializable state value.

⬆ back to top

Re-exports

The following items are re-exported from other libraries for ease of use:

  • setAutoFreeze - Enables / disables automatic freezing of the trees produces. By default enabled.

  • current - Given a draft object (doesn't have to be a tree root), takes a snapshot of the current state of the draft

  • original - Given a draft object (doesn't have to be a tree root), returns the original object at the same path in the original state tree, if present

  • castDraft - Converts any immutable type to its mutable counterpart. This is just a cast and doesn't actually do anything

  • Draft - Exposed TypeScript type to convert an immutable type to a mutable type

See the following links for more information on the immer API: https://immerjs.github.io/immer/api/

⬆ back to top

Typescript exports

The following type definitions are used by this library internally and are exported for typescript users to use as required.

/** Initial state provided to the hook */
export declare type InitialState<S> = S | (() => S);
/** New state, or a state updater callback provided to a `setState` call */
export declare type Updates<S> = S | ((draftState: Draft<S>) => Draft<S> | void | undefined);
/** Function used to update the state */
export declare type SetState<S> = (updates: Updates<S>, includeInHistory?: boolean) => void;
/** Extra API used for time travel features */
export declare type ExtraAPI<S> = {
    history: readonly S[];
    stepNum: number;
    isFirstStep: boolean;
    isLastStep: boolean;
    goTo: (step: number) => void;
    goBack: () => void;
    goForward: () => void;
    saveCheckpoint: () => void;
    restoreCheckpoint: () => void;
    checkpoint: number;
    isCheckpointValid: boolean;
    reset: () => void;
};
/** Return value of the hook */
export declare type UseImmerStateReturn<S> = readonly [S, SetState<S>, ExtraAPI<S>];
/**
 * Hook similar to useState, but uses immer internally to ensure immutable updates.
 * Allows using the setter function to be written 'mutably',
 * while letting immer take care of applying the immutable updates.
 *
 * Provides time travel support including `history`, `checkpoints`, `goTo`,
 * and `reset` functionality.
 *
 * If not in production mode, checks for mutations between renders and will
 * throw an error if detected.
 *
 * https://github.com/Shrugsy/use-immer-state#readme
 * @param initialState - initial state, or lazy function to return initial state
 * @returns [state, setState, extraAPI]:
 * - state - the current state
 * - setState- A function to update the state
 * - extraAPI - An object containing details and methods related to inbuilt time travel features
 */
export declare function useImmerState<S = undefined>(): UseImmerStateReturn<S | undefined>;
export declare function useImmerState<S>(initialState: InitialState<S>): UseImmerStateReturn<S>;