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 🙏

© 2026 – Pkg Stats / Ryan Hefner

zustand-dot

v1.2.1

Published

Zustand middleware for deep dot-path access and reactive hooks

Readme

Zustand Dot Path Middleware

A typesafe, reactive middleware for Zustand that enables deep property access using dot notation. Support your complex state objects with granular subscriptions, immutable updates, and stable default values.

Features

  • 🎯 Deep Access: Get, set, and subscribe to deeply nested values using dot path strings (e.g., user.posts.0.title).
  • 🛡️ Fully Typesafe: Paths and return values are strictly inferred. Invalid paths throw compile-time errors.
  • Reactive Hook: usePath subscribes only to the specific path you request.
  • 🔔 Vanilla Subscriptions: subscribePath runs a listener whenever a specific path changes, with or without React.
  • 🧘 Stable Defaults: usePath memoizes default values deeply, preventing unnecessary re-renders when passing objects/arrays as defaults.
  • 🔄 Immutable Updates: setPath performs structural sharing, updating only what changed.
  • 🔢 Array Support: seamless array access via dot notation (items.0) or brackets (items[0]).

Installation

npm install zustand-dot zustand
# or
yarn add zustand-dot zustand
# or
pnpm add zustand-dot zustand
# or
bun add zustand-dot zustand

Setup

Wrap your store creator with dotPath.

import { create } from 'zustand'
import { dotPath } from 'zustand-dot'

interface State {
  user: {
    profile: {
      name: string
      age: number
    }
    posts: Array<{ id: number; title: string }>
  }
}

const useStore = create<State>()(
  dotPath((set) => ({
    user: {
      profile: { name: 'Alice', age: 30 },
      posts: [],
    },
  }))
)

API

usePath(path, defaultValue?)

A React hook that subscribes to a specific path in the store.

// Component re-renders ONLY when user.profile.name changes
const [name, setName] = useStore.usePath('user.profile.name')

// With a default value (memoized deeply!)
const [posts, setPosts] = useStore.usePath('user.posts', [])
// ^ Safe! Passing [] as default won't cause infinite re-renders.
  • Arguments:
    • path: string - Dot notation path to the value.
    • defaultValue: (optional) - Value to return if the path resolves to null or undefined.
  • Returns: [value, setter] tuple.

Object and array defaults are compared deeply on every render, so hoist large defaults to a module constant.

setPath(path, valueOrUpdater)

Updates a value deeply, creating nested objects/arrays if they don't exist.

// Set a value directly
useStore.setPath('user.profile.age', 31)

// Functional update (receives current raw value at path)
useStore.setPath('user.posts', (posts) => [...posts, { id: 1, title: 'New' }])

// Arrays via index
useStore.setPath('user.posts.0.title', 'Updated Title')
  • Arguments:
    • path: string.
    • valueOrUpdater: The new value, or a function (prev) => next.

getPath(path, defaultValue?)

Imperatively get a value from the store (non-reactive).

const age = useStore.getPath('user.profile.age')

resetPath(path)

Resets a specific path (subtree) back to its initial state captured at store creation time.

// Reverts user.profile to { name: 'Alice', age: 30 }
useStore.resetPath('user.profile')

Action functions stored in state are snapshotted by reference, so resetting a subtree that contains actions keeps them callable. Resetting a path that was absent from the initial state removes the key instead of leaving undefined behind.

subscribePath(path, listener)

Subscribe to a path outside React. The listener receives the new and previous values, and only fires when the value at that path actually changes.

const unsubscribe = useStore.subscribePath('user.profile.name', (name, prev) => {
  console.log(`${prev} renamed to ${name}`)
})

// Later
unsubscribe()

TypeScript Support

The middleware leverages advanced recursive types to provide autocomplete and validation.

// ✅ Valid
useStore.setPath('user.profile.name', 'Bob')

// ❌ Error: Property 'nominative' does not exist
useStore.setPath('user.profile.nominative', 'Bob')

// ❌ Error: Type 'number' is not assignable to type 'string'
useStore.setPath('user.profile.name', 123)

Path Syntax

  • Dot notation: a.b.c
  • Array indices: items.0.id

The runtime parser also accepts brackets (items[0].id) and quoted keys (config["remote.url"]), but Paths<T> only generates dot notation, so the typed API rejects those forms. Use them with a cast when a key contains a literal dot.

Path Depth

Path enumeration comes from dot.paths and stops at 8 levels by default. Raise the cap (up to 16) by passing options to the middleware:

const useStore = create<State>()(
  dotPath((set) => ({ ... }), { depth: 12 })
)

Exported Types

Paths, Get, GetStrict, and PathsOptions are re-exported from dot.paths under their original names. Get resolves any string path and yields never on a miss; GetStrict additionally rejects invalid paths at compile time.

Performance

All dot-path operations use cached path parsing, iterative deep-set, and closure-free state updates. Benchmarked against equivalent vanilla Zustand patterns using vitest bench.

| Operation | vs Vanilla | Absolute (per op) | | -------------------------- | ---------------- | ----------------- | | Read — shallow | 1.09x slower | ~22 ns | | Read — deep (4 levels) | 1.92x slower | ~38 ns | | Read — array element | 1.75x slower | ~36 ns | | Write — shallow | 1.13x faster | ~36 ns | | Write — deep (4 levels) | 2.56x slower | ~161 ns | | Write — functional updater | 1.41x slower | ~62 ns | | Write — array element | 2.15x slower | ~142 ns | | Reset — subtree | 4.67x faster | ~197 ns |

Shallow reads are within 10% of direct property access. Shallow writes are faster than vanilla setState because setPath uses replace: true, skipping Zustand's partial state merge. Resets beat the vanilla pattern because the initial subtree is snapshotted once at store creation and restored from cached path segments.

Run npm run bench to reproduce.

License

MIT