UNPKG

@mongez/react-atom

Version:
442 lines (434 loc) 12.7 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); //#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) { __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } } } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion let react = require("react"); react = __toESM(react); let _mongez_atom = require("@mongez/atom"); let react_jsx_runtime = require("react/jsx-runtime"); //#region ../@mongez/react-atom/src/store.tsx /** * React context that holds the active atom store. Components that read or * write atoms via the React hooks resolve the store-scoped clone from this * context. When the context is null (no provider mounted), hooks fall back * to the module-level singleton atom, which is the right behavior for a * client-only SPA. */ const AtomStoreContext = (0, react.createContext)(null); /** * Provider that scopes atom reads and writes to a request-local `AtomStore`. * * Wrap the root of your component tree (or any subtree) with this provider * to give that subtree its own isolated copy of every atom's state. This is * the supported pattern for SSR in Next.js, Remix, and TanStack Start — * each request creates its own store, so concurrent requests cannot see * each other's state. * * Without a provider, atoms fall back to the module-level singleton (the * historical client-only behavior). */ function AtomStoreProvider({ store, initialAtoms, initialValues, children }) { const [activeStore] = (0, react.useState)(() => { const next = store ?? (0, _mongez_atom.createAtomStore)(); if (initialAtoms) for (const atomTemplate of initialAtoms) next.use(atomTemplate); if (initialValues) next.hydrate(initialValues); return next; }); (0, react.useEffect)(() => { return () => { if (!store) activeStore.destroy(); }; }, [activeStore, store]); return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AtomStoreContext.Provider, { value: activeStore, children }); } /** * Read the active atom store. Returns null when no `<AtomStoreProvider>` is * mounted in the tree above this component. */ function useAtomStore() { return (0, react.useContext)(AtomStoreContext); } function useAtom(arg) { const store = (0, react.useContext)(AtomStoreContext); if (typeof arg === "string") return store?.get(arg); return store ? store.use(arg) : arg; } //#endregion //#region ../@mongez/react-atom/src/context.tsx /** * @deprecated Re-export of `AtomStoreContext` from "./store". The context * value type changed from a key→atom record to an `AtomStore` instance; if * you only consumed this via `useAtom(key)` the migration is transparent. */ const AtomContext = AtomStoreContext; /** * Backwards-compatible alias for `<AtomStoreProvider>`. * * Maps the legacy `register` (atoms to pre-clone) to `initialAtoms`, and * `defaultValue` (record of initial atom values) to `initialValues`. * * @deprecated Use `<AtomStoreProvider>` from "./store" directly. */ function AtomProvider({ register, defaultValue, children }) { return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AtomStoreProvider, { initialAtoms: register, initialValues: defaultValue, children }); } //#endregion //#region ../@mongez/react-atom/src/react-atom.tsx /** * Build the React-aware action bag injected into every atom created via * the `atom()` factory in this package. * * Every hook in here goes through `useAtom(this)` first so that * components rendered inside an `<AtomStoreProvider>` operate on the * store-scoped clone, not the module-level template. * * Subscriptions are wired through `useSyncExternalStore` to keep React 18+ * concurrent rendering tear-free. */ function reactActions(data) { return { ...data.actions, Provider(props) { const atom = useAtom(this); (0, react.useEffect)(() => { atom.update(props.value); }, [props.value, atom]); return props.children; }, useWatch(key, callback) { const atom = useAtom(this); (0, react.useEffect)(() => { const sub = atom.watch(key, callback); return () => sub.unsubscribe(); }, [ atom, key, callback ]); }, useState() { const atom = useAtom(this); const subscribe = (0, react.useCallback)((onChange) => { const sub = atom.onChange(onChange); return () => sub.unsubscribe(); }, [atom]); const getSnapshot = (0, react.useCallback)(() => atom.value, [atom]); return [(0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot), (0, react.useCallback)((next) => { atom.update(next); }, [atom])]; }, useValue() { const atom = useAtom(this); const subscribe = (0, react.useCallback)((onChange) => { const sub = atom.onChange(onChange); return () => sub.unsubscribe(); }, [atom]); const getSnapshot = (0, react.useCallback)(() => atom.value, [atom]); return (0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot); }, use(key) { const atom = useAtom(this); const subscribe = (0, react.useCallback)((onChange) => { const sub = atom.watch(key, onChange); return () => sub.unsubscribe(); }, [atom, key]); const getSnapshot = (0, react.useCallback)(() => atom.get(key), [atom, key]); return (0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot); } }; } /** * Create a new React-aware atom. * * The returned atom carries hooks (`useState`, `useValue`, `use`, `useWatch`) * and a `<Provider>` component as instance methods. All hooks honor the * nearest `<AtomStoreProvider>` and use `useSyncExternalStore` underneath. */ function atom(data) { return (0, _mongez_atom.createAtom)({ ...data, actions: reactActions(data) }); } /** * Create a React-aware collection atom for working with arrays. */ function atomCollection(options) { return (0, _mongez_atom.atomCollection)({ ...options, actions: { ...options.actions, ...reactActions(options) } }); } //#endregion //#region ../@mongez/react-atom/src/helpers.ts /** * Create a boolean atom */ function openAtom(key, defaultOpened = false) { return atom({ key, default: defaultOpened, actions: { toggle() { this.update(!this.currentValue); }, open() { this.update(true); }, close() { this.update(false); }, useOpened() { return this.useState()[0]; } } }); } /** * Create a loading atom */ function loadingAtom(key, defaultLoading = false) { return atom({ key, default: defaultLoading, actions: { startLoading() { this.update(true); }, stopLoading() { this.update(false); }, toggleLoading() { this.update(!this.currentValue); } } }); } /** * Create a fetching atom */ function fetchingAtom(key, defaultValue = null, defaultFetching = true) { return atom({ key, actions: { startLoading() { this.change("isLoading", true); }, stopLoading() { this.change("isLoading", false); }, useLoading() { return this.use("isLoading"); }, useData() { return this.use("data"); }, useError() { return this.use("error"); }, usePagination() { return this.use("pagination"); }, success(data, pagination) { this.merge({ isLoading: false, data, pagination }); }, append(data) { const newData = [...this.value.data, ...data]; this.merge({ isLoading: false, data: newData }); }, prepend(data) { const newData = [...data, ...this.value.data]; this.merge({ isLoading: false, data: newData }); }, failed(error) { this.merge({ isLoading: false, error }); } }, default: { isLoading: defaultFetching, data: defaultValue, error: void 0, pagination: void 0 } }); } //#endregion //#region ../@mongez/react-atom/src/portal-atom.ts /** * Create a portal atom * This atom is used to create a portal (a modal, a tooltip, a dropdown, etc.) */ function portalAtom(name, opened = false) { return atom({ key: `${name}-portal`, default: { opened, data: {} }, actions: { open(data) { this.merge({ opened: true, data }); }, close() { this.change("opened", false); }, toggle(data) { if (this.get("opened")) return this.change("opened", false); this.merge({ opened: true, data }); }, useOpened() { return this.use("opened"); }, useData() { return this.use("data"); } } }); } //#endregion //#region ../@mongez/react-atom/src/ssr.tsx /** * The default DOM id used by {@link HydrateAtomsScript} and * {@link readHydration}. Override per-provider if you need to embed * multiple snapshots in one document. */ const DEFAULT_HYDRATION_SCRIPT_ID = "__mongez_atom_state"; /** * Build a JSON string suitable for embedding inside an HTML `<script>` * tag. Two safety steps beyond a plain `JSON.stringify`: * * 1. The closing tag sequence `</` is escaped to `<\/` so an atom value * containing literal HTML cannot break out of the script element. * 2. The U+2028 / U+2029 line separators (which are valid JSON but not * valid JavaScript string literals) are escaped. */ function serializeSnapshot(snapshot, options = {}) { return JSON.stringify(snapshot, options.replacer, options.space).replace(/<\/(script)/gi, "<\\/$1").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"); } /** * Convenience that snapshots a store and serializes the result in one call. * * const payload = serializeStore(serverStore); * // payload is a script-safe JSON string */ function serializeStore(store, options) { return serializeSnapshot(store.snapshot(), options); } /** * Renders an inline `<script type="application/json">` carrying a store * snapshot for the client to pick up. * * Place this once per `<AtomStoreProvider>` you want to hydrate. The * matching client-side call is {@link readHydration}. * * // server * <AtomStoreProvider store={serverStore}> * <App /> * <HydrateAtomsScript snapshot={serverStore.snapshot()} /> * </AtomStoreProvider> * * // client root * <AtomStoreProvider initialValues={readHydration() ?? undefined}> * <App /> * </AtomStoreProvider> */ function HydrateAtomsScript({ snapshot, id = DEFAULT_HYDRATION_SCRIPT_ID, nonce }) { return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("script", { id, type: "application/json", nonce, dangerouslySetInnerHTML: { __html: typeof snapshot === "string" ? snapshot : serializeSnapshot(snapshot) } }); } /** * Read a hydration snapshot embedded via {@link HydrateAtomsScript} from * the current document. * * - On the server (no `document`), returns `null`. * - When the script tag is missing, returns `null`. * - When the script body is not valid JSON, returns `null` and logs the * error via `console.error` (so a malformed payload is visible during * development but does not crash hydration). */ function readHydration(id = DEFAULT_HYDRATION_SCRIPT_ID) { if (typeof document === "undefined") return null; const el = document.getElementById(id); if (!el) return null; try { return JSON.parse(el.textContent ?? "null"); } catch (err) { console.error(`[@mongez/react-atom] Could not parse hydration script #${id}:`, err); return null; } } //#endregion exports.AtomContext = AtomContext; exports.AtomProvider = AtomProvider; exports.AtomStoreContext = AtomStoreContext; exports.AtomStoreProvider = AtomStoreProvider; exports.DEFAULT_HYDRATION_SCRIPT_ID = DEFAULT_HYDRATION_SCRIPT_ID; exports.HydrateAtomsScript = HydrateAtomsScript; exports.atom = atom; exports.atomCollection = atomCollection; exports.fetchingAtom = fetchingAtom; exports.loadingAtom = loadingAtom; exports.openAtom = openAtom; exports.portalAtom = portalAtom; exports.readHydration = readHydration; exports.serializeSnapshot = serializeSnapshot; exports.serializeStore = serializeStore; exports.useAtom = useAtom; exports.useAtomStore = useAtomStore; //# sourceMappingURL=index.cjs.map