← Files WixARCHIVED FILE
skills/wix-headless-templates/rentals/project/src/wix/rentals/rentals-store.ts
3.67 KB · Oct 8, 2026 · 12:02 UTC
// The rentals listing as a framework-free store — the logic behind useRentals, usable from React
// (useRentals wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe and
// render), or as the specification for a port. Pages are 20 long with `hasMore`/`loadMore()`.
// SSR-friendly: seed with `initialRentals` (+ `initialHasMore`) and nothing is fetched for the first
// paint; without them `start()` loads the first page. One store per mounted listing.
import { RENTALS_PAGE_SIZE, fetchRentals } from "./rentals";
import type { RentalSummary } from "./types";
export interface RentalsStoreOptions {
initialRentals?: RentalSummary[];
/** Whether a page follows the seeded one (the page's `hasMore`); false when omitted. */
initialHasMore?: boolean;
pageSize?: number;
}
/** Everything a listing surface renders from. Read it with getState() or through a subscription. */
export interface RentalsState {
/** null while the first page is in flight — render skeletons, not an empty state. */
rentals: RentalSummary[] | null;
/** Another page exists — render a "load more" affordance. */
hasMore: boolean;
loadingMore: boolean;
error: string | null;
}
export interface RentalsStore {
getState(): RentalsState;
subscribe(listener: () => void): () => void;
/** Fetch the first page unless it was seeded. Call once when mounted (a browser). */
start(): void;
/** Stop reacting; drop late responses. */
stop(): void;
/** Append the next page. No-op while a page is loading or when none is left. */
loadMore(): Promise<void>;
}
export function createRentalsStore({ initialRentals, initialHasMore = false, pageSize = RENTALS_PAGE_SIZE }: RentalsStoreOptions = {}): RentalsStore {
let rentals: RentalSummary[] | null = initialRentals ?? null;
let hasMore = initialHasMore;
let loadingMore = false;
let error: string | null = null;
let started = false;
let generation = 0;
const listeners = new Set<() => void>();
let snapshot: RentalsState | null = null;
const emit = () => {
snapshot = null;
for (const fn of listeners) fn();
};
function getState(): RentalsState {
if (snapshot) return snapshot;
snapshot = { rentals, hasMore, loadingMore, error };
return snapshot;
}
function loadFirstPage(): void {
const id = ++generation;
rentals = null;
hasMore = false;
error = null;
emit();
fetchRentals({ limit: pageSize, offset: 0 })
.then((page) => {
if (!started || generation !== id) return; // superseded — drop it
rentals = page.items;
hasMore = page.hasMore;
emit();
})
.catch((e) => {
if (!started || generation !== id) return;
rentals = [];
error = e instanceof Error ? e.message : String(e);
emit();
});
}
return {
getState,
subscribe(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
},
start() {
if (started) return;
started = true;
if (!initialRentals) loadFirstPage();
},
stop() {
started = false;
generation++;
},
async loadMore() {
if (!started || loadingMore || !hasMore || !rentals) return;
const id = generation;
loadingMore = true;
emit();
try {
const page = await fetchRentals({ limit: pageSize, offset: rentals.length });
if (!started || generation !== id) return;
rentals = [...rentals, ...page.items];
hasMore = page.hasMore;
} catch (e) {
if (!started || generation !== id) return;
error = e instanceof Error ? e.message : String(e);
} finally {
if (generation === id) loadingMore = false;
emit();
}
},
};
}
SHA-256: b1e09d3423d358024b673f2de8df1496cebf56f7d0513eddc66e56026ac9f83b