Hooks

The SDK provides hooks for programmatic control over dynamic component loading, giving you full flexibility to build custom UI patterns.

useDynamicComponent

Load and manage a dynamic component programmatically. This hook provides access to the component, loading state, errors, and control functions.

Basic Usage

TSX
import { useDynamicComponent } from '@dpage-ai/react-native-dpage';

function MyScreen() {
  const {
    Component,     // The loaded component (or null)
    loading,       // Boolean loading state
    error,         // Error object (if any)
    reload,        // Function to reload from cache
    refresh,       // Function to force refresh from API
  } = useDynamicComponent('MyComponent', {
    projectId: 'optional-override',
  });

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorView error={error} onRetry={reload} />;
  if (!Component) return null;

  return <Component title="Hello" count={42} />;
}

Return Values

PropTypeDefaultDescription
ComponentReact.ComponentType | null-The loaded component, ready to render
loadingboolean-True while component is being loaded
errorError | null-Error if loading failed
reload() => void-Reload component (checks cache first)
refresh() => void-Force refresh from API (bypasses cache)

Options

Keep overrides object identity stable

If you pass runtimeDepsOverrides, hoist it or create it via useMemo. A new object every render bypasses caching and recompiles the component.
TSX
useDynamicComponent('ComponentName', {
  projectId: 'override-project',  // Optional project override
  runtimeDepsOverrides: editModeDeps, // Optional per-render runtime deps overrides
});
TSX
import React, { useMemo } from 'react';
import { useDynamicComponent } from '@dpage-ai/react-native-dpage';

function MyScreen() {
  const editModeDeps = useMemo(
    () => ({
      View: EditableView,
      Text: EditableText,
    }),
    []
  );

  const { Component } = useDynamicComponent('MyComponent', {
    runtimeDepsOverrides: editModeDeps,
  });

  return Component ? <Component /> : null;
}

Custom Loading UI

Build completely custom loading experiences with the hook:

TSX
function PromoBanner() {
  const { Component, loading, error, reload } = useDynamicComponent('PromoBanner');

  if (loading) {
    return (
      <View style={styles.skeleton}>
        <Animated.View style={[styles.shimmer, animatedStyle]} />
      </View>
    );
  }

  if (error || !Component) {
    return (
      <Pressable onPress={reload} style={styles.retry}>
        <Text>Tap to retry</Text>
      </Pressable>
    );
  }

  return <Component />;
}

Conditional Loading

Load components based on conditions:

TSX
function FeatureCard({ feature }: { feature: string }) {
  const { Component, loading, error } = useDynamicComponent(`Feature_${feature}`);

  // Skip if feature not available
  if (error) return null;
  
  if (loading) return <FeatureSkeleton />;
  if (!Component) return null;

  return <Component />;
}

Refreshing Content

Use refresh to force fetch the latest version:

TSX
function RefreshableContent() {
  const { Component, loading, refresh } = useDynamicComponent('LiveContent');

  return (
    <ScrollView
      refreshControl={
        <RefreshControl
          refreshing={loading}
          onRefresh={refresh}
        />
      }
    >
      {Component && <Component />}
    </ScrollView>
  );
}

Multiple Components

Load multiple components independently:

TSX
function Dashboard() {
  const header = useDynamicComponent('DashboardHeader');
  const stats = useDynamicComponent('DashboardStats');
  const feed = useDynamicComponent('ActivityFeed');

  const isLoading = header.loading || stats.loading || feed.loading;

  if (isLoading) return <DashboardSkeleton />;

  return (
    <View>
      {header.Component && <header.Component />}
      {stats.Component && <stats.Component />}
      {feed.Component && <feed.Component />}
    </View>
  );
}

Error Handling Patterns

TSX
import { HttpError, FetchTimeoutError } from '@dpage-ai/react-native-dpage';

function RobustComponent() {
  const { Component, error, reload } = useDynamicComponent('MyComponent');

  if (error) {
    if (error instanceof HttpError) {
      if (error.status === 404) {
        return <NotFoundView />;
      }
      if (error.status >= 500) {
        return <ServerErrorView onRetry={reload} />;
      }
    }
    
    if (error instanceof FetchTimeoutError) {
      return <TimeoutView onRetry={reload} />;
    }

    return <GenericErrorView error={error} />;
  }

  return Component ? <Component /> : null;
}

Best Practice

Use the hook when you need fine-grained control over loading states, error handling, or want to compose multiple dynamic components together.

useNamedPersistentState

A useState-like hook for persistent state that survives app restarts. Use this for data that should be saved (scores, settings, drafts) instead of ephemeral UI state.

Basic Usage

TSX
import { useNamedPersistentState } from '@dpage-ai/react-native-dpage';

function Counter() {
  // State persists across app restarts
  const [count, setCount] = useNamedPersistentState('counter.value', 0);

  return (
    <View>
      <Text>Count: {count}</Text>
      <Button title="Increment" onPress={() => setCount(c => c + 1)} />
    </View>
  );
}

Signature

TSX
function useNamedPersistentState<T>(
  key: string,       // Unique key for this state
  initialValue: T    // Default value if no persisted value exists
): [T, (v: T | ((prev: T) => T)) => void]

When to Use

PropTypeDefaultDescription
useNamedPersistentStatePersistent-User progress, scores, settings, preferences, draft content, saved data
useStateEphemeral-Modal visibility, loading flags, temporary input, UI toggles

Key Naming Conventions

TSX
// Use descriptive, namespaced keys
const [score, setScore] = useNamedPersistentState('game.score', 0);
const [theme, setTheme] = useNamedPersistentState('settings.theme', 'dark');
const [draft, setDraft] = useNamedPersistentState('editor.draft', '');
const [prefs, setPrefs] = useNamedPersistentState('user.preferences', {});

Miniapp Isolation

State is automatically scoped per miniapp. Two miniapps using the same key will have separate values.

Cloud Sync

State is persisted to AsyncStorage automatically. For cloud sync, use the SDK's sync callbacks:

TSX
import { PersistenceManager } from '@dpage-ai/react-native-dpage';

// Set up cloud sync callbacks
PersistenceManager.setSyncCallbacks({
  // Called when miniapp closes (user navigates back)
  onMiniappClose: async (miniappId) => {
    const snapshot = await PersistenceManager.getSnapshot(miniappId);
    if (snapshot) {
      await uploadToCloud(miniappId, snapshot);
      await PersistenceManager.markSynced(miniappId);
    }
  },
  onAppForeground: async () => {
    // Sync any unsynced changes from previous session
    const unsynced = await PersistenceManager.getUnsyncedMiniapps();
    // ... upload to cloud
  },
});

See the Persistent State docs for full cloud sync details.

Integration with Navigation

TSX
import { useFocusEffect } from '@react-navigation/native';

function DynamicScreen({ route }) {
  const { screenName } = route.params;
  const { Component, loading, refresh } = useDynamicComponent(screenName);

  // Refresh when screen comes into focus
  useFocusEffect(
    useCallback(() => {
      refresh();
    }, [refresh])
  );

  if (loading) return <ScreenLoader />;

  return Component ? <Component /> : <FallbackScreen />;
}