React — slots
A slot is a named position — "storefront.cr.hero" — that resolves to
whatever asset is currently bound to it. The component asks for the name;
what it points at is data, so an admin can change the hero image without a
deploy, a rebuild, or you.
/** * React: the slot system. * * A slot is a named position in your UI — `"storefront.cr.hero"` — that an * admin can rebind to a different asset without a deploy. The component asks * for the NAME; what it resolves to is data, not code. * * Type-checked in CI against the workspace SDK, like every example here. */
import { NitidaProvider, useSlot, useSlots } from "@nitida/sdk/react";import { NitidaClient } from "@nitida/sdk/web";
// Browser client: the type REJECTS `apiKey`, because a write key in a client// bundle is a write key to a paid platform. Calls go through your own route.const nt = new NitidaClient({ endpoint: "/api/media", tenantCode: "demo", tenantId: 1,});
export function App({ children }: { children: React.ReactNode }) { return <NitidaProvider client={nt}>{children}</NitidaProvider>;}
export function Hero() { const { url, isLoading, error } = useSlot("storefront.cr.hero", { preset: "lg", });
// A slot that has never been bound resolves to no URL. That is a normal // state, not a failure — render the layout without the image rather than an // error, or the page breaks the first time someone adds a new slot. if (isLoading) return <div className="hero hero--skeleton" />; if (error || !url) return <div className="hero hero--empty" />;
// A framework-agnostic example on purpose: `next/image` would make it wrong // for Vite, Remix and plain React. The SDK hands you a URL; what renders it // is your call. return ( // biome-ignore lint/performance/noImgElement: framework-agnostic example <img src={url} alt="" className="hero__image" /> );}
/** * `useSlots` resolves a set in ONE request. Reaching for `useSlot` in a loop * turns a gallery into N round-trips. */export function Gallery() { const { resolutions, isLoading } = useSlots( ["gallery.1", "gallery.2", "gallery.3"], { preset: "md" }, );
if (isLoading) return <div className="gallery gallery--skeleton" />;
return ( <div className="gallery"> {resolutions .filter((r) => r.url) .map((r) => ( // biome-ignore lint/performance/noImgElement: framework-agnostic example <img key={r.slotKey} src={r.url ?? ""} alt="" /> ))} </div> );}useSlots, not useSlot in a loop
Section titled “useSlots, not useSlot in a loop”useSlots resolves the whole set in one request. Calling useSlot inside a
map turns a nine-image gallery into nine round-trips, each with its own
loading state, and they land out of order.
An unbound slot is a normal state
Section titled “An unbound slot is a normal state”A slot nobody has bound yet resolves with no URL. That is not an error — it is what every slot looks like on the day it is added. Render the layout without the image:
if (error || !url) return <div className="hero hero--empty" />;Treating it as a failure means the first person to add a slot to the codebase breaks the page for everyone until someone remembers to bind it.
The browser client cannot hold a key
Section titled “The browser client cannot hold a key”@nitida/sdk/web exports an NitidaClient whose type rejects apiKey
and signingKey. That is deliberate: the browser talks to your route, and
your route holds the key. If you find yourself wanting to pass a key here, the
architecture went wrong one step earlier.