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

@thefoxieflow/signalctx

v1.0.5

Published

Lightweight signal-based React context store

Readme

signalctx

A tiny, signal-based state utility for React that solves the useContext re-render problem using useSyncExternalStore.

If you’ve ever had this issue:

{
  count, book;
}
// updating count re-renders book components 😡

signalctx is designed specifically to fix that.


✨ Why signal-ctx?

❌ The Problem with useContext

React Context subscribes to the entire value.

const { book } = useContext(StoreCtx);

When any property changes, every consumer re-renders — even if they don’t use it.

This is not a bug. Context has no selector mechanism.


✅ The Solution

signal-ctx:

  • Moves state outside React
  • Uses external subscriptions
  • Allows selector-based updates

So only the components that actually use the changed data re-render.


✨ Features

  • ⚡ Signal-style state container
  • 🎯 Selector-based subscriptions
  • 🧵 React 18 concurrent-safe
  • 🧩 Context-backed but not context-driven
  • 📦 Very small bundle size
  • 🌳 Tree-shakable
  • 🧠 Explicit and predictable

📦 Installation

npm install @thefoxieflow/signalctx

Peer dependency: React 18+


🧠 Core Idea

Context does not store state.

It stores a stable signal reference.

<Provider value={store} />

The state lives outside React, and components subscribe directly to the signal.


🔹 Signal

A signal is:

  • A function that returns state
  • Can be subscribed to
  • Can be updated imperatively
type Signal<T extends object> = {
  (): T; // get state

  // add listener
  on(fn: Subscriber): () => void;

  // notify all listeners
  notify(): void;

  // reset to initial value
  reset(): void;

  // update state
  set(action: SetAction<T>): void;
};

🔹 Low-Level Functions

newSignal(init)

Creates a low-level signal.

const signal = newSignal({ count: 0 });
const state = signal(); // get state { count: 0 }

signal.on(() => console.log("changed"));

setInterval(() => {
  signal.set((s) => {
    s.count++;
  });
  // will trigger signal.on listeners
  signal.notify();
}, 2000);

🔹 React Hooks

useValue(store, selector?)

Subscribe to a signal.

const count = useValue(store, (s) => s.count);
  • Uses useSyncExternalStore
  • Re-renders only when the selected value changes
  • Selector is optional

useSet(store, selector?)

Returns a setter function.

const set = useSet(store);

// update entire state
const prev = store();
set({ ...prev, count: prev.count + 1 });

// or update partially
set((s) => {
  s.count++;
});

Scoped update:

const store = newSignal(() => ({
  book: { title: "1984", page: 1 },
  user: { name: "Alice" },
}));
// book must be object for selector
const setBook = useSet(store, (s) => s.book);

// update an object
const setBook = () => {
  setBook({
    title: "1999",
    page: 10,
  });
};

// or update partially
setBook((b) => {
  b.title = "1999";
});

⚠️ Updates are mutation-based. Spread manually if you want immutability.


🔹 Context-Based API

createCtx(init)

Creates a context-backed signal store hook.

import { createCtx } from "@thefoxieflow/signalctx";

export const useAppCtx = createCtx(() => ({
  count: 0,
  book: { title: "1984" },
}));

The returned function has these properties:

  • useAppCtx(selector, options) - Hook to select state
  • useAppCtx.Provider - Context provider component
  • useAppCtx.useSet(selector, options) - Hook to get setter function
  • useAppCtx.useSignal(options) - Hook to access raw signal underlying the context

🚀 Usage

1. Create a Provider

// use default initial value from useAppCtx
type Props = {
  children: React.ReactNode;
};

export function AppCtxProvider({ children }: Props) {
  return <useAppCtx.Provider>{children}</useAppCtx.Provider>;
}

// overwrite value
export function AppCtxProvider({ children }: Props) {
  return (
    <useAppCtx.Provider
      value={{
        count: 10,
        book: { title: "Brave New World" },
      }}
    >
      {children}
    </useAppCtx.Provider>
  );
}
<AppCtxProvider>
  <App />
</AppCtxProvider>

2. Read only what you need

function Count() {
  const count = useAppCtx((s) => s.count);
  return <div>{count}</div>;
}

function Book() {
  const book = useAppCtx((s) => s.book);
  return <div>{book.title}</div>;
}

3. Update state

function Increment() {
  const setCount = useAppCtx.useSet((s) => s);

  return (
    <button
      onClick={() =>
        setCount((s) => {
          s.count++;
        })
      }
    >
      +
    </button>
  );
}

4. Custom signal for additional logic

const signalWithTraceSet = <T extends object & { traceSet: number }>(
  init: () => T
) => {
  const core = newSignal(init);

  const signal: Signal<T> = () => core();

  signal.reset = core.reset;
  signal.notify = core.notify;
  signal.on = core.on;

  // set interceptor
  signal.set = (action: SetAction<T>) => {
    console.log("before set", core().traceSet);
    core.set(action);
    core().traceSet += 1;
    console.log("after set", core().traceSet);
  };

  return signal;
};

const useHelloCtx = createCtx(
  () => ({ traceSet: 0, text: "hello" }),
  signalWithTraceSet
);

✅ Updating count does NOT re-render Book.


🧩 Why This Works

  • Context value never changes
  • React does not re-render on context updates
  • useSyncExternalStore compares selected snapshots
  • Only changed selectors trigger re-renders

This is the same model used by:

  • Redux useSelector
  • Zustand selectors
  • React’s official external store docs

⚠️ Important Rule

Never destructure the entire state. Always select the smallest possible slice.

❌ Bad:

const { count } = useAppCtx((s) => s);

✅ Good:

const count = useAppCtx((s) => s.count);

🧩 Multiple Stores

You can create isolated stores using name.

type Props = {
  children: React.ReactNode;
  name?: string;
  initialValue?: { count: number; book: { title: string } };
};

export function AppCtxProvider({ children, name, initialValue }: Props) {
  return (
    <useAppCtx.Provider value={initialValue} name={name}>
      {children}
    </useAppCtx.Provider>
  );
}

Usage

<AppCtxProvider name="storeA" initialValue={{ count: 1, book: { title: "A" } }}>
  {/* useAppCtx(s => s.book) is from storeA */}
  <AppA />
  <AppCtxProvider
    name="storeB"
    initialValue={{ count: 5, book: { title: "B" } }}
  >
    {/* useAppCtx(s => s.book) is from storeB */}
    {/* useAppCtx(s => s.book, { name: "storeA" }) is from storeA */}
    <AppB />
  </AppCtxProvider>
</AppCtxProvider>;

function AppB() {
  // Read from parent storeB, book.title = "B"
  const currentBook = useAppCtx((s) => s.book); // or useAppCtx(s => s.book, { name: "storeB" })

  const layerAbook = useAppCtx((s) => s.book, { name: "storeA" }); // book.title = "A"

  // AppB want to change data in context StoreA layer
  const setLayerAbook = useAppCtx.useSet((s) => s.book, {
    name: "storeA",
  });

  const handleSetLayerABook = (text) => {
    setLayerAbook((b) => {
      if (b.title !== "A") {
        console.error("title in storeA should be A");
      }

      b.title = text;
    });
  };
}

Each store is independent.


🌐 Server-Side Rendering (SSR)

Signal Ctx is SSR-safe.

  • Uses useSyncExternalStore
  • Identical snapshot logic on server & client
  • No shared global state between requests

⚠️ Caveats

  • No middleware
  • No devtools
  • No persistence
  • Mutation-based updates by design

Best suited for:

  • UI state
  • Lightweight global stores
  • flexible shared state

🧪 TypeScript

Fully typed with generics and inferred selectors.

const count = useAppCtx((s) => s.count); // number

📄 License

MIT


⭐ Philosophy

signalctx is intentionally small.

It favors:

  • Explicit ownership
  • Predictable updates
  • Minimal abstraction

If you understand React, you understand signalctx.