# @wwog/react

A practical React component library providing declarative flow control and common UI utility components to make your React code more concise and readable.

[![npm version](https://img.shields.io/npm/v/@wwog/react.svg)](https://www.npmjs.com/package/@wwog/react)
[![ESM](https://img.shields.io/badge/📦-ESM%20only-brightgreen.svg)](https://nodejs.org/api/esm.html)

---

[中文文档](./README_zh.md)

[wiki](https://deepwiki.com/wwog/react)

## AI-friendly

> This library is designed with AI-assisted development in mind — declarative, type-safe, and predictable.

[`use-wwog-react.md`](./use-wwog-react.md) is a Claude Code **skill** (it ships with `name` + `description` frontmatter). Install it so Claude Code prefers `@wwog/react` declarative components over hand-rolled patterns:

| Scope | How to install |
|---|---|
| **Project** (shared with team) | Copy to `.claude/skills/use-wwog-react/SKILL.md` |
| **User** (all your projects) | Copy to `~/.claude/skills/use-wwog-react/SKILL.md` |

The file already contains valid skill frontmatter — no further configuration is needed; Claude Code auto-discovers it on the next session.

## Installation

```bash
# Using npm
npm install @wwog/react

# Using yarn
yarn add @wwog/react

# Using pnpm
pnpm add @wwog/react
```

## Features

- **ESModule only**: Modern module system support
- **Full TypeScript support**: Written in TypeScript with complete type definitions
- **Zero dependencies**: Only React and React DOM as peer dependencies
- **Declarative flow control**: JSX-style conditional rendering and flow control components
- **Utility components**: Simple and practical common UI utility components
- **Lightweight and efficient** Excellent performance and compact size
- **Tree-shakable**: per-module ESM output, so an import pulls only the modules it uses — importing `cx` alone ships a few hundred bytes
- **Events & lifetime**: a VS Code-compatible event system (`Emitter` / `Event`) with `DisposableStore` / `DisposableMap` for resource lifetime

## Components & Usage

### Flow Control Components

#### `<If>`

A declarative conditional rendering component, similar to if-else statements but used in JSX.

```tsx
import { If } from "@wwog/react";

function Example({ count }) {
  return (
    <If condition={count > 10}>
      <If.Then>
        <p>Count is greater than 10</p>
      </If.Then>
      <If.ElseIf condition={count > 5}>
        <p>Count is greater than 5</p>
      </If.ElseIf>
      <If.Else>
        <p>Count is 5 or less</p>
      </If.Else>
    </If>
  );
}
```

#### `<Switch>`, `<Case>`, `<Default>`

A declarative and type-safe alternative to JavaScript's switch statement.

```tsx
import { Switch } from "@wwog/react";

function Example({ status }) {
  return (
    <Switch value={status}>
      <Switch.Case value="loading">
        <Loading />
      </Switch.Case>
      <Switch.Case value="success">
        <Success />
      </Switch.Case>
      <Switch.Case value="error">
        <Error />
      </Switch.Case>
      <Switch.Default>
        <p>Unknown status</p>
      </Switch.Default>
    </Switch>
  );
}
```

#### `<When>` (v1.1.5+)

A concise conditional rendering component supporting multiple logic combinations. More succinct than <If>, suitable for simple conditions.

```jsx
import { When } from "@wwog/react";

function Example() {
  const isAdmin = useIsAdmin();
  const isLoading = useIsLoading();
  const hasErrors = useHasErrors();

  return (
    <>
      {/* Render when all conditions are true */}
      <When all={[isAdmin, !isLoading]}>
        <AdminPanel />
      </When>

      {/* Render when any condition is true */}
      <When any={[isLoading, hasErrors]} fallback={<ReadyContent />}>
        <LoadingOrErrorMessage />
      </When>

      {/* Render when all conditions are false */}
      <When none={[isAdmin, isLoading]}>
        <RegularUserContent />
      </When>
    </>
  );
}
```

#### `<True>` / `<False>` (v1.1.6+)

Helper components for simple boolean conditional rendering.

```tsx
import { True, False } from "@wwog/react";

function Example({ isActive }) {
  return (
    <>
      <True condition={isActive}>
        <p>Active</p>
      </True>
      <False condition={isActive}>
        <p>Inactive</p>
      </False>
    </>
  );
}
```

- `<True condition={...}>`: Renders children when condition is true.
- `<False condition={...}>`: Renders children when condition is false.

#### `<Toggle>`

A declarative toggle component that switches values among predefined options and passes them to child components via specified props, supporting custom toggle logic.

```tsx
import { Toggle } from "@wwog/react";

<Toggle
  options={["light", "dark"]}
  render={(value, toggle) => {
    /* xxx */
  }}
/>;
```

- `options`: Array of values to toggle between.
- `index`: Initial Options index.
- `next`: Custom toggle logic function.
- `render`: Render Function.

### Utility Components

#### `<ArrayRender>`

Efficiently render array data, supports filtering, sorting, and custom rendering. Optimized for performance with minimal loops.

```tsx
import { ArrayRender } from "@wwog/react";

function UserList({ users }) {
  return (
    <ArrayRender
      items={users}
      filter={(user) => user.active}
      sort={(a, b) => a.name.localeCompare(b.name)}
      renderItem={(user, index) => (
        <div key={user.id}>
          {index + 1}. {user.name}
        </div>
      )}
      renderEmpty={() => <div>No users found</div>}
    />
  );
}
```

- `items`: Array of data to render
- `renderItem`: Function to render each item, receives (item, index) as parameters
- `filter`: Optional filter function to filter items
- `sort`: Optional sort function for array sorting, uses standard comparison function (a, b) => number
- `renderEmpty`: Optional function to render content when array is empty

**Performance Note**: When no sorting is needed, filtering is done during the map loop for optimal performance. When sorting is provided, filtering is applied first, then sorting, to minimize operations.
```

#### `<Clamp>` (added in v1.2.14, removed in v1.3.0)

> **Removed — do not use.** The compatibility problem is too big: the desktop web page works well, h5 has a problem. `Clamp` is no longer exported, so `import { Clamp }` fails.

For a fixed number of lines, use CSS directly — no component needed:

```css
.clamp-2 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
```

#### `<Pipe>` (v1.1.7+)

A declarative data pipeline component for multi-step data transformation and chaining.

> Declarative data processing, replacing nested function calls.
> Improves code readability and logic clarity.
> Suitable for data cleaning, formatting, etc.

```tsx
import { Pipe } from "@wwog/react";

function Example({ users }) {
  return (
    <Pipe
      data={users}
      transform={[
        (data) => data.filter((user) => user.active),
        (data) => data.map((user) => user.name),
      ]}
      render={(names) => <div>{names.join(", ")}</div>}
      fallback={<div>No Data</div>}
    />
  );
}
```

- `data`: Initial data.
- `transform`: Array of transformation functions, applied in order.
- `render`: Render the final result.
- `fallback`: Content to render if result is null/undefined.

#### `<Scope>` (v1.1.7+)

Provides a local scope for children, declaratively defines temporary variables, and simplifies complex rendering logic.

> Avoids defining temporary state or calculations outside the component.
> Declaratively defines local variables for better self-containment.
> Suitable for forms, computation-heavy rendering, etc.

```tsx
import { Scope } from "@wwog/react";

function Example() {
  return (
    <Scope let={{ count: 1, text: "Hello" }}>
      {({ count, text }) => (
        <div>
          {text} {count}
        </div>
      )}
    </Scope>
  );
}

// Function-style let is supported
<Scope
  let={(props) => ({ total: props.items.length })}
  props={{ items: [1, 2] }}
  fallback={<div>Empty</div>}
>
  {({ total }) => <div>Total: {total}</div>}
</Scope>;
```

- `let`: Object or function defining scope variables.
- `props`: Props passed to the let function.
- `children`: Render function for scope variables.
- `fallback`: Fallback content when empty.

#### `<DateRender>` (v1.2.3+)

A declarative component for formatting and rendering dates, simple to use with support for custom formatting.

```tsx
import { DateRender } from "@wwog/react";

function Example() {
  return (
    <>
      {/* Using default formatting */}
      <DateRender source="2025-05-06">
        {(formatted) => <div>Date: {formatted}</div>}
      </DateRender>

      {/* Using custom formatting */}
      <DateRender
        source={new Date()}
        format={(date) => date.toLocaleDateString("en-US")}
      >
        {(formatted) => <div>Date: {formatted}</div>}
      </DateRender>
    </>
  );
}
```

- `source`: The input date to render (Date object, ISO string, or timestamp).
- `format`: Optional function to format the date, defaults to `toLocaleString()`.
- `children`: Function to render the formatted date, receives the formatted date as an argument.

#### `<Observer>` (v1.3.1+)

A declarative Intersection Observer component for lazy loading, infinite scrolling, and viewport-based interactions.

```tsx
import { Observer } from "@wwog/react";

function LazyImage({ src, alt }) {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <Observer
      onIntersect={(entry) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
        }
      }}
      threshold={0.1}
      triggerOnce
    >
      <div className="image-container">
        {isVisible ? (
          <img src={src} alt={alt} />
        ) : (
          <div className="placeholder">Loading...</div>
        )}
      </div>
    </Observer>
  );
}

// Infinite scrolling example
function InfiniteList({ items, onLoadMore }) {
  return (
    <div>
      {items.map((item) => (
        <div key={item.id}>{item.content}</div>
      ))}
      <Observer
        onIntersect={(entry) => {
          if (entry.isIntersecting) {
            onLoadMore();
          }
        }}
        rootMargin="100px"
      >
        <div>Loading more...</div>
      </Observer>
    </div>
  );
}
```

- `onIntersect`: Callback function triggered when intersection changes, receives IntersectionObserverEntry as parameter.
- `threshold`: Intersection threshold, can be a number (0-1) or array of numbers, defaults to 0.
- `root`: Root element for intersection observation, defaults to viewport.
- `rootMargin`: Root margin for expanding/shrinking the root's bounding box, defaults to "0px".
- `triggerOnce`: Whether to trigger only once, defaults to false.
- `disabled`: Whether to disable observation, defaults to false.
- `children`: Child elements to observe.
- `className`: CSS class name for the wrapper element.
- `style`: Inline styles for the wrapper element.

#### `<Repeat>` (v1.3.13+)

A declarative repeat-render component, commonly used for skeleton screens and placeholders.

```tsx
import { Repeat } from "@wwog/react";

function SkeletonList() {
  return (
    <Repeat times={5}>
      {(i) => <SkeletonItem key={i} />}
    </Repeat>
  );
}
```

- `times`: Number of times to repeat. Renders nothing when `<= 0`.
- `children`: Render function receiving the current 0-based index.

#### `<Portal>` (v1.3.13+)

A declarative `createPortal` wrapper that renders children into a specified DOM node. Handles SSR safely by deferring mount until the client is ready.

```tsx
import { Portal } from "@wwog/react";

// Render into document.body (default)
function Modal({ children }) {
  return <Portal>{children}</Portal>;
}

// Render into a specific element
function Tooltip({ children }) {
  return (
    <Portal to={document.getElementById("overlay-root")}>
      {children}
    </Portal>
  );
}

// Disable portal and render inline
function ConditionalPortal({ usePortal, children }) {
  return <Portal disabled={!usePortal}>{children}</Portal>;
}
```

- `to`: Target DOM element to mount into. Defaults to `document.body`.
- `disabled`: When `true`, renders children inline without a portal. Defaults to `false`.
- `children`: Child elements to render into the portal.

#### `<Boundary>` (v1.3.13+)

A declarative Error Boundary wrapper with render-prop fallback and reset capability.

```tsx
import { Boundary } from "@wwog/react";

function App() {
  return (
    <Boundary
      fallback={(error, reset) => (
        <div>
          <p>Something went wrong: {error.message}</p>
          <button onClick={reset}>Retry</button>
        </div>
      )}
      onError={(error, info) => reportError(error, info)}
    >
      <RiskyComponent />
    </Boundary>
  );
}
```

- `fallback`: Render function called when an error is caught. Receives `(error: Error, reset: () => void)`.
- `onError`: Optional callback for error reporting (e.g. logging to Sentry).
- `children`: Child elements to protect.

#### `<FocusTrap>` (v1.4.0+)

A focus trap component that constrains keyboard focus cycling to focusable elements within a container, with support for custom key mappings and navigation logic.

```tsx
import { FocusTrap } from "@wwog/react";

// Default Tab trapping
<FocusTrap>
  <input />
  <button>Save</button>
</FocusTrap>

// Arrow key navigation
<FocusTrap keyMap={{ ArrowDown: "next", ArrowUp: "prev" }}>
  <div>
    <button>Item 1</button>
    <button>Item 2</button>
  </div>
</FocusTrap>

// Cross-list navigation — items from multiple lists
// seamlessly cross in a single focus order
<FocusTrap keyMap={{ ArrowDown: "next", ArrowUp: "prev" }}>
  <div>
    <h4>List A</h4>
    <button>A-1</button>
    <button>A-2</button>
  </div>
  <div>
    <h4>List B</h4>
    <button>B-1</button>
    <button>B-2</button>
  </div>
</FocusTrap>
```

- `keyMap`: Custom key-to-direction mapping. Default `{ Tab: "next" }` (Shift+Tab auto-maps to `"prev"`). Example: `{ ArrowDown: "next", ArrowUp: "prev" }`.
- `onNavigate`: Custom focus resolution — return the element to focus, or `null` to use default cycling.
- `autoFocus`: Auto-focus the first tabbable element on mount.
- `restoreFocus`: Restore focus to the previously focused element on unmount.
- `disabled`: Temporarily disable the trap.
- `focusableOptions`: Options passed to `getTabbableElements` (e.g. `{ includeContainer: true }`).

#### `<SizeBox>`

Create a fixed-size container for layout adjustment and spacing control.

> v1.1.8: Fixed SizeBox not working in 'flex' layouts, add classname props

```tsx
import { SizeBox } from "@wwog/react";

function Layout() {
  return (
    <div>
      <Header />
      {/* Vertical spacing */}
      <SizeBox height={20} />
      <Content />
      {/* Fixed-size container */}
      <SizeBox width={200} height={150}>
        <SideContent />
      </SizeBox>
    </div>
  );
}
```

#### `<Styles>` (v1.2.7+)

Categorically write styles and basic string styles, with built-in functionality similar to clsx for combining type description object values, supporting duplicate class name removal and nesting.

```tsx
import { Styles } from "@wwog/react";
import clazz from "./index.module.css";

function Example() {
  return (
    <Styles
      className={{
        base: "p-2 bg-white",
        hover: "hover:bg-gray-100",
        active: "active:bg-gray-200",
        focus: "focus:ring-2",
        other: "button",
      }}
    >
      <Styles className={clazz.button}>
        <button>Click me</button>
      </Styles>
    </Styles>
  );
}
```

You can also use a container wrapper element:

```tsx
<Styles
  className={{
    base: ["p-2"],
    hover: { "hover:bg-blue-500": true },
  }}
  asWrapper="span"
>
  Content
</Styles>
```

- `className` [string | StylesDescriptor]: Category object for class names, all values in the object will be merged
- `asWrapper` [boolean | HTMLElementType]: Whether to generate a wrapper containing all classNames, default is false, pass tag name like 'div' or 'span'
- `children` : Only works with a single child element; if there are multiple child elements, please pass asWrapper to write types and avoid ambiguity

### hooks

#### `useEvent` / `useEventValue` / `useEventCallback` (v1.6.0+)

React bindings for the event system: subscribe for as long as a component is alive, render the last payload of an event, and hand out callbacks that are stable *and* fresh.

```tsx
import { useEvent, useEventValue, useEventCallback } from "@wwog/react";

function Chat({ filter }: { filter: string }) {
  const [messages, setMessages] = useState<string[]>([]);
  // subscribes on mount, unsubscribes on unmount, always calls the latest closure
  useEvent(socket.onMessage, (message) => {
    if (matches(message, filter)) setMessages((all) => [...all, message]);
  });
  return <ul>{messages.map((m) => <li key={m}>{m}</li>)}</ul>;
}

function Upload() {
  const percent = useEventValue(uploader.onProgress, 0); // 0 until the first fire
  return <progress value={percent} max={100} />;
}

const save = useEventCallback(() => persist(draft)); // one reference, latest draft
const saveDebounced = useMemo(() => debounce(save, 300), [save]);
```

| Hook | Signature | What it does |
|---|---|---|
| `useEvent` | `useEvent(event, handler): void` | Subscribes on mount, unsubscribes on unmount; a re-render never resubscribes and the handler always sees the latest props/state. Conditional subscription: `useEvent(enabled ? event : Event.None, handler)`. |
| `useEventValue` | `useEventValue(event, initial): T` | Keeps the most recent payload and re-renders on each fire. `initial` is only used on mount. |
| `useEventCallback` | `useEventCallback(fn): F` | One reference for the component's whole lifetime that still reads the latest closure — the `useCallback` you no longer have to keep a dependency list for. |

**The two things that bite:**

- **The event must be a stable reference.** `emitter.event` is cached and safe to pass inline; `Event.map(ev, fn)` returns a new event per call, so memoize derived events (`useMemo(() => Event.map(ev, fn), [ev])`) or bind them to a `DisposableStore` — otherwise every render swaps the source.
- **A subscription belongs to an effect.** `StrictMode` runs effects twice (mount → unmount → mount), so a store created in `useMemo`/`useRef` and disposed in the cleanup is already released on the second mount and the subscription silently stops working. The hooks own this for you; when you manage subscriptions by hand, create the `DisposableStore` *inside* the effect.

Fires that happen between render and the effect are lost (events are hot) — `Event.buffer` is the opt-in fix, and `useEventValue` / `ValueWithChangeEvent` are for "I need the current value as well". `useEventValue` follows React's `Object.is` bailout: firing twice with the same reference re-renders once, and `Event.latch` is how you make "only when it really changed" explicit.


#### useControlled

- Applied to states that can be controlled or uncontrolled components

#### useScreen (v1.3.5+)

> Return the current breakpoint name

- Supports passing in custom breakpoints, defaults to the same breakpoint definitions as TailwindCSS

This hook is implemented based on listening. If useScreen needs to be used multiple times without changing the passed parameters, it is recommended to wrap Context

Development notes: Internally implemented via `mediaQuery`, it does not listen to a specific breakpoint but is optimized to listen only to the previous and next breakpoints of the current breakpoint for better performance.

### utils

> Internal functions used by some components, which can also be used if needed

#### `createExternalState` (v1.2.9+, useGetter added in v1.2.13)
> v1.4.2: Remove transform options
> v1.3.14: Breaking: `use()` renamed to `useState()` for React 19 compiler compatibility (the compiler requires hook names to start with `use`; the old `use` method was misidentified as a non-hook)
> v1.2.21: Refactor the API to move sideeffects into options and enhance support for the transform interface
> v1.2.13: add useGetter
> Breaking: `sideEffect` replaced by `onSet` and `onChange` for clearer callback semantics

**Migration (v1.3.13 → v1.3.14)**

```diff
- const [theme, setTheme] = themeState.use();
+ const [theme, setTheme] = themeState.useState();
```

`useGetter()` is unchanged.

> A lightweight external state management utility that allows you to create and manage state outside the React component tree while maintaining perfect integration with components.

### `createStorageState` (v1.3.2+)

> Extends from createExternalState and uses storage to persist state, supports `localStorage` and `sessionStorage`

- `createStorageState<T>(key, initialState, options?)`: Creates persisted state
  - `options.onSet`: Invoked on every `set()` (storage write happens first, then the user callback)
  - `options.onChange`: Invoked only when the value actually changes
  - `options.storageType`: `'local'` | `'session'`, optional, defaults to `'local'`
  - `options.syncAcrossTabs` (v1.5.0+): follow writes from other tabs, off by default. When on, `storage` events apply the other tab's value through `set` (`onSet` / `onChange` / `useSelector` keep working); the remote value is never written back, and a remote removal / `clear()` returns the state to `initialState` without resurrecting it in storage. `sessionStorage` is per-tab, so no such event arrives

> v1.5.0: a write whose serialized result matches what is already stored is skipped. Setting an equal-content object is common, and re-serializing plus rewriting the whole value on every `set` is the expensive step on this path. The value restored at creation counts as the baseline; a parse failure leaves no baseline so the next `set` overwrites the bad entry.

```tsx
import { createExternalState } from "@wwog/react";

// Create a global theme state
const themeState = createExternalState("light", {
  onChange: (newTheme, oldTheme) => {
    console.log(`Theme changed from ${oldTheme} to ${newTheme}`);
  },
});

// Get or modify state from anywhere
console.log(themeState.get()); // 'light'
themeState.set("dark");

// Use the state in components
function ThemeConsumer() {
  const [theme, setTheme] = themeState.useState();

  return (
    <div className={theme}>
      Current theme: {theme}
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Toggle theme
      </button>
    </div>
  );
}

// For read-only access (v1.2.13+)
function ReadOnlyThemeConsumer() {
  const theme = themeState.useGetter();

  return <div>Current theme is: {theme}</div>;
}
```

**Selective subscription (v1.5.0+)**

`useState()` subscribes to the whole state, so any field re-renders. With an object state, `useSelector` subscribes to one slice only:

```tsx
import { createExternalState, shallowEqual } from "@wwog/react";

const appState = createExternalState({ name: "wwog", age: 1, theme: "light" });

function NameLabel() {
  // changing age / theme does NOT re-render this component
  const name = appState.useSelector((s) => s.name);
  return <span>{name}</span>;
}

// A selector that builds a new object/array returns a fresh reference every call,
// so it must be given an equality function or unrelated writes re-render too
const head = appState.useSelector((s) => ({ name: s.name, age: s.age }), shallowEqual);

// Slice subscription outside components: the listener is skipped while the slice is unchanged
const stop = appState.subscribeWithSelector((s) => s.age, (age, prevAge) => {
  console.log(`${prevAge} -> ${age}`);
});
stop();
```

A `set` still notifies every subscriber, but each consumer only compares its own slice, so an unrelated component pays a comparison instead of a render.

- `createExternalState<T>(initialState, options?)`: Creates a state accessible outside components

  - `initialState`: Initial state value
  - `options.onSet`: Optional callback invoked on every `set()` call, even when the value is unchanged
  - `options.onChange`: Optional callback invoked only when the stored value actually changes (compared via `Object.is`)
  - `options.notify` (v1.5.0+): when subscribers are notified — `'sync'` (default, done before `set` returns) or `'microtask'` (several `set` calls in one task notify once, intermediate states skipped, while `onSet` / `onChange` still run for every `set`)
  - Returns an object with methods:
    - `get()`: Get the current state value
    - `set(newState)`: Update the state value
    - `useState()`: React Hook, returns `[state, setState]` for using this state in components (same return shape as React `useState`)
    - `useGetter()`: React Hook that only returns the state value, useful when you only need to read the state
    - `useSelector(selector, isEqual?)` (v1.5.0+): Subscribe to the slice returned by `selector`; no re-render while it compares equal. `isEqual` defaults to `Object.is`
    - `subscribe(listener)` (v1.5.0+): Subscribe to any change outside components; returns an unsubscribe function
    - `subscribeWithSelector(selector, listener, options?)` (v1.5.0+): Slice subscription outside components, with `options.isEqual` / `options.fireImmediately`; on the first change `prevSlice` is the slice as of subscribing

> `set` must be given a **new reference**: `set((prev) => { prev.list.push(x); return prev })` compares `Object.is`-equal to the previous value, counts as "unchanged", and notifies nobody.

> A subscriber (`subscribe` / `subscribeWithSelector`) that throws is caught, logged with `console.error`, and skipped so the remaining subscribers are still notified — and the `onSet` / `onChange` callbacks, which run after notification, still execute.

> In development, a `useSelector` whose selector returns a new reference with shallow-equal contents warns once per hook and suggests passing `isEqual`. The check reads the bare `process.env.NODE_ENV`, which Vite/webpack replace at build time, so the hint is dropped from production builds; where nothing replaces it and `process` is absent (native ESM, esbuild without `define`), it counts as development and the hint shows.

  Use cases:

- Global state management (themes, user settings, etc.)
- Cross-component communication
- Reactive state in services or utility classes
- Sharing state with non-React code

#### `shallowEqual` (v1.5.0+)

One-level, key-wise `Object.is` comparison for arrays and plain objects — pass it as `isEqual` to `useSelector` / `subscribeWithSelector`.

```tsx
const head = appState.useSelector((s) => ({ name: s.name, age: s.age }), shallowEqual);
```

> Only arrays and plain objects are compared key-wise: `Date` / `Map` / `Set` / class instances have no own enumerable keys, so a key-wise pass would call two different values equal. Those types fall back to reference equality — an extra render is preferable to a missed update.

#### `formatDate`

A relatively standard date formatting function

#### `childrenLoop`

Interruptible child node traversal, enabling some branch processes to have ultimate performance

#### `Counter`

Incrementally class

#### `safePromiseTry` (v1.2.10+)

Support `Promise.try` Use `Promise.try`, otherwise use internal implementation

#### `cx` (v1.2.5+)

An efficient CSS class name merging utility function, similar to `clsx` or `classnames`, but automatically removes duplicate class names.

```tsx
import { cx } from "@wwog/react";

function Example({ isActive, isDisabled }) {
  return (
    <div
      className={cx("base-class", ["array-class-1", "array-class-2"], {
        "active-class": isActive,
        "disabled-class": isDisabled,
      })}
    >
      Content
    </div>
  );
}
```

Supports various parameter types:

- String: `"class1 class2"`
- String array: `["class1", "class2"]`
- Object: `{ "class1": true, "class2": false }`
- Any combination of the above types

#### `Emitter` / `Event` (v1.6.0+)

A push-based event system, ported in full from VS Code's `src/vs/base/common/event.ts` (MIT). An event **is a function**: calling it subscribes, and the returned handle unsubscribes.

```ts
import { Emitter, DisposableStore } from "@wwog/react";

class Document {
  private readonly _onDidChange = new Emitter<string>();
  readonly onDidChange = this._onDidChange.event; // read-only to the outside

  edit(text: string) {
    this._onDidChange.fire(text);
  }
}

const store = new DisposableStore();
const doc = new Document();
store.add(doc.onDidChange((text) => console.log(text)));

doc.edit("hello"); // fires
store.dispose();   // unsubscribes everything
```

- **Derive**: `Event.map`, `filter`, `forEach`, `reduce`, `latch`, `once`, `onceIf`, `any`, `split`, `chain`, `signal`, `defer`, `debounce`, `throttle`, `accumulate`, `buffer`, `toPromise`, `forward`, `runAndSubscribe`, `fromDOMEventEmitter`, `fromNodeEventEmitter`, `fromObservable`.
- **Specialised emitters**: `PauseableEmitter`, `DebounceEmitter`, `MicrotaskEmitter`, `AsyncEmitter` (sequential async delivery with `waitUntil` and a cancellation token), `EventMultiplexer`, `DynamicListEventMultiplexer`, `EventBufferer`, `Relay`, `ValueWithChangeEvent`.
- **Leak detection**: pass `leakWarningThreshold` to an `Emitter` (or call `setGlobalLeakWarningThreshold`) to have listeners counted per call site, reported as `ListenerLeakError` and refused as `ListenerRefusalError` when far over; `_profName` enables `EventProfiling`.
- Events are **hot** — a late subscriber misses earlier fires. For "current value plus changes" use `ValueWithChangeEvent` or `createExternalState`.
- A derived event that third parties can reach (`Event.map(src, fn, store)`) should be given a `DisposableStore`, otherwise a forgotten unsubscribe leaks a listener on the source.

#### `DisposableStore` / `DisposableMap` (v1.6.0+)

Lifetime helpers used by the event API, and useful on their own for any resource exposing `dispose()`.

```ts
import { DisposableStore, DisposableMap, type CompatDisposable } from "@wwog/react";

const store = new DisposableStore();
store.add(subscription);        // dispose() releases all; later adds release on add
store.clear();                  // releases the contents, keeps the store usable

const perKey = new DisposableMap<string, CompatDisposable>();
perKey.set("a", someResource);  // overwriting a key releases the previous value
perKey.deleteAndDispose("a");
```

The disposal protocol is `dispose()`, and where the runtime provides `Symbol.dispose` (Chrome 125+, Safari 18.4+, Firefox 134+, Node 20+) it is attached too, so `using sub = emitter.event(handler)` works as well. Your own classes can opt in with `withDisposeSymbol(MyClass.prototype)`, which points `Symbol.dispose` at their `dispose()`. The symbol is kept out of the public types on purpose: naming the global `Disposable` would break `tsc` for projects whose `lib` stops before `esnext.disposable`.

> `src/utils/event.ts` is derived from Microsoft's VS Code (MIT, Copyright (c) Microsoft Corporation); see the file header for the exact attribution and the list of deviations.

## License
