that benefit from explicit state machines rather than imperative conditionals.
Step 1: Define Onboarding States & Events
The state machine models the user journey as a finite set of states with explicit transitions. This prevents race conditions and ensures predictable UX across platforms.
// onboardingMachine.ts
import { createMachine, assign } from 'xstate';
type OnboardingContext = {
currentStep: number;
totalSteps: number;
accountCreated: boolean;
permissionsGranted: string[];
analyticsEvents: string[];
};
type OnboardingEvent =
| { type: 'NEXT' }
| { type: 'BACK' }
| { type: 'SKIP' }
| { type: 'CREATE_ACCOUNT' }
| { type: 'GRANT_PERMISSION'; permission: string }
| { type: 'COMPLETE' };
export const onboardingMachine = createMachine<OnboardingContext, OnboardingEvent>({
id: 'onboarding',
initial: 'welcome',
context: {
currentStep: 0,
totalSteps: 4,
accountCreated: false,
permissionsGranted: [],
analyticsEvents: []
},
states: {
welcome: {
on: {
NEXT: 'preferences',
SKIP: 'core_experience',
CREATE_ACCOUNT: 'account_setup'
}
},
preferences: {
on: {
NEXT: 'permissions',
BACK: 'welcome',
SKIP: 'core_experience'
}
},
permissions: {
on: {
NEXT: 'account_setup',
BACK: 'preferences',
GRANT_PERMISSION: {
actions: assign({
permissionsGranted: ({ context, event }) =>
event.permission ? [...context.permissionsGranted, event.permission] : context.permissionsGranted
})
}
}
},
account_setup: {
on: {
NEXT: 'core_experience',
BACK: 'permissions',
CREATE_ACCOUNT: {
actions: assign({ accountCreated: true })
}
}
},
core_experience: {
type: 'final',
entry: 'trackOnboardingComplete'
}
}
});
Step 2: Implement Navigation Controller
The navigation layer maps state machine states to React Native screens. It handles back/forward routing, skip logic, and platform-specific transitions.
// OnboardingNavigator.tsx
import React, { useEffect } from 'react';
import { View, Text, Button } from 'react-native';
import { useMachine } from '@xstate/react';
import { onboardingMachine } from './onboardingMachine';
import { trackEvent } from './analytics';
const WelcomeScreen = ({ send }: { send: (event: any) => void }) => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Welcome to the app</Text>
<Button title="Next" onPress={() => send('NEXT')} />
<Button title="Skip" onPress={() => send('SKIP')} />
</View>
);
const PreferencesScreen = ({ send }: { send: (event: any) => void }) => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Set your preferences</Text>
<Button title="Next" onPress={() => send('NEXT')} />
<Button title="Back" onPress={() => send('BACK')} />
<Button title="Skip" onPress={() => send('SKIP')} />
</View>
);
const CoreExperience = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Main App Experience</Text>
</View>
);
export const OnboardingNavigator = () => {
const [state, send] = useMachine(onboardingMachine, {
actions: {
trackOnboardingComplete: () => trackEvent('onboarding_completed', { timestamp: Date.now() })
}
});
useEffect(() => {
trackEvent('onboarding_step_viewed', {
step: state.value,
stepIndex: state.context.currentStep
});
}, [state.value]);
const renderStep = () => {
switch (state.value) {
case 'welcome': return <WelcomeScreen send={send} />;
case 'preferences': return <PreferencesScreen send={send} />;
case 'core_experience': return <CoreExperience />;
default: return <WelcomeScreen send={send} />;
}
};
return renderStep();
};
Step 3: Integrate Analytics & Attribution
Onboarding must be instrumented at every state transition. Attribution data (UTM, deep links, referral source) is captured at initialization and attached to progression events.
// analytics.ts
import { Attribution } from 'react-native-attribution';
export const trackEvent = (eventName: string, params: Record<string, any> = {}) => {
const attribution = Attribution.getCurrent();
const payload = {
...params,
attribution_source: attribution?.source || 'organic',
campaign: attribution?.campaign || null,
device_os: Platform.OS,
app_version: __APP_VERSION__
};
// Send to your analytics provider (Firebase, Mixpanel, Amplitude, etc.)
console.log(`[Analytics] ${eventName}`, payload);
};
Progressive permission requests defer system dialogs until the user demonstrates intent. This respects platform guidelines and reduces denial rates.
// permissionManager.ts
import { Platform, PermissionsAndroid } from 'react-native';
import { trackEvent } from './analytics';
export const requestProgressivePermission = async (
permission: string,
rationale: string,
send: (event: any) => void
) => {
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(permission, {
title: 'Permission Required',
message: rationale,
buttonPositive: 'Allow',
buttonNegative: 'Deny'
});
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
send({ type: 'GRANT_PERMISSION', permission });
trackEvent('permission_granted', { permission });
} else {
trackEvent('permission_denied', { permission });
}
}
};
Architecture Decisions & Rationale
- State Machine over Imperative Navigation: Onboarding flows exhibit complex branching (skip, back, partial completion, network failure). XState enforces explicit transitions, preventing invalid states and making testing deterministic.
- Progressive Permission Model: iOS and Android penalize upfront permission requests. Deferring requests until contextual need increases grant rates by 22β35% according to platform telemetry.
- Decoupled Analytics Layer: Events are emitted from state machine actions, ensuring every progression step is tracked regardless of UI implementation. This enables funnel analysis and A/B testing without UI coupling.
- Platform-Agnostic Core with Native Wrappers: The state machine and analytics layer are framework-agnostic. React Native components handle rendering, while native modules manage system dialogs and attribution. This ensures consistent behavior across iOS and Android.
Pitfall Guide
-
Mandatory Account Creation Upfront: Forcing authentication before demonstrating value increases drop-off by 30β45%. Users abandon when cognitive load exceeds perceived utility. Best practice: defer account creation until after first meaningful interaction, or use progressive profiling.
-
Ignoring Platform Navigation Conventions: iOS expects swipe-back gestures and bottom navigation; Android expects hardware back button support and top-level navigation drawers. Hardcoded navigation stacks break platform expectations and increase frustration. Best practice: map state machine transitions to platform-native navigation patterns.
-
Over-Animating Transitions: Heavy animations increase bundle size, degrade performance on mid-tier devices, and delay time-to-value. Users tolerate minimal motion for state changes. Best practice: use native transition APIs (React Native Animated or Reanimated), cap duration at 300ms, and disable animations for users with reduceMotion enabled.
-
Missing Offline/Timeout Handling: Onboarding flows that assume constant connectivity fail in real-world conditions. Network timeouts during account creation or attribution fetch cause unhandled errors and app crashes. Best practice: implement retry logic with exponential backoff, cache attribution data locally, and provide offline fallback screens.
-
Not Instrumenting Drop-Off Points: Without step-level analytics, teams cannot identify where users abandon. Funnel analysis requires explicit event tracking at every state transition. Best practice: emit step_viewed, step_completed, and step_abandoned events with timestamps and device context.
-
Hardcoding Copy Instead of Dynamic Localization: Onboarding text is frequently localized, but hardcoding strings in components creates maintenance debt and breaks A/B testing. Best practice: store copy in a remote configuration service, support dynamic string interpolation, and version localization packs.
-
Skipping Feature Flags for Rollouts: Deploying onboarding changes to 100% of users risks conversion loss. Best practice: gate onboarding variations with feature flags, run cohort-based A/B tests, and implement automatic rollback on negative metric shifts.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| E-commerce / Marketplace | Progressive with deferred account creation | Users need to browse inventory before committing; account creation after cart addition increases conversion | Low (standard state machine + deferred auth) |
| Utility / Tool App | Frictionless with contextual permissions | Immediate utility is the primary value driver; permissions requested only when feature is accessed | Low (minimal state machine, native permission wrappers) |
| Social / Content Platform | Guided with progressive profiling | Network effects require early identity establishment; defer full profile completion | Medium (complex state machine + attribution tracking) |
| B2B SaaS / Enterprise | Guided with SSO integration | Compliance and security require upfront authentication; streamline with SSO and SAML | High (enterprise auth integration, compliance logging) |
Configuration Template
// onboardingConfig.ts
export const onboardingConfig = {
version: '2.1.0',
maxRetries: 3,
timeoutMs: 5000,
analytics: {
trackStepViews: true,
trackAbandonment: true,
attributionSource: 'deep_link'
},
permissions: {
progressive: true,
fallback: 'request_later',
platformOverrides: {
ios: ['notifications', 'camera'],
android: ['location', 'storage']
}
},
navigation: {
enableSwipeBack: true,
respectReduceMotion: true,
transitionDurationMs: 300
},
featureFlags: {
onboardingV2: '50%',
skipAccountGate: true,
contextualPermissions: true
}
};
Quick Start Guide
- Install dependencies:
npm install xstate @xstate/react react-native-attribution
- Initialize the state machine in your app entry point with
useMachine(onboardingMachine)
- Map state transitions to React Native screens using the provided navigator pattern
- Instrument analytics by attaching
trackEvent calls to state machine actions
- Deploy behind a feature flag and monitor Day 7 retention and step completion rates
Onboarding is not a marketing exercise; it's a conversion engine. Treat it as a deterministic system, instrument every transition, and align technical implementation with user psychology. The architecture outlined here eliminates guesswork, reduces abandonment, and scales across platforms without technical debt.