seitu
Version:
157 lines (156 loc) • 4.61 kB
JavaScript
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 };