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

@optiwari.india/react-event-state

v1.0.0

Published

An event-driven React state primitive using useSyncExternalStore with WeakMap deduplication

Readme

@optiwari.india/react-event-state

An event-driven React state primitive using React 18/19's useSyncExternalStore with automatic listener deduplication, WeakMap garbage collection, and strict TypeScript inference.

npm version TypeScript License: MIT


Highlights

  • ⚡ Zero Tearing & Concurrent Safe: Built natively with React 18/19's useSyncExternalStore.
  • 🔄 Listener Deduplication: 100 components listening to the same event share one underlying DOM listener.
  • 🧹 Automatic Garbage Collection: Uses a WeakMap<EventTarget, Map<string, Store>> registry with reference-counted cleanup so unmounted components and detached DOM nodes never leak memory.
  • 🌐 SSR & Universal Safe: No ReferenceError: document is not defined in Next.js, Remix, or Node SSR environments.
  • 🎯 Rich Target Support: Accepts document, window, direct HTMLElements, React RefObjects (useRef), and lazy getter functions.
  • 🧠 Full TypeScript Inference: Automatically infers MouseEvent, KeyboardEvent, UIEvent, and custom events from standard DOM maps (DocumentEventMap, WindowEventMap, HTMLElementEventMap).
  • 🏭 Companion Factory: Includes createEventState() for pre-declaring reusable event streams at module or domain scope.

Installation

npm install @optiwari.india/react-event-state

Peer dependencies: react >= 18.0.0


Quick Start

1. Document Events (Default Target)

By default, useEventState targets document:

import React from "react";
import { useEventState } from "@optiwari.india/react-event-state";

export function App() {
  // Strongly typed as MouseEvent | null
  const click = useEventState("click");

  return (
    <div>
      <h3>Click anywhere</h3>
      {click ? (
        <p>Last clicked at ({click.clientX}, {click.clientY})</p>
      ) : (
        <p>No clicks yet</p>
      )}
    </div>
  );
}

2. Window Events

import React from "react";
import { useEventState } from "@optiwari.india/react-event-state";

export function WindowWatcher() {
  // Strongly typed as UIEvent | null
  const resize = useEventState("resize", window, { passive: true });

  return (
    <div>
      <p>Latest resize timestamp: {resize?.timeStamp ?? "None"}</p>
    </div>
  );
}

3. Element Target via React useRef

Passing a React ref is fully supported; listeners attach automatically once the DOM node commits:

import React, { useRef } from "react";
import { useEventState } from "@optiwari.india/react-event-state";

export function ButtonTracker() {
  const buttonRef = useRef<HTMLButtonElement>(null);
  // Automatically attaches to the DOM node when mounted
  const click = useEventState("click", buttonRef);

  return (
    <div>
      <button ref={buttonRef}>Click Me</button>
      <p>Clicked: {click ? "Yes" : "Not yet"}</p>
    </div>
  );
}

4. Custom Initial Value

// Return type is MouseEvent | "idle"
const click = useEventState("click", document, { initialValue: "idle" as const });

5. Factory Pattern (createEventState)

Define reusable event streams at module scope or subscribe outside React:

import { createEventState } from "@optiwari.india/react-event-state";

// Safe to define at module level even during SSR
export const globalKeydown = createEventState("keydown");

// In React:
function ShortcutListener() {
  const keyEvent = globalKeydown.use();
  return <div>Key: {keyEvent?.key}</div>;
}

// Outside React (e.g. state machines or vanilla analytics):
const unsubscribe = globalKeydown.subscribe((event) => {
  console.log("Key pressed:", event.key);
});

Architecture & Design Decisions

1. Why useSyncExternalStore over useState + useEffect?

A naive implementation using useState and useEffect creates multiple severe problems:

  1. Duplicate Listeners: Every mounted component registers its own duplicate listener on the DOM target.
  2. Concurrent Tearing: React 18/19 concurrent rendering transitions can read differing event values across components in the same tree.
  3. Mounting Render Cascade: useEffect runs asynchronously after paint, causing extra re-render cycles and potential hydration mismatch during SSR.

useSyncExternalStore guarantees:

  • Synchronous snapshot consistency without tearing.
  • Unified subscription management.
  • Complete hydration safety via getServerSnapshot().

2. Store Registry & Deduplication

                  target + eventName + options
                                │
               ┌────────────────┴────────────────┐
               ▼                                 ▼
      Component A: useEventState        Component B: useEventState
               │                                 │
               └────────────────┬────────────────┘
                                ▼
                           EventStore
                 (Single DOM Event Listener)

The internal registry uses:

WeakMap<EventTarget, Map<string, InternalEventStore>>
  • Target deduplication: If 50 components subscribe to "keydown" on document, exactly one document.addEventListener("keydown", ...) is registered.
  • Option discrimination: Separate stores are cleanly isolated if different capture/passive options are used (e.g. { capture: true } vs { capture: false }).
  • Zero Memory Leaks: The WeakMap ensures that if an EventTarget (such as a temporary DOM node) is removed from the DOM and holds no other references, it is eligible for garbage collection immediately.
  • Reference Counting with Microtask Deferral: When the last subscriber unmounts, DOM listeners are detached (removeEventListener) and registry entries are purged. Cleanup is deferred across microtasks so that React re-renders or StrictMode double-invocations do not unnecessarily churn DOM listeners or drop snapshots.

Edge Cases & Limitations

[!IMPORTANT] Event State vs Event Action: An event in the DOM is an instantaneous action. State in React is a persistent snapshot.

  1. No preventDefault() / stopPropagation() in Render: By the time React schedules and executes a component render, the browser has already finished dispatching the DOM event. Calling event.preventDefault() inside your component body or useEffect has no effect. Use traditional DOM event handler props (onClick={...}) when you need imperative flow control or default cancellation.

  2. Browser Post-Dispatch Nullification: The DOM specification mandates that browsers set event.currentTarget to null once an event finishes dispatching. Therefore, event.currentTarget will be null when inspected in your component. Use event.target instead.

  3. DOM Subtree Retention: Holding a reference to a raw DOM Event object keeps a reference to event.target. If event.target was a DOM node that later unmounts, it will remain in memory until the next event fires or the subscribing component unmounts.


API Reference

useEventState(eventName, target?, options?)

React hook to subscribe to DOM events as state.

  • eventName (string): Name of the event to observe (e.g. "click", "resize", "keydown").
  • target (TargetResolvable, optional): Target source. Defaults to document. Can be:
    • EventTarget (window, document, HTMLElement)
    • React RefObject (useRef)
    • Getter function (() => EventTarget | null)
    • null or undefined
  • options (EventStateOptions, optional):
    • capture (boolean): Use capture phase. Defaults to false.
    • passive (boolean): Hint that handler will not call preventDefault(). Defaults to false.
    • initialValue (TInitial): Initial value before any event fires. Defaults to null.

createEventState(eventName, target?, options?)

Factory creating an EventStateSource object.

  • .use(): React hook bound to this store.
  • .subscribe(listener): Non-React subscription function returning an unsubscribe callback.
  • .getSnapshot(): Returns latest event or initial value.
  • .getTarget(): Resolves the target instance.

License

MIT © 2026