/** * @aquienpz/sdk/react — React hooks layered on top of the universal SDK. * * Kept in a subpath so the SSR-safe core (`@aquienpz/sdk`) stays * dependency-free of react. Import only what you need: * * import { useSlot, useSlots, AquienpzProvider } from "@aquienpz/sdk/react"; * * Pattern: wrap your app in `` once at * the root; hooks read the client from context. No prop-drilling. */ import { createContext, createElement, type ReactNode, useContext, useEffect, useMemo, useState } from "react"; import { type AquienpzClient } from ".."; import type { ResolveSlotOptions, SlotResolution } from ".."; // --------------------------------------------------------------------------- // Provider // --------------------------------------------------------------------------- const ClientContext = createContext(null); export function AquienpzProvider(props: { client: AquienpzClient; children: ReactNode }): ReactNode { return createElement(ClientContext.Provider, { value: props.client }, props.children); } export function useAquienpzClient(): AquienpzClient { const client = useContext(ClientContext); if (!client) { throw new Error("useAquienpzClient: wrap your app in ."); } return client; } // --------------------------------------------------------------------------- // Slot hooks // --------------------------------------------------------------------------- type SlotState = { /** Resolved DTO (`null` while loading or unbound). */ resolution: SlotResolution | null; /** Convenience: the CDN URL, when resolved. */ url: string | null; isLoading: boolean; error: Error | null; }; const emptyState: SlotState = { resolution: null, url: null, isLoading: true, error: null }; /** * Subscribe to a single slot. Re-resolves when the key changes or the * cache is invalidated. Returns `{resolution, url, isLoading, error}`. */ export function useSlot(slotKey: string, options: ResolveSlotOptions = {}): SlotState { const client = useAquienpzClient(); const [state, setState] = useState(emptyState); // Stabilize options across renders so the effect only refires on the // values that matter. const presetKey = options.preset ?? ""; const ttlMs = options.ttlMs ?? 60_000; useEffect(() => { let alive = true; setState((prev) => ({ ...prev, isLoading: true, error: null })); client.slots .resolve(slotKey, { preset: options.preset, ttlMs }) .then((resolution) => { if (!alive) return; setState({ resolution, url: resolution.url, isLoading: false, error: null }); }) .catch((err: unknown) => { if (!alive) return; setState({ resolution: null, url: null, isLoading: false, error: err instanceof Error ? err : new Error(String(err)) }); }); return () => { alive = false; }; }, [client, slotKey, presetKey, ttlMs, options.preset]); return state; } /** * Bulk version — fetches N keys in one round-trip. Returns a map keyed * by slot key. Pass a STABLE array reference (memoize with useMemo) to * avoid re-fetches on every render. */ export function useSlots( slotKeys: string[], options: ResolveSlotOptions = {}, ): { resolutions: Record; isLoading: boolean; error: Error | null } { const client = useAquienpzClient(); const keysHash = useMemo(() => slotKeys.join("|"), [slotKeys]); const presetKey = options.preset ?? ""; const ttlMs = options.ttlMs ?? 60_000; const [state, setState] = useState<{ resolutions: Record; isLoading: boolean; error: Error | null; }>({ resolutions: {}, isLoading: true, error: null }); useEffect(() => { let alive = true; setState((prev) => ({ ...prev, isLoading: true, error: null })); client.slots .resolveMany(slotKeys, { preset: options.preset, ttlMs }) .then((resolutions) => { if (!alive) return; setState({ resolutions, isLoading: false, error: null }); }) .catch((err: unknown) => { if (!alive) return; setState({ resolutions: {}, isLoading: false, error: err instanceof Error ? err : new Error(String(err)) }); }); return () => { alive = false; }; // slotKeys is hashed via keysHash; using it directly here would // re-fire on every render (new array identity each render). // biome-ignore lint/correctness/useExhaustiveDependencies: keysHash captures slotKeys identity }, [client, keysHash, presetKey, ttlMs]); return state; }