@mongez/react-atom
Version:
A simple state management tool for React Js.
442 lines (434 loc) • 12.7 kB
JavaScript
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