Skip to content
scroll to zoom · drag to pan

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-slots.tsx
/**
* 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 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.

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.

@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.