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

zustand-nibble

v0.3.3

Published

Composable zustand stores with nibbles

Downloads

126

Readme

zustand-nibble

Split a zustand store into smaller pieces, called nibbles. Compared to slices which are spread at the top-level of the store, a nibble can be placed anywhere in the parent store.

import { type StateCreator, create } from 'zustand';
import nibble from 'zustand-nibble';

export interface Child {
    name: string;
    age: number;
    birthday: () => void;
}

export interface Parent {
    name: string;
    age: number;
    child: Child;
    birthday: () => void;
}

const createJoe: StateCreator<Child> = set => ({
    name: 'Joe Doe',
    age: 10,
    birthday: () => set(state => ({ age: state.age + 1 })),
});

const useParent = create<Parent>()((set, get, api) => ({
    name: 'John Doe',
    age: 42,
    child: nibble(api)(state => state.child)(createJoe),
    birthday: () => set(state => ({ age: state.age + 1 })),
}));

nibble(api)(getter, setter?) receives the following arguments:

  • The parent store api
  • A getter that extracts the child's state from the parent state
  • A custom setter for the parent state, required for middlewares mutating setState

It returns a function that accepts a StateCreator to create the child state, similar to a zustand middleware.

Installation

npm

npm install zustand-nibble

bun

bun install zustand-nibble

Why not use Immer?

immer and zustand-nibble both simplify nested state updates.

I would argue, that immer is the better choice here. In fact, zustand-nibble uses immer under the hood to update the parent state.

The primary use of a nibble is to decouple the child state from the parent state. This allows the composition of independent states into any structure, even dynamically.

In the example above, createJoe is independent of the parent state. It can be integrated in any store that accepts a Child, using a nibble to link them together. This decoupling in not possible with immer alone, as it always operates on the parent state.

Naturally, immer and zustand-nibble can be used together.

Use with middlewares

You can use any middleware on the child store by applying it to your state creator:

const useParent = create<Parent>()((set, get, api) => ({
    name: 'John Doe',
    age: 42,
    child: nibble(api)(state => state.child)(immer(set => ({ // <- apply immer middleware
        name: 'Joe Doe',
        age: 10,
        birthday: () => set(draft => { draft.age += 1 }),
    }))),
    birthday: () => set(state => ({ age: state.age + 1 })),
}));

If you use a middleware on the parent store that mutates the setState function, you may need to provide a custom setter to the nibble.

Immer

As nibble uses immer to update the parent state, you can just pass the set function when using the immer middleware on the parent store.

const useParent = create<Parent>()(immer((set, get, api) => ({
    name: 'John Doe',
    age: 42,
    child: nibble(api)(state => state.child, set)(createJoe), // <- pass mutated set function
    birthday: () => set(state => ({ age: state.age + 1 })),
})));

Custom Setter

You have to provide a custom setter if the setState function is not compatible with the standard form:

type SetState<T>: (nextState: (state: T) => T) => void;

The setter must be a function that accepts an updater working on an immer draft.

type Setter<T> = (updater: (draft: Draft<T>) => void) => void;

The default setter uses immer's produce to update the parent state.

const defaultSetter: Setter<T> = updater => api.setState(produce<T>(updater))

Use as Recipe

The function returned by nibble can be used as a recipe in multiple stores.

// Omit the api to create a recipe
const createChild = nibble<Parent>()(state => state.child); // Recipe<Parent, Child>

const useDad = create<Parent>()((set, get, api) => ({
    name: 'John Doe',
    age: 42,
    child: createChild(api)(createJoe), // call recipe
    //...
}));

const useMom = create<Parent>()((set, get, api) => ({
    name: 'Jane Doe',
    age: 37,
    child: createChild(api)(createJoe), // call recipe
    //...
}));

/* Note that the childs are separate instances.
There is no state sharing through nibbles */

Arrays

Arrays are objects in JavaScript, by default the setter will merge the array using Object.assign. This is equvialent to how zustand handles array states.

Likewise, you can use the replace flag to disable this merging behavior.

childStore.setState([4, 5]); // [1, 2, 3] -> [4, 5, 3]
childStore.setState([4, 5], /*replace*/ true); // [1, 2, 3] -> [4, 5]

Tip: If possible wrap the array in an object and instead use that object as the root of your state. This applies to both zustand and zustand-nibble.

// Instead of this:
const useNumbers = create<number[]>(...); // zustand
nibble<number[]>()(getter); // zustand-nibble

// Do this:
interface State {
    values: number[];
}
const useNumbers = create<State>(...); // zustand
nibble<State>()(getter); // zustand-nibble