SDK Helper
NubeSDK apps run inside an isolated web worker with no direct access to the DOM. Everything your app does: reading state, rendering components, navigating, and storing data goes through the SDK instance the runtime hands to your App(nube) entry point.
In practice, that nube instance ends up threaded through every function and component, and a handful of patterns get re-implemented in every project: reading the current page, narrowing page types, rendering one component per product, showing a toast when an event fires.
@tiendanube/nube-sdk-helper packs those patterns into a small, strongly-typed toolkit.
Installation
npm
Yarn
pnpm
npm install @tiendanube/nube-sdk-helper @tiendanube/nube-sdk-types
yarn add @tiendanube/nube-sdk-helper @tiendanube/nube-sdk-types
pnpm add @tiendanube/nube-sdk-helper @tiendanube/nube-sdk-types
@tiendanube/nube-sdk-types is a peer dependency and must be installed alongside the helper package.
Registering the instance
The runtime passes the SDK instance only as the argument of your entry point. The helper's core idea is simple: register it once, and every other helper can reach it on its own, with no more passing nube down through your whole app.
import {
setNubeInstance,
getCurrentState,
ui,
} from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before anything else
const state = getCurrentState();
ui.showToast(`You are on the ${state.location.page.type} page`);
}
Three functions manage the instance:
setNubeInstance(nube): registers the instance (call once at the top ofApp).getNubeInstance(): returns the registered instance, throwing a descriptive error if it has not been registered yet.clearNubeInstance(): clears the instance (handy in tests).
Why this matters
With the instance registered globally, the most common actions become free-standing functions you can call from anywhere: a deeply nested component, a utility module, an event handler, without receiving nube as a parameter.
import { navigate, setNubeInstance, ui } from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
}
// Anywhere else in your app, with no `nube` in scope:
function onCheckoutClick() {
navigate("/checkout"); // routes to the path internally via the SDK instance
ui.showToast("Taking you to checkout...", "info");
}
Without the helper, you would need a nube reference in scope, call nube.getBrowserAPIs().navigate(...), and build the toast component by hand. The helper collapses both into one-liners.
The same applies to browser storage:
import { browser, setNubeInstance } from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export async function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
await browser.asyncLocalStorage.setItem("seen-banner", "true");
const seen = await browser.asyncLocalStorage.getItem("seen-banner");
}
Reading state with selectors
Selectors follow one consistent pattern: call them with no argument and they read the current SDK state; pass an explicit state and they become pure functions.
import {
getCartItems,
getCustomer,
getPageType,
setNubeInstance,
} from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
// No argument: reads the current state from the SDK instance.
const items = getCartItems();
const pageType = getPageType();
const customer = getCustomer();
}
Pass a mock state to any selector and it behaves as a pure function: no side effects, no dependency on the registered instance. This pattern applies to every selector in the family.
Guards
Guards do double duty: they validate at runtime and narrow the type for TypeScript, unlocking the page-specific typed data for the compiler.
import {
getCurrentState,
isProductPage,
setNubeInstance,
} from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
const { page } = getCurrentState().location;
if (isProductPage(page)) {
// `page` is now narrowed to a ProductPage, so `page.data.product` is typed.
console.log(page.data.product.name);
}
}
Unlike a cast (as ProductPage), guards check the structure at runtime before narrowing the type. If the condition does not pass, TypeScript does not expose page.data.product.
There is a guard for nearly every shape you will encounter in a NubeSDK app:
- Pages:
isProductPage,isCategoryPage,isCheckoutPage,isHomePage,isAllProductsPage,isSearchPage - Cart:
isCart,isCartItem,isCartValidationSuccess,isCartValidationPending,isCartValidationFail - Domain:
isStore,isCustomer,isPayment,isShipping,isAddress, and more - Components / page data:
isNubeComponent,hasProductList,hasSections,hasSingleProduct,isSectionWithProducts
Getters
Beyond state selectors, getters expose the metadata the runtime injects about your app. A useful one is getScriptURL, which parses the URL your app script was loaded from (cached as a frozen URL):
import {
getScriptParam,
getScriptURL,
setNubeInstance,
} from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
const url = getScriptURL();
console.log("Script origin:", url.origin);
console.log("Script pathname:", url.pathname);
// Read configuration passed as query params on the script URL, e.g. ?variant=b
const variant = getScriptParam("variant"); // string | null
}
This is the idiomatic way to configure an app from the script tag without shipping a separate config request.
Page matching
pageMatch and onPage solve the same problem from two angles. pageMatch dispatches once against a state you provide, and each handler receives the correctly-typed payload for its page. onPage wraps pageMatch but subscribes to navigation, re-running on every page change and returning an unsubscribe function.
Use pageMatch for a one-off decision based on the current state:
import {
getCurrentState,
pageMatch,
setNubeInstance,
} from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
// Runs once against the state you pass in.
pageMatch(getCurrentState(), {
product: (state, product) => console.log("Product:", product.name),
checkout: (state, checkout) => console.log("Step:", checkout.step),
});
}
Use onPage to keep reacting as the user navigates:
import { onPage, setNubeInstance } from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
const stop = onPage({
product: (state, product) => console.log("Viewing product:", product.id),
checkout: (state, checkout) => {
if (checkout.step === "success") console.log("Purchase complete");
},
// category / home handlers are optional
});
// Call stop() later, when you no longer need to react to navigation.
void stop;
}
For checkout specifically, onCheckoutStep lets you react to a particular step:
import { onCheckoutStep, setNubeInstance } from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
onCheckoutStep({
success: () => console.log("Purchase complete"),
});
}
Render
A frequent need is rendering something into a per-product grid slot: a badge, a label, an icon. forEachProduct extracts every product from the current state (regardless of page type), maps each one through a render factory, drops empty results, and auto-assigns a unique key from the product id.
import {
forEachProduct,
onPage,
setNubeInstance,
} from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
import { Badge } from "./components/Badge";
export function App(nube: NubeSDK) {
setNubeInstance(nube);
// Render a badge over every product image in the home grid.
onPage({
home: () => {
nube.render(
"product_grid_item_image_center_center",
forEachProduct((product) => <Badge product={product} />),
);
},
});
}
Without JSX, the factory returns a component object directly:
import {
forEachProduct,
onPage,
setNubeInstance,
} from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
onPage({
home: () => {
nube.render(
"product_grid_item_image_bottom_right",
forEachProduct((product) => ({ type: "txt", children: product.name })),
);
},
});
}
Return null or undefined from the factory to skip a product: those entries are filtered out automatically.
UI helpers and events
ui wraps the most common view operations, including rendering the same component across multiple slots in one call:
import { setNubeInstance, ui } from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
ui.renderAll(["corner_top_left", "corner_top_right"], {
type: "txt",
children: "Hi",
});
ui.showToast("Done!", "success");
ui.clear("corner_top_right");
}
onEvent and toastOn reduce the repetitive "listen and react" pattern to one line each, both returning an unsubscribe function:
import { onEvent, setNubeInstance, toastOn } from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
const off = onEvent("cart:update", (state) => {
console.log("items:", state.cart.items.length);
});
// Call off() later, when you no longer need it.
void off;
toastOn("cart:add:success", "Added to cart", "success");
toastOn("cart:update", (state) => `Cart: ${state.cart.items.length} items`);
}
Slot discovery
The slots on a page are not fixed. On sectionable themes the store owner adds, removes and reorders dynamic sections, so the same logical slot may exist several times, in a different position, or not at all. On top of that, the same section is sometimes exposed under different names as a static slot and as a dynamic one — for example after_section_products_featured versus after_dynamic_section_featured_products. In practice this pushes apps into hardcoding slot names and guessing theme by theme.
The slot helpers wrap nube.api.getAvailableSlots() to answer the questions an app actually has — which slots exist here? and where is the first/last section of this type? — returning a slot that is ready for ui.render, or null when nothing matches.
Slot discovery relies on the getAvailableSlots() API, which is not available in the Patagonia theme or in Checkout at this time. Queries return no slots in those contexts.
Listing the page's slots
Three async functions expose the current page's slot registry:
import {
getAvailableSlots,
getDynamicSlots,
getStaticSlots,
setNubeInstance,
} from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export async function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
// Every slot on the page, split by kind.
const { static: statics, dynamic: dynamics } = await getAvailableSlots();
// Only the theme's fixed injection points, addressable by `slotId`.
const fixed = await getStaticSlots();
const hasNewsletter = fixed.some(
(slot) => slot.slotId === "before_section_newsletter",
);
// Only the dynamic slots, each carrying its section coordinates.
const sections = await getDynamicSlots();
const featured = sections.filter(
(slot) => slot.sectionType === "featured_products",
);
}
getDynamicSlots() returns the raw slots: each one carries the sectionType the section actually has on the page, with no equivalent-name resolution. For that, use the section queries below.
Finding a section's slot
Four queries answer "where is section X?" and hand back a slot ready to render into:
import {
afterLastSection,
beforeFirstSection,
setNubeInstance,
ui,
} from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
import { Text } from "@tiendanube/nube-sdk-jsx";
function Component() {
return <Text>Hello</Text>;
}
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
// `ui.render` accepts the promise directly and renders nothing on `null`.
ui.render(beforeFirstSection("newsletter"), <Component />);
ui.render(afterLastSection("newsletter"), <Component />);
}
Every query follows the same resolution order:
- Dynamic sections first. It filters the page's dynamic slots by section type and position (
before/after), then picks the lowestsectionIndex(first) or the highest one (last). This holds regardless of how many sections of that type exist and of where they sit on the page. - Fallback to the static slot. If the theme has no matching dynamic section, it looks for the static
${position}_section_${type}slot — e.g.before_section_newsletter. Static slots are unique, sobeforeFirstSectionandbeforeLastSectionreturn the same slot in that case. - Nothing found. It logs a
SlotNotFounderror to the console and resolves tonull.
Equivalent section names
The static slots were named after the section they wrap (products_featured), while the dynamic sections were named after the page data that feeds them (featured_products). The same section therefore answers to two names, depending on whether the theme renders it as a fixed or as a dynamic section.
The helper keeps a private equivalence table (today featured_products ↔ products_featured) and runs every query over the requested type and its aliases, trying the spelling the app asked for first. The practical result: your app keeps passing whichever name it knows, and the section is found either way.
// Both calls find the same section, whether it is static or dynamic.
ui.render(afterLastSection("featured_products"), <Component />);
ui.render(afterLastSection("products_featured"), <Component />);
Unknown section types are accepted and resolve to themselves, which covers custom sections whose name only exists at runtime. The SectionType type surfaces the known names as autocomplete, but accepts any string.
When nothing matches
A query that finds nothing does not throw: it logs a SlotNotFound to the console and resolves to null, so a missing slot degrades into "nothing rendered" instead of breaking the app. Passing the promise to ui.render already handles this. If you prefer to await, check the result:
const slot = await beforeFirstSection("products_featured");
if (slot) {
ui.render(slot, <Component />);
}
API Reference
Formal reference for every symbol exported from the package root (@tiendanube/nube-sdk-helper).
Instance
Manage the SDK instance used by the rest of the helper.
setNubeInstance(nube)
Registers the NubeSDK instance for the current app. Call once, at the start of App(nube).
setNubeInstance(nube: Readonly<NubeSDK>): void
getNubeInstance()
Returns the registered instance. If none was registered, it falls back to self.__SDK_INSTANCE__ and, when neither exists, throws a descriptive error.
getNubeInstance(): Readonly<NubeSDK>
clearNubeInstance()
Clears the registered instance. Useful in tests or when tearing down between app reloads.
clearNubeInstance(): void
Getters
Expose the current state and the metadata the runtime injects about the app.
getCurrentState()
Returns the current (readonly) SDK state.
getCurrentState(): Readonly<NubeSDKState>
getAppData()
Returns the app data injected by the runtime: id and script.
getAppData(): Readonly<{ id: string; script: string }>
getScriptURL()
Returns the URL the app script was loaded from, as a frozen URL instance (cached on first call).
getScriptURL(): Readonly<URL>
getScriptSearchParams()
Returns the (readonly) URLSearchParams of the script URL.
getScriptSearchParams(): Readonly<URLSearchParams>
getScriptParam(key)
Returns the value of a specific query param from the script URL, or null if absent. The idiomatic way to configure an app from the script tag without a separate request.
getScriptParam(key: string): Nullable<string>
State selectors
Focused accessors for the most-read slices of state. All take an optional state; when omitted, they read the current state from the registered instance (pass an explicit state to make them pure).
getCart(state?: NubeSDKState): Cart
getCartItems(state?: NubeSDKState): CartItem[]
getPageType(state?: NubeSDKState): Page["type"] // e.g. "home", "product", "checkout"
getCustomer(state?: NubeSDKState): Nullable<Customer> // null when unavailable on the page
Guards
Guards do double duty: they validate at runtime and narrow the type for TypeScript. Unlike a cast (as ProductPage), they check the structure before narrowing — if the check fails, the type is never exposed.
import {
getCurrentState,
isProductPage,
setNubeInstance,
} from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
const { page } = getCurrentState().location;
if (isProductPage(page)) {
// `page` is narrowed to ProductPage, so `page.data.product` is typed.
console.log(page.data.product.name);
}
}
Pages — take a Page and narrow to the matching page type:
| Guard | Narrows to |
|---|---|
isProductPage | ProductPage |
isCategoryPage | CategoryPage |
isCheckoutPage | CheckoutPage |
isAllProductsPage | AllProductsPage |
isSearchPage | SearchPage |
isHomePage | HomePage |
isAccountPage | AccountPage |
isAccountLoginPage | AccountLoginPage |
isAccountRegisterPage | AccountRegisterPage |
isAccountInfoPage | AccountInfoPage |
isAccountResetPage | AccountResetPage |
isAccountNewPasswordPage | AccountNewPasswordPage |
isAccountOrdersPage | AccountOrdersPage |
Cart — take unknown and validate the shape:
| Guard | Narrows to |
|---|---|
isCart | Cart |
isCartItem | CartItem |
isCartValidationSuccess | successful cart validation |
isCartValidationPending | pending cart validation |
isCartValidationFail | failed cart validation |
Domain — take unknown and validate the shape:
| Guard | Narrows to |
|---|---|
isStore | Store |
isCustomer | Customer |
isPayment | Payment |
isShipping | Shipping |
isShippingOption | ShippingOption |
Address — take unknown and validate the shape:
| Guard | Narrows to |
|---|---|
isAddress | Address |
isShippingAddress | ShippingAddress |
isBillingAddress | BillingAddress |
Components and page data — useful to check the shape of page.data before accessing it:
| Guard | Narrows to |
|---|---|
isNubeComponent | NubeComponent |
hasProductList | { products: ProductDetails[] } |
hasSections | { sections: unknown[] } |
isSectionWithProducts | a section containing products |
hasSingleProduct | { product: ProductDetails } |
Page matching
pageMatch(state, handlers)
Dispatches to the handler matching the page type in the state you pass. Each handler receives the correctly-typed payload for its page. Runs once.
pageMatch(state: NubeSDKState, handlers: PageHandlers): void
onPage(handlers)
Subscribes to page:loaded and calls the matching handler on every navigation. Returns an unsubscribe function.
onPage(handlers: PageHandlers): () => void
onCheckoutStep(handlers)
Listens to checkout:ready and, when the current page is a checkout, invokes the handler registered for the current step. Also runs immediately for the current step if already in checkout. Returns an unsubscribe function.
onCheckoutStep(handlers: CheckoutStepHandlers): () => void
Related types: PageDataMap, PageHandlerFunction<T>, PageHandlers, CheckoutStepHandlers.
Render
getProductsFromState(state)
Extracts every product from the state, regardless of page type (direct lists, products inside sections, and the main product on detail pages). Returns an empty array when there are none.
getProductsFromState(state: NubeSDKState): ProductDetails[]
forEachProduct(renderFactory)
Builds a render function that extracts products from the state, maps each through renderFactory, drops null/undefined results, and auto-assigns a unique key from the product id.
forEachProduct(
renderFactory: (product: ProductDetails) => NubeComponent | null | undefined,
): (state: NubeSDKState) => NubeComponent[]
UI
ui is a frozen object with four view helpers.
ui.showToast(message: string, variant?: ToastVariant): void // variant defaults to "info"
ui.clear(slot: UISlot): void
ui.render(slot: UISlot | StaticSlot | DynamicSlot, component: RenderableComponent): void
ui.render(slot: Promise<QuerySlotResult>, component: RenderableComponent): void
ui.renderAll(slots: UISlot[], component: RenderableComponent): void
showToast— shows a toast in the top-right corner.variantis"success" | "error" | "warning" | "info".clear— clears a slot (nube.clearSlot). Does not accept promises.render— renders a component into a slot. Accepts a slot name, aStaticSlot/DynamicSlotdescriptor, or the promise returned by a section query : in that case it awaits the result, renders nothing when it isnull, and logs any error to the console.renderAll— renders the same component across multiple slots in one call.
Related types: ToastVariant, RenderableComponent, UIHelper, QuerySlotResult.
Slot discovery
Queries over the slots available on the current page, built on top of nube.api.getAvailableSlots() . All are async, and all throw if no SDK instance was registered. Available from helper version 0.3.0.
getAvailableSlotsAPI()
Returns the available-slots adapter from the registered instance. It is resolved once and memoized for the lifetime of the app, so repeated queries reuse the same command channel.
getAvailableSlotsAPI(): AvailableSlotsCommands
getAvailableSlots()
Every slot on the current page, split by kind.
getAvailableSlots(): Promise<{ static: StaticSlot[]; dynamic: DynamicSlot[] }>
getStaticSlots()
Only the static slots: the theme's fixed injection points, addressable by their slotId.
getStaticSlots(): Promise<StaticSlot[]>
getDynamicSlots()
Only the dynamic slots. Each one carries its section coordinates (sectionType, sectionId, sectionIndex), which is what lets you pick a specific instance when a section repeats. The sectionType values come through as they are on the page, with no equivalent-name resolution.
getDynamicSlots(): Promise<DynamicSlot[]>
Section queries
Find the slot before/after the first/last section of a type. They search the dynamic sections first (by lowest or highest sectionIndex) and, when there are none, fall back to the static ${position}_section_${type} slot. Since the static slot is unique, first and last return the same slot in that fallback. When nothing matches, they log a SlotNotFound to the console and resolve to null.
beforeFirstSection(sectionType: SectionType): Promise<QuerySlotResult>
afterFirstSection(sectionType: SectionType): Promise<QuerySlotResult>
beforeLastSection(sectionType: SectionType): Promise<QuerySlotResult>
afterLastSection(sectionType: SectionType): Promise<QuerySlotResult>
Queries run over the requested type and its equivalent names, trying the spelling the app asked for first — see Equivalent section names .
SlotNotFound
Error describing a query that matched nothing. The queries do not throw it: they log it through SlotNotFound.log and return null.
class SlotNotFound extends Error {
constructor(queryDescription: string, appid: string | number);
static log(queryDescription: string): void;
}
Related types:
type QuerySlotResult = StaticSlot | DynamicSlot | null;
type SectionType = "newsletter" | "products_sale" | "products_new" | "products_featured" | "featured_products" | (string & {});
SectionType surfaces the known names as autocomplete but accepts any string, to cover custom and dynamic sections whose name is only known at runtime.
Events
Ergonomic wrappers around nube.on that return an unsubscribe function.
onEvent(event, listener)
Equivalent to nube.on(event, listener), but the returned function detaches the listener via nube.off — no need to keep a reference to both the instance and the listener to clean up.
onEvent<T extends NubeSDKListenableEvent>(
event: T,
listener: EventListenerMap[T],
): () => void
toastOn(event, message, variant?)
Shows a toast whenever the event fires. The message can be a static string or a function deriving it from state. Returns an unsubscribe function.
toastOn<T extends NubeSDKListenableEvent>(
event: T,
message: string | ((state: Readonly<NubeSDKState>) => string),
variant?: ToastVariant,
): () => void
Browser
Access the Browser APIs (storage, navigation) through the SDK instance. See also Browser APIs .
browser
Object exposing the Browser APIs (asyncLocalStorage, asyncSessionStorage, navigate, ...). Materialized lazily on first property access and cached.
import { browser, setNubeInstance } from "@tiendanube/nube-sdk-helper";
import type { NubeSDK } from "@tiendanube/nube-sdk-types";
export async function App(nube: NubeSDK) {
setNubeInstance(nube); // call this first, before any other helper
await browser.asyncLocalStorage.setItem("seen-banner", "true");
const seen = await browser.asyncLocalStorage.getItem("seen-banner");
}
navigate(route)
Navigates to a route within the store's domain. The route must start with /.
navigate(route: `/${string}`): void
clearBrowserCache()
Clears the internal Browser APIs cache, forcing a fresh instance on next access. Useful in tests.
clearBrowserCache(): void
Utilities
General-purpose functions, independent of the SDK instance.
deepClone(obj)
Deep-clones a value. Uses structuredClone when available (it is, in the NubeSDK worker runtime), falling back to JSON serialization.
deepClone<T>(obj: T): T
debounce(func, wait)
Returns a debounced version of the function that only runs after wait ms with no new calls.
debounce<T extends (...args: never[]) => unknown>(
func: T,
wait: number,
): (...args: Parameters<T>) => void
throttle(func, limit)
Returns a throttled version of the function that runs at most once every limit ms.
throttle<T extends (...args: never[]) => unknown>(
func: T,
limit: number,
): (...args: Parameters<T>) => void
Next steps
Help us improve NubeSDK
Found an issue or have a suggestion? Let us know on GitHub.