UNPKG

seitu

Version:
157 lines (156 loc) • 4.61 kB
import { deepEqual } from "fast-equals"; import * as React from "react"; //#region src/react/hooks/index.ts const UNSET = Symbol("seitu.unset"); /** * Subscribe to a reactive value. Pass a handle or a factory (run once; recreated * when `deps` change). * * Sources with `getServer` (every `seitu/web` handle) use it for SSR and * hydration; others use `get()`. `getSnapshot` skips selector/equality when * source, selector, and version are unchanged — keep `selector` stable. * Factories run during render (twice under StrictMode); keep them cheap and * create long-lived resources at module scope. * * @example Inline subscription * ```tsx twoslash title="/app/page.tsx" * 'use client' * * import { createWebStorageValue } from 'seitu/web' * import { useSubscription } from 'seitu/react' * import * as z from 'zod' * * export default function Page() { * const value = useSubscription(() => createWebStorageValue({ * type: 'sessionStorage', * key: 'test', * defaultValue: 0, * schema: z.number(), * })) * * return <div>{value}</div> * } * ``` * * @example Instance outside of component * ```tsx twoslash title="/app/page.tsx" * 'use client' * * import { createWebStorage } from 'seitu/web' * import { useSubscription } from 'seitu/react' * import * as z from 'zod' * * const sessionStorage = createWebStorage({ * type: 'sessionStorage', * schemas: { count: z.number(), name: z.string() }, * defaultValues: { count: 0, name: '' }, * }) * * export default function Page() { * const value = useSubscription(sessionStorage) * return <div>{value.count}</div> * } * ``` * * @example Subscription with selector * ```tsx twoslash title="/app/page.tsx" * 'use client' * * import { createWebStorage } from 'seitu/web' * import { useSubscription } from 'seitu/react' * import * as z from 'zod' * * const sessionStorage = createWebStorage({ * type: 'sessionStorage', * schemas: { * count: z.number(), * name: z.string(), * }, * defaultValues: { count: 0, name: '' }, * }) * * export default function Page() { * // Re-renders only when count changes * const count = useSubscription(sessionStorage, { selector: value => value.count }) * * return <div>{count}</div> * } * ``` * * @example Element from a callback ref * ```tsx twoslash title="/app/page.tsx" * 'use client' * * import * as React from 'react' * import { createScrollState } from 'seitu/web' * import { useSubscription } from 'seitu/react' * * export default function Page() { * // A callback ref plus `deps` rebuilds on remount; `() => ref.current` binds once. * const [el, setEl] = React.useState<HTMLDivElement | null>(null) * const state = useSubscription( * () => createScrollState({ element: el, direction: 'vertical' }), * { deps: [el] } * ) * * return ( * <div ref={setEl}> * {String(state.top.reached)} * </div> * ) * } * ``` */ function useSubscription(source, options) { const { selector, deps = [], isEqual = deepEqual } = options ?? {}; const isFactory = typeof source === "function"; const subscription = React.useMemo(() => isFactory ? source() : source, [isFactory ? void 0 : source, ...deps]); const cell = React.useRef(null); const c = cell.current ??= { subscription, selector, isEqual, version: 0, snapshotVersion: -1, raw: UNSET, snapshotSelector: void 0, snapshot: UNSET, server: void 0 }; if (c.subscription !== subscription) { c.subscription = subscription; c.raw = UNSET; c.snapshot = UNSET; c.server = void 0; } c.selector = selector; c.isEqual = isEqual; const getSnapshot = React.useCallback(() => { const raw = c.subscription.get(); const sel = c.selector; if (c.snapshot !== UNSET && raw === c.raw && sel === c.snapshotSelector && c.version === c.snapshotVersion) return c.snapshot; const next = sel ? sel(raw) : raw; c.raw = raw; c.snapshotSelector = sel; c.snapshotVersion = c.version; if (c.snapshot !== UNSET && c.isEqual(c.snapshot, next)) return c.snapshot; c.snapshot = next; return next; }, [c]); const getServerSnapshot = React.useCallback(() => { const serverGet = c.subscription.getServer; if (!serverGet) return getSnapshot(); if (c.server === void 0) { const raw = serverGet(); c.server = { value: c.selector ? c.selector(raw) : raw }; } return c.server.value; }, [c, getSnapshot]); const subscribe = React.useCallback((onStoreChange) => subscription.subscribe(() => { c.version++; onStoreChange(); }), [c, subscription]); return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); } //#endregion export { useSubscription };