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 🙏

© 2025 – Pkg Stats / Ryan Hefner

hook-store-bridge

v1.1.4

Published

A utility to bridge React hook state into Zustand or other state management libraries.

Readme

hook-store-bridge

NPM version License hook-store-bridge demo showing performance improvement

A utility that bridges a React logic hook with a performant state management store (like Zustand), solving state-sharing across components without the performance pitfalls of React Context.

Features

  • 💡 Explicit & Predictable: A clear API ({ tracked, methods }) that makes data flow easy to reason about.
  • Highly Performant: Avoids unnecessary re-renders by design, updating components only when the state they subscribe to actually changes.
  • 🎯 Solves Stale Closures: The core pattern robustly handles stale closures for dependent props and state.
  • 🚫 No More Prop-Drilling: Share state globally without passing props through dozens of components.
  • 🔧 State Library Agnostic: Uses Zustand by default but can be configured to work with any state management library.
  • 🛡️ Type-Safe: Fully written in TypeScript with excellent type inference out of the box.
  • 🌐 SSR Compatible: Works seamlessly with Server-Side Rendering frameworks like Next.js.

The Problem

Sharing the logic of a powerful custom hook (e.g., Vercel AI SDK's useChat) between distant components is challenging. You typically have two options:

  1. Prop Drilling: Leads to verbose, hard-to-maintain code.
  2. React Context: A good solution, but it has a major performance drawback. Any component consuming the Context will re-render whenever any value in the Context changes. For a frequently-updating hook like useChat, this causes a cascade of unnecessary re-renders, slowing down your app.

The Solution

hook-store-bridge offers a third way. It "lifts" your hook's logic into an optimized store (like Zustand) that supports granular, selector-based subscriptions. This is achieved through a simple but powerful design pattern.

You structure your hook to return two distinct objects:

  • tracked: An object containing all values that should trigger a component re-render when they change.
  • methods: An object containing all the functions and actions for the store.

This explicit separation gives you the convenience of global state with the best possible rendering performance. A component that only calls a method from methods will not re-render when the tracked state changes.

Installation

npm install hook-store-bridge
# or
yarn add hook-store-bridge
# or
pnpm add hook-store-bridge

Quick Start

Let's create a simple counter store to demonstrate the core concepts.

1. Define Your Logic Hook

First, create a custom hook that defines your store's logic. It must return a { tracked, methods } object.

// useCounterLogic.ts
import { useState, useMemo, useCallback } from 'react'
import type { StoreLogicResult, UseStoreLogic } from 'hook-store-bridge'

export const useCounterLogic = (initialValue = 0) => {
  const [count, setCount] = useState(initialValue)

  // 1. Define your methods using useCallback for stable references.
  const increment = useCallback(() => setCount((c) => c + 1), [])
  const decrement = useCallback(() => setCount((c) => c - 1), [])

  // 2. Return the state and methods in the required structure.
  return {
    tracked: { count },
    methods: { increment, decrement },
  }
}

2. Create the Bridge

Use createHookBridge to create the StoreProvider and useBridgedStore hook.

// counterStore.ts
import { createHookBridge } from 'hook-store-bridge'
import { useCounterLogic } from './useCounterLogic'

export const { useBridgedStore, StoreProvider } = createHookBridge({
  useStoreLogic: useCounterLogic,
})

3. Provide the Store

Wrap your application or component tree with the StoreProvider.

// App.tsx
import { StoreProvider } from './counterStore'
import { CounterDisplay, CounterControls } from './MyComponents'

function App() {
  return (
    // You can pass arguments to your logic hook via the `logicArgs` prop.
    <StoreProvider logicArgs={[10]}>
      <h1>Counter</h1>
      <CounterDisplay />
      <CounterControls />
    </StoreProvider>
  )
}

4. Use the Store in Any Component

Now, any child component can access the store's state and methods.

CounterDisplay.tsx (Subscribes to state) This component reads from the tracked state and will re-render when count changes.

import { useBridgedStore } from './counterStore'

export function CounterDisplay() {
  const { store } = useBridgedStore()
  // Use a selector to subscribe ONLY to the `count` state.
  const count = store.use.count()

  return <h2>Count: {count}</h2>
}

CounterControls.tsx (Only uses methods) This component only calls methods. Because it does not subscribe to any tracked state, it will not re-render when the count changes, giving you optimal performance.

import { useBridgedStore } from './counterStore'

export function CounterControls() {
  // Methods are spread directly onto the hook's return value.
  const { increment, decrement } = useBridgedStore()

  return (
    <div>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  )
}

The Core Principle: tracked vs. methods

To avoid stale closure bugs and ensure your components always have the latest logic, you must follow one simple rule:

If a method's behavior depends on a changing value (like a prop), that method MUST be included in the tracked state object.

This ensures that when the dependency changes, a new method instance is created, which in turn updates the tracked state. This triggers a re-render in consumers, providing them with the fresh method from the latest render.

Example: A Logger Hook

Incorrect Usage ❌ Here, log depends on prefix, but the log method itself is not tracked. If the prefix prop changes, components will keep using the old log function, which will log with the old prefix (a stale closure bug).

const useLoggerLogic = ({ prefix }) => {
  const methods = {
    log: (message: string) => console.log(`[${prefix}]: ${message}`),
  }
  return {
    tracked: {}, // The `log` method is missing!
    methods,
  }
}

Correct Usage ✅ By including the log method in the tracked state, we guarantee that when prefix changes, a new log function is created. This updates the Zustand store, causing consumers to re-render and receive the new log method with the correct prefix in its closure.

const useLoggerLogic: UseStoreLogic<{ prefix: string }, { log: (message: string) => void }, {}> = ({ prefix }) => {
  // When `prefix` changes, useCallback creates a new function instance.
  const log = useCallback((message: string) => {
    console.log(`[${prefix}]: ${message}`)
  }, [prefix])

  return {
    tracked: { log }, // Correct! The dependent method is tracked.
    methods: {},
  }
}

API Reference

createHookBridge(options)

The main function to create your store bridge.

  • options.useStoreLogic: (Required) The hook that defines the store's core logic. This hook can optionally receive arguments and must return an object with { tracked, methods }.
  • options.createStoreConfig: (Optional) A function for providing a custom store implementation (e.g., using a different library or adding Zustand middleware).

StoreProvider

A React component that initializes the store and provides it to its children.

  • props.logicArgs: (Optional) An array of arguments to be passed to your useStoreLogic hook.
  • props.children: The component tree that will have access to the store.

useBridgedStore()

A hook for child components to access the store. It returns an object containing:

  • store: The Zustand store instance. You can subscribe to state slices with selector hooks (e.g., store.use.myState()).
  • All functions from your methods object are spread onto the return value for direct access (e.g., const { myMethod } = useBridgedStore()).

Utilities

You can also import utility functions directly from the util subpath:

createSelectors(store)

A utility function that adds selector hooks to a Zustand store instance. This allows you to subscribe to specific state slices with store.use.myState() syntax.

import { createSelectors } from 'hook-store-bridge/util'
import { createStore } from 'zustand'

const store = createStore<{ count: number }>(() => ({ count: 0 }))
const storeWithSelectors = createSelectors(store)

// Now you can use selector hooks
const count = storeWithSelectors.use.count()

WithSelectors<S>

A TypeScript utility type that adds selector capabilities to a store type.

import type { WithSelectors } from 'hook-store-bridge/util'
import type { StoreApi } from 'zustand'

type MyStore = WithSelectors<StoreApi<{ count: number }>>

Custom Store Configuration

You can override the default Zustand setup by providing a createStoreConfig function. This is useful for adding middleware (like Redux DevTools) or integrating a different state library.

See the source code for the default implementation and type definitions.

Example with Redux DevTools:

import { createHookBridge } from 'hook-store-bridge'
import { createStore } from 'zustand'
import { devtools } from 'zustand/middleware'
import { createSelectors } from 'hook-store-bridge/util'
import { useMyStoreLogic } from './useMyStoreLogic'

export const { useBridgedStore, StoreProvider } = createHookBridge({
  useStoreLogic: useMyStoreLogic,
  createStoreConfig: () => ({
    createStore: (initState) => {
      const store = createStore<ReturnType<typeof useMyStoreLogic>['tracked']>()(
        devtools(() => initState, { name: 'MyStore' }))
      return createSelectors(store)
    },
    updateState: (store, newState) => {
      store.setState(newState, false, 'hookStateUpdate')
    },
  }),
})

Examples

Check out the examples directory for complete working examples:

License

This project is licensed under the ISC License - see the LICENSE file for details.