Core Concepts
Understand how the Rocapine Onboarding Studio SDK works under the hood.
Architecture Overview
Onboarding Studio follows a headless-first architecture, separating data management from UI rendering. This gives you maximum flexibility to use pre-built components or build your own UI.
Package Structure
The SDK is split into two packages:
-
@rocapine/react-native-onboarding(Core/Headless)- Data fetching and caching
- State management and hooks
- Type definitions
- No UI components
-
@rocapine/react-native-onboarding-ui(Optional UI)- Pre-built screen components
- Theme system
- Styled components for all page types
Architecture Layers
┌─────────────────────────────────────┐
│ Your React Native App │
│ (Navigation, Routing, Business) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ UI Layer (Your Choice) │
│ • Pre-built: OnboardingPage │
│ • Custom: Your own components │
│ • Mixed: Both approaches │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Core SDK (Headless) │
│ • OnboardingProvider (Data) │
│ • useOnboardingStep (Hooks) │
│ • Caching & State Management │
│ • Type-safe data structures │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ CMS Backend │
│ (Supabase - Onboarding Studio) │
└─────────────────────────────────────┘
Headless-First Benefits
- Flexibility: Build completely custom UIs that match your brand
- Bundle Size: Only install UI components you actually use
- Incremental Adoption: Start with pre-built components, migrate to custom UI later
- Framework Agnostic: Core SDK works with any React Native navigation solution
How It Works
1. Initialization
When you wrap your app with OnboardingProvider and ThemeProvider:
<OnboardingProvider
client={client}
customAudienceParams={{ onboardingId: "abc-123" }}
customActions={{
trackCta: async ({ variables }) => {
console.log("trackCta", variables);
},
}}
>
<ThemeProvider>
<YourApp />
</ThemeProvider>
</OnboardingProvider>
The SDK:
- Initializes React Query for data fetching (OnboardingProvider)
- Sets up AsyncStorage caching (OnboardingProvider)
- Prepares theme context (ThemeProvider)
- Establishes progress tracking (OnboardingProvider)
- Registers
customActionsforComposableScreenButton presses — see Custom Actions
2. Data Fetching
When you call useOnboardingQuestions({ stepNumber: 1 }):
const {
step,
isLastStep,
stepsLength,
onboardingMetadata,
steps
} = useOnboardingQuestions({ stepNumber: 1 });
The SDK:
- Checks AsyncStorage cache first (offline support)
- Fetches from CMS if cache is stale or missing
- Uses React Query's
useSuspenseQueryfor automatic loading states - Validates data with Zod schemas
- Updates progress context
3. Rendering
You have two options for rendering:
Option A: Use Pre-built Components (from @rocapine/react-native-onboarding-ui)
import { OnboardingPage } from "@rocapine/react-native-onboarding-ui";
<OnboardingPage step={step} onContinue={handleContinue} />
The pre-built component:
- Inspects the
step.typefield - Routes to the appropriate renderer (
QuestionRenderer,MediaContentRenderer, etc.) - Passes step data and callbacks
- Handles fallback for unimplemented types (sandbox mode)
Option B: Build Your Own UI (Headless approach)
import { useOnboardingStep } from "@rocapine/react-native-onboarding";
const { step } = useOnboardingStep({ stepNumber: 1 });
// Build your own UI based on step.type and step.payload
switch (step.type) {
case "Question":
return <YourCustomQuestionUI step={step} />;
case "MediaContent":
return <YourCustomMediaUI step={step} />;
// ... handle other types
}
This gives you complete control over the UI while still benefiting from the SDK's data fetching, caching, and type safety.
Start & End Nodes
The onboarding is an id-based graph. Two optional, studio-authored markers make its entry and exit explicit. Both are additive — payloads without them behave exactly as before (start at the first step; end by running off the array).
Start node
configuration.startStepId names the unique step the flow starts on, decoupled from array position. Resolve it with useOnboardingStart() in your entry route (it suspends until the payload is loaded), or the pure resolveStartStepNumber(steps, startStepId) helper:
import { useOnboardingStart } from "@rocapine/react-native-onboarding";
const { startStepNumber } = useOnboardingStart();
// e.g. router.push(`/onboarding/${startStepNumber}`);
When startStepId is absent or references a missing step, it falls back to the first step (1).
End nodes
Ending is a branching outcome. A step's nextStep can target the reserved end sentinel ONBOARDING_END_STEP_ID ("__END__") — as the defaultTargetStepId or as any branch.targetStepId. When branching resolves to it, resolveNextStepNumber returns null (complete) instead of a step number, so a decision point can end the flow from any step with no trailing screen required. Multiple end transitions are allowed, and legacy payloads still end via the existing positional/branch fallback.
import { ONBOARDING_END_STEP_ID } from "@rocapine/react-native-onboarding";
// terminal screen — continuing from it completes the onboarding
{ id: "all-set", nextStep: { defaultTargetStepId: ONBOARDING_END_STEP_ID, branches: [] } }
// conditional end from a decision point (buy → thank-you screen; decline → finish)
{
id: "paywall",
nextStep: {
defaultTargetStepId: ONBOARDING_END_STEP_ID,
branches: [
{ condition: { variable: "purchased", operator: "eq", value: "true" }, targetStepId: "thank-you" },
],
},
}
Completion callback
Pass onComplete to OnboardingProvider to run post-onboarding logic. The host fires it from its terminal branch via the completeOnboarding() helper (returned from useOnboardingStep), instead of inferring the end and navigating ad-hoc:
<OnboardingProvider
client={client}
onComplete={({ variables, metadata }) => {
// mark onboarding done, navigate to paywall/home, etc.
router.replace("/home");
}}
>
{/* ... */}
</OnboardingProvider>
const { step, steps, completeOnboarding } = useOnboardingStep({ stepNumber });
const next = resolveNextStepNumber(step, variables, steps);
if (next === null) {
completeOnboarding(); // fires onComplete with { variables, metadata }
} else {
router.push(`/onboarding/${next}`);
}
onComplete receives the collected variables and the onboarding metadata. All end transitions are equivalent — it does not report which one was taken. The true end is signaled by resolveNextStepNumber(...) === null (the end sentinel or no valid next), not by the positional isLastStep.
Caching Strategy
AsyncStorage Cache
The SDK uses AsyncStorage to cache the onboarding payload for offline access. The caching mode depends on whether you pass a cacheKey to the client.
Default key (cacheKey omitted) — stale-while-revalidate, under rocapine-onboarding-studio:
- The payload is cached after a successful fetch (the offline fallback is never cached).
- On launch the cache is served instantly, then refreshed from the network in the background, so studio re-deploys propagate and a stale cache heals.
- Best for "always show the latest published onboarding."
Custom key (cacheKey: "...") — persist-first, under rocapine-onboarding-sdk-{cacheKey}:
- The payload is served cache-first with no background revalidation — a pinned version stays put across launches and is never swapped out mid-flow.
- The host owns when to refetch: call
client.clearCache()(or rotate thecacheKeyvalue), and the next launch is a cache miss that fetches fresh. - Best for resumable flows where a user mid-onboarding must keep the version they started.
Sandbox mode (isSandbox: true) ignores caching entirely and always fetches fresh.
// Pin the started version; refetch only when the app decides to.
const client = new OnboardingStudioClient(projectId, { cacheKey: "v2" });
// ...later, to allow a fresh fetch on next launch:
await client.clearCache();
Cache Invalidation:
To force an in-session refetch (in addition to clearCache() clearing the persisted copy), invalidate the React Query key:
import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
queryClient.invalidateQueries({ queryKey: ["onboardingQuestions"] });
Progress Tracking
The SDK automatically tracks progress through your onboarding flow:
Progress Context
const { activeStep, totalSteps, progress } = useContext(OnboardingProgressContext);
activeStep: Current step number (updated byuseOnboardingQuestions)totalSteps: Total number of steps in the flowprogress: Percentage (0-1) of completion
ProgressBar Component
Add <ProgressBar /> once in your app layout, typically inside both providers:
<OnboardingProvider {...props}>
<ThemeProvider>
<ProgressBar /> {/* Global progress indicator */}
<YourApp />
</ThemeProvider>
</OnboardingProvider>
The ProgressBar:
- Reads from progress context automatically
- Shows/hides based on
step.displayProgressHeader - Animates smoothly with Reanimated
- Positions itself at the top of safe area
Step Types
Each step returned by the CMS has this structure:
{
id: string; // Unique identifier
type: string; // "Question" | "MediaContent" | "Carousel" | ...
name: string; // Display name (for debugging)
displayProgressHeader: boolean; // Show/hide progress bar
payload: object; // Type-specific data
customPayload: object | null; // Your custom fields
continueButtonLabel?: string; // Optional CTA text
figmaUrl?: string | null; // Design reference
}
Type Discrimination
The SDK uses the type field for routing:
switch (step.type) {
case "Question":
return <QuestionRenderer step={step} onContinue={onContinue} />;
case "MediaContent":
return <MediaContentRenderer step={step} onContinue={onContinue} />;
// ... more types
}
Runtime Validation
All step data is validated at runtime using Zod schemas:
const QuestionStepSchema = z.object({
id: z.string(),
type: z.literal("Question"),
payload: z.object({
title: z.string(),
subtitle: z.string().nullish(),
answers: z.array(/* ... */),
// ...
}),
// ...
});
If data doesn't match the schema, you'll get a clear error message.
Sandbox Mode
Enable sandbox mode during development:
const client = new OnboardingStudioClient("your-project-id", {
isSandbox: true,
});
Sandbox Features:
- Shows dev messages for unimplemented screen types
- More verbose error messages
- Allows testing without production CMS data
- Auto-continues on unimplemented screens
Production Mode:
- Silently auto-continues on unimplemented screens
- Minimal error messages
- Optimized for end-user experience
Locale Support
Fetch onboarding content in different languages:
<OnboardingProvider locale="fr" />
The locale is passed to the CMS API, which returns localized content based on your Onboarding Studio configuration.
Custom Headers
The CMS API returns custom headers with metadata:
ONBS-Onboarding-Id: The active onboarding IDONBS-Audience-Id: The targeted audienceONBS-Onboarding-Name: Display name of the onboarding
These headers are available in the client but typically not needed in your app logic.
Suspense Boundaries
The SDK uses React Query's useSuspenseQuery, which requires Suspense boundaries:
import { Suspense } from "react";
<Suspense fallback={<LoadingScreen />}>
<OnboardingScreen />
</Suspense>
Expo Router handles Suspense automatically, so you usually don't need to add boundaries manually.
Error Handling
The SDK provides multiple layers of error handling:
- Network Errors: Automatic retry with exponential backoff
- Validation Errors: Clear messages about schema mismatches
- Missing Dependencies: Explicit instructions for required packages
- Sandbox Mode: Development-friendly error messages
Theme System
The SDK provides a theme system via ThemeProvider from the UI SDK with:
- Color Tokens: Semantic color names (primary, neutral, text, etc.)
- Typography Tokens: Font families, sizes, weights, line heights
- Semantic Text Styles: Pre-configured styles (heading1, body, button, etc.)
- Mode Support: Light and dark mode with separate token sets
Learn more in Customization → Theming.
Performance Considerations
Optimizations
- Lazy Loading: Renderers are lazy-loaded per screen type
- Native Driver: Animations use native driver when possible
- Memoization: Components are memoized where appropriate
- Query Deduplication: React Query prevents duplicate requests
Best Practices
- Use pagination for long onboarding flows
- Preload media assets when possible
- Keep payload sizes reasonable
- Cache static assets locally
Next Steps
- 📘 Explore the API Reference for detailed prop documentation
- 🎨 Learn about Customization Options
- 🎭 Review available Page Types