UTA DevHub
Guides

Initial UI Rendering Guide

Implementation patterns for the Initial UI rendering stage in the application bootstrap process.

Initial UI Rendering Guide

Overview

The Initial UI rendering stage is the second phase of the application bootstrap process, occurring immediately after the pre-ui completes. This crucial stage focuses on rendering the initial UI shell and providing immediate visual feedback to users while background initialization continues. This guide provides implementation patterns for effectively managing the Initial UI rendering stage.

Quick Start

To implement the Initial UI rendering stage:

  1. Register tasks with the initializer:

    import { initializer, InitStage } from '@/core/shared/app/initialization';
     
    // Register an Initial UI stage task
    initializer.registerTask(InitStage.INITIAL_UI, {
      name: 'navigation:setup',
      execute: async () => {
        // Set up navigation listeners, deep links, etc.
        await setupNavigationConfiguration();
      }
    });
  2. Implement a conditional rendering pattern in your main component:

    function AppContent() {
      const { initialized, currentStage } = useInitialization();
      
      // Show splash screen during Pre-UI initialization
      if (currentStage === InitStage.PRE_UI || !currentStage) {
        return <SplashScreen />;
      }
      
      // Show loading screen during Initial UI and Background stages
      if (!initialized) {
        return <LoadingScreen stage={currentStage} />;
      }
      
      // Fully initialized - show main app content
      return <NavigationContainer>{/* App navigation */}</NavigationContainer>;
    }
  3. Create skeleton screens or loading placeholders that appear during this stage

When To Use

Add initialization tasks to the Initial UI stage when they:

  • Involve UI rendering: Components and screens that make up the initial app shell
  • Determine navigation flow: Logic that decides which screens to show based on auth state (using data from the pre-ui)
  • Create loading indicators: Skeleton screens, progress indicators, and placeholders
  • Set up navigation: Route configuration and navigation state initialization

The Initial UI stage should focus on tasks that create a responsive, interactive shell that users can see while more complex background operations complete in the background.

Implementation Patterns

As shown in the diagram, the Initial UI stage follows the pre-ui and transitions to the background once the initial user interface is rendered.

// app/(app)/_layout.tsx
import { Redirect, Stack } from 'expo-router';
import { useEffect } from 'react';
import { initializer, InitStage } from '@/core/shared/app/initialization';
import { useAuthStore } from '@/core/domains/auth/store';
import { useInitialization } from '@/core/shared/app/initialization';
import { LoadingScreen } from '@/ui/screens/LoadingScreen';
 
export default function AppLayout() {
  const { initialized, currentStage } = useInitialization();
  const { isAuthenticated } = useAuthStore();
  
  // Register navigation initialization as an Initial UI task
  useEffect(() => {
    const navigationTask = {
      name: 'navigation:setup',
      execute: async () => {
        // Setup navigation listeners, deep links, etc.
      }
    };
    
    initializer.registerTask(InitStage.INITIAL_UI, navigationTask);
  }, []);
  
  // Show loading while initialization is in progress
  if (!initialized && currentStage !== InitStage.PRE_UI) {
    return <LoadingScreen stage={currentStage} />;
  }
  
  // Handle authentication-based routing
  if (!isAuthenticated) {
    return <Redirect href="/auth" />;
  }
  
  return <Stack />;
}

App Shell Rendering

The app shell provides the initial framework for your application:

// App.tsx
import React, { useEffect } from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { QueryClientProvider } from '@tanstack/react-query';
import { StatusBar } from 'react-native';
import { ThemeProvider } from './ui/theme/ThemeProvider';
import { InitializationProvider } from '@/core/shared/app/initialization/InitializationProvider';
import { AppNavigator } from './navigation/AppNavigator';
import { SplashScreen } from './ui/screens/SplashScreen';
import { useInitialization } from '@/core/shared/app/initialization';
import { queryClient } from '@/core/shared/query/queryClient';
 
export default function App() {
  // Hide the native splash screen once our JS is running
  useEffect(() => {
    // This assumes you're using a splash screen library like 'react-native-splash-screen'
    SplashScreen.hide();
  }, []);
 
  return (
    <SafeAreaProvider>
      <StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
      <QueryClientProvider client={queryClient}>
        <ThemeProvider>
          <InitializationProvider>
            <AppContent />
          </InitializationProvider>
        </ThemeProvider>
      </QueryClientProvider>
    </SafeAreaProvider>
  );
}
 
function AppContent() {
  const { initialized, currentStage, error } = useInitialization();
  
  // Show custom splash screen while Pre-UI initialization completes
  if (currentStage === 'pre-ui' || !currentStage) {
    return <SplashScreen />;
  }
  
  // Once Pre-UI is done, show the AppNavigator which handles further stages
  return <AppNavigator />;
}

Configure navigation based on authentication state determined during Pre-UI:

// navigation/AppNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useInitialization } from '@/core/shared/app/initialization';
import { useAuthStore } from '@/core/domains/auth/store';
import { AuthNavigator } from './AuthNavigator';
import { MainNavigator } from './MainNavigator';
import { OnboardingNavigator } from './OnboardingNavigator';
import { LoadingScreen } from '@/ui/screens/LoadingScreen';
import { ErrorScreen } from '@/ui/screens/ErrorScreen';
 
const RootStack = createNativeStackNavigator();
 
export function AppNavigator() {
  const { initialized, currentStage, error } = useInitialization();
  const { isAuthenticated, hasCompletedOnboarding } = useAuthStore();
  
  // Handle initialization errors
  if (error) {
    return <ErrorScreen error={error} />;
  }
  
  // Register this navigator as an initialization task for the Initial UI stage
  useRegisterInitialUITask();
  
  return (
    <NavigationContainer>
      <RootStack.Navigator screenOptions={{ headerShown: false }}>
        {!initialized ? (
          // Show loading screen during background initialization
          <RootStack.Screen name="Loading" component={LoadingScreen} />
        ) : isAuthenticated ? (
          // User is authenticated
          hasCompletedOnboarding ? (
            // Show main app
            <RootStack.Screen name="Main" component={MainNavigator} />
          ) : (
            // Show onboarding
            <RootStack.Screen name="Onboarding" component={OnboardingNavigator} />
          )
        ) : (
          // User is not authenticated
          <RootStack.Screen name="Auth" component={AuthNavigator} />
        )}
      </RootStack.Navigator>
    </NavigationContainer>
  );
}
 
// Register navigation initialization as a task
function useRegisterInitialUITask() {
  const { initializer, InitStage } = useInitialization();
  
  useEffect(() => {
    const navigationTask = {
      name: 'navigation:setup',
      execute: async () => {
        // Set up any necessary navigation listeners
        // Configure deep link handling
        // Initialize navigation state persistence
      }
    };
    
    initializer.registerTask(InitStage.INITIAL_UI, navigationTask);
  }, []);
}

Progressive Loading UI

Create adaptive loading screens that show real-time initialization progress:

// ui/screens/LoadingScreen.tsx
import React from 'react';
import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
import { useInitialization } from '@/core/shared/app/initialization';
import { useTheme } from '@/ui/theme/ThemeProvider';
import { ProgressBar } from '@/ui/components/ProgressBar';
import { StatusMessage } from '@/ui/components/StatusMessage';
 
export function LoadingScreen() {
  const { currentStage, progress, statusMessage } = useInitialization();
  const theme = useTheme();
  
  return (
    <View style={[styles.container, { backgroundColor: theme.colors.background }]}>
      <Text style={[styles.title, { color: theme.colors.text }]}>
        {getLoadingTitle(currentStage)}
      </Text>
      
      <ActivityIndicator size="large" color={theme.colors.primary} />
      
      {progress !== undefined && (
        <ProgressBar 
          progress={progress} 
          style={styles.progressBar}
          color={theme.colors.primary}
        />
      )}
      
      {statusMessage && (
        <StatusMessage 
          message={statusMessage} 
          style={styles.statusMessage}
          color={theme.colors.secondary}
        />
      )}
    </View>
  );
}
 
function getLoadingTitle(stage) {
  switch (stage) {
    case 'initial-ui':
      return 'Setting things up...';
    case 'background':
      return 'Almost ready...';
    case 'finalization':
      return 'Finishing up...';
    default:
      return 'Loading...';
  }
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  title: {
    fontSize: 18,
    fontWeight: '600',
    marginBottom: 20,
  },
  progressBar: {
    width: '80%',
    height: 6,
    borderRadius: 3,
    marginTop: 30,
    marginBottom: 10,
  },
  statusMessage: {
    marginTop: 20,
    textAlign: 'center',
  },
});

Skeleton Screens for Content

Use skeleton screens to provide immediate visual structure while data loads:

// features/product-catalog/screens/ProductListScreen.tsx
import React from 'react';
import { View, FlatList, StyleSheet } from 'react-native';
import { useInitialization } from '@/core/shared/app/initialization';
import { ProductCard } from '../components/ProductCard';
import { ProductCardSkeleton } from '../components/ProductCardSkeleton';
import { useProducts } from '@/core/domains/products/hooks';
 
export function ProductListScreen() {
  const { initialized } = useInitialization();
  const { data: products, isLoading } = useProducts();
  
  // Show skeletons during initialization or while products are loading
  const showSkeletons = !initialized || isLoading;
  
  // Generate placeholder data for skeletons
  const skeletonData = Array(6).fill({ id: 'skeleton' });
  
  return (
    <View style={styles.container}>
      <FlatList
        data={showSkeletons ? skeletonData : products}
        renderItem={({ item }) => 
          showSkeletons ? (
            <ProductCardSkeleton />
          ) : (
            <ProductCard product={item} />
          )
        }
        numColumns={2}
        keyExtractor={(item) => item.id}
        contentContainerStyle={styles.list}
      />
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  list: {
    padding: 16,
  },
});

Initial UI Stage Registration

Register the Initial UI stage initialization tasks:

// core/shared/app/initialization/ui-tasks.ts
import { initializer, InitStage, InitTask } from './initializer';
import { navigationService } from '@/navigation/navigationService';
import { navigationEvents } from '@/navigation/events';
 
// Task to initialize navigation
const navigationInitTask: InitTask = {
  name: 'navigation:init',
  
  async execute(): Promise<void> {
    try {
      // Initialize navigation service
      navigationService.initialize();
      
      // Restore saved navigation state if available
      await navigationService.restoreNavigationState();
      
      // Register navigation state persistence
      navigationService.setupStatePersistence();
      
      // Notify that navigation is ready
      navigationEvents.emit('navigation:ready');
    } catch (error) {
      console.error('Navigation initialization failed:', error);
      // Continue with default navigation configuration
      navigationService.resetToDefaultState();
    }
  }
};
 
// Task to prepare layout animation settings
const layoutAnimationsTask: InitTask = {
  name: 'ui:animations',
  
  async execute(): Promise<void> {
    try {
      // Enable LayoutAnimation for smoother transitions
      if (Platform.OS === 'android') {
        UIManager.setLayoutAnimationEnabledExperimental(true);
      }
      
      // Set up default animation configurations
      LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
    } catch (error) {
      console.error('Layout animations setup failed:', error);
      // Continue without animation configuration
    }
  }
};
 
// Register all Initial UI stage tasks
export function registerInitialUITasks() {
  initializer.registerTask(InitStage.INITIAL_UI, navigationInitTask);
  initializer.registerTask(InitStage.INITIAL_UI, layoutAnimationsTask);
}
 
// Call this function in the app's initialization sequence

Common Challenges

Properly restore navigation state during the Initial UI stage:

// navigation/navigationService.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NavigationState } from '@react-navigation/native';
 
class NavigationService {
  private _navigator: NavigationContainerRef | null = null;
  private _isReady: boolean = false;
  
  initialize() {
    this._isReady = true;
  }
  
  setNavigator(navigator: NavigationContainerRef) {
    this._navigator = navigator;
  }
  
  async restoreNavigationState(): Promise<NavigationState | undefined> {
    try {
      const savedStateString = await AsyncStorage.getItem('navigation:state');
      
      if (savedStateString) {
        // Parse the saved state
        const state = JSON.parse(savedStateString);
        return state as NavigationState;
      }
      
      return undefined;
    } catch (error) {
      console.error('Failed to restore navigation state:', error);
      return undefined;
    }
  }
  
  setupStatePersistence() {
    if (!this._navigator) return;
    
    // Save navigation state on state change
    this._navigator.addListener('state', (e) => {
      const state = e.data.state;
      this.persistNavigationState(state);
    });
  }
  
  async persistNavigationState(state: NavigationState) {
    try {
      await AsyncStorage.setItem('navigation:state', JSON.stringify(state));
    } catch (error) {
      console.error('Failed to persist navigation state:', error);
    }
  }
  
  resetToDefaultState() {
    if (this._navigator && this._isReady) {
      this._navigator.resetRoot({
        index: 0,
        routes: [{ name: 'Auth' }],
      });
    }
  }
  
  // Additional navigation helper methods
}
 
export const navigationService = new NavigationService();

Handling Auth State Changes During Initial UI

React to authentication state changes during the Initial UI stage:

// core/domains/auth/initialization/navigation-handler.ts
import { initializer, InitStage, InitTask } from '@/core/shared/app/initialization';
import { authEvents } from '@/core/domains/auth/events';
import { navigationService } from '@/navigation/navigationService';
 
const authNavigationHandlerTask: InitTask = {
  name: 'auth:navigation-handler',
  dependencies: ['navigation:init'], // Depends on navigation being initialized
  
  async execute(): Promise<void> {
    // Set up auth state change listener
    authEvents.on('auth:stateChanged', (authState) => {
      if (authState.isAuthenticated) {
        // User logged in
        if (authState.isNewUser) {
          // New user - navigate to onboarding
          navigationService.navigate('Onboarding');
        } else {
          // Existing user - navigate to main flow
          navigationService.navigate('Main');
        }
      } else {
        // User logged out - navigate to auth flow
        navigationService.navigate('Auth');
      }
    });
    
    // Handle session expiration
    authEvents.on('auth:sessionExpired', () => {
      // Show session expired dialog and navigate to login
      navigationService.navigate('Auth', {
        screen: 'Login',
        params: { showSessionExpired: true }
      });
    });
  }
};
 
// Register task
initializer.registerTask(InitStage.INITIAL_UI, authNavigationHandlerTask);

Transitioning Between Loading States

Create smooth transitions between different loading states:

// ui/components/LoadingTransition.tsx
import React, { useEffect, useState } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import { useInitialization } from '@/core/shared/app/initialization';
 
interface LoadingTransitionProps {
  children: React.ReactNode;
  fallback: React.ReactNode;
}
 
export function LoadingTransition({ children, fallback }: LoadingTransitionProps) {
  const { initialized, currentStage } = useInitialization();
  const [showChildren, setShowChildren] = useState(false);
  const opacity = useState(new Animated.Value(0))[0];
  
  useEffect(() => {
    if (initialized) {
      // Delay showing children to allow for smooth transition
      const timer = setTimeout(() => {
        setShowChildren(true);
        
        Animated.timing(opacity, {
          toValue: 1,
          duration: 300,
          useNativeDriver: true,
        }).start();
      }, 150);
      
      return () => clearTimeout(timer);
    }
  }, [initialized, opacity]);
  
  if (!showChildren) {
    return <View style={styles.container}>{fallback}</View>;
  }
  
  return (
    <Animated.View style={[styles.container, { opacity }]}>
      {children}
    </Animated.View>
  );
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});
 
// Usage example:
// <LoadingTransition fallback={<SkeletonScreen />}>
//   <ProductListScreen />
// </LoadingTransition>

Performance Considerations

  • (Do ✅) Prioritize perceived performance

    • Show the UI shell as quickly as possible
    • Use skeleton screens instead of spinners
    • Animate transitions to mask loading times
  • (Do ✅) Defer non-essential UI elements

    • Render critical navigation components first
    • Use placeholders for content that requires data fetching
    • Load complex UI elements progressively
  • (Don't ❌) Block the UI thread

    • Avoid synchronous operations in render methods
    • Keep initialization logic off the main thread
    • Break up heavy operations into smaller chunks
  • (Do ✅) Measure and optimize render times

    • Track time-to-interactive metrics
    • Reduce component tree depth when possible
    • Use React.memo and useMemo appropriately

Practical Examples

Auth-Aware Navigation Container

// navigation/AuthAwareNavigationContainer.tsx
import React, { useRef, useState, useEffect } from 'react';
import { NavigationContainer, NavigationState } from '@react-navigation/native';
import { useAuthStore } from '@/core/domains/auth/store';
import { navigationService } from './navigationService';
import { useInitialization } from '@/core/shared/app/initialization';
import { linking } from './linking'; // Deep linking configuration
 
export function AuthAwareNavigationContainer({ children }) {
  const navigationRef = useRef(null);
  const { isAuthenticated } = useAuthStore();
  const { currentStage } = useInitialization();
  const [initialState, setInitialState] = useState(undefined);
  
  // Register with navigation service
  useEffect(() => {
    if (navigationRef.current) {
      navigationService.setNavigator(navigationRef.current);
    }
  }, [navigationRef.current]);
  
  // Load saved navigation state
  useEffect(() => {
    const loadNavState = async () => {
      const savedState = await navigationService.restoreNavigationState();
      if (savedState) {
        setInitialState(savedState);
      }
    };
    
    loadNavState();
  }, []);
  
  // Handle state changes
  const handleStateChange = (state) => {
    if (state) {
      // Track navigation analytics
      const currentRouteName = getActiveRouteName(state);
      analyticsService.trackScreenView(currentRouteName);
    }
  };
  
  return (
    <NavigationContainer
      ref={navigationRef}
      initialState={initialState}
      onStateChange={handleStateChange}
      linking={linking}
      theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}
    >
      {children}
    </NavigationContainer>
  );
}
 
// Helper to get the active route name
function getActiveRouteName(state) {
  const route = state.routes[state.index];
  
  if (route.state) {
    // Dive into nested navigators
    return getActiveRouteName(route.state);
  }
  
  return route.name;
}

Feature-Specific Loading Strategy

// features/orders/screens/OrdersScreen.tsx
import React, { useEffect } from 'react';
import { View, Text, FlatList, StyleSheet } from 'react-native';
import { useInitialization } from '@/core/shared/app/initialization';
import { useOrders } from '@/core/domains/orders/hooks';
import { OrderItem } from '../components/OrderItem';
import { OrderItemSkeleton } from '../components/OrderItemSkeleton';
import { EmptyOrdersView } from '../components/EmptyOrdersView';
import { ErrorView } from '@/ui/components/ErrorView';
 
export function OrdersScreen() {
  const { initialized } = useInitialization();
  const { 
    data: orders, 
    isLoading, 
    isError, 
    error, 
    refetch 
  } = useOrders();
  
  // Register feature-specific initialization
  useRegisterFeatureInitialization();
  
  // Determine what to show
  const showSkeletons = !initialized || isLoading;
  const showEmptyState = initialized && !isLoading && (!orders || orders.length === 0);
  const showError = initialized && isError;
  
  if (showError) {
    return (
      <ErrorView 
        error={error}
        onRetry={refetch}
        message="Could not load your orders"
      />
    );
  }
  
  return (
    <View style={styles.container}>
      {showEmptyState ? (
        <EmptyOrdersView />
      ) : (
        <FlatList
          data={showSkeletons ? Array(5).fill({ id: 'skeleton' }) : orders}
          renderItem={({ item }) => 
            showSkeletons ? (
              <OrderItemSkeleton />
            ) : (
              <OrderItem order={item} />
            )
          }
          keyExtractor={(item) => item.id}
          contentContainerStyle={styles.list}
        />
      )}
    </View>
  );
}
 
// Register feature-specific initialization
function useRegisterFeatureInitialization() {
  const { initializer, InitStage } = useInitialization();
  
  useEffect(() => {
    // Only register once
    if (initialized) return;
    
    const ordersInitTask = {
      name: 'features:orders-init',
      execute: async () => {
        // Prepare orders feature (set up event listeners, etc.)
        // This happens during Initial UI stage but doesn't block rendering
      }
    };
    
    initializer.registerTask(InitStage.INITIAL_UI, ordersInitTask);
    
    // More data-intensive tasks should be deferred to the Background stage
    // See the Background Initialization Guide for details:
    // /docs/guides/app-initialization/background
  }, [initialized]);
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  list: {
    padding: 16,
  },
});

Migration Considerations

When migrating from traditional loading approaches to our staged initialization pattern for Initial UI rendering, consider these helpful strategies:

From Global Loading States to Staged Loading

Many apps traditionally use a global loading pattern like this:

// Traditional approach
const MainScreen = () => {
  const [isLoading, setIsLoading] = useState(true);
  const [data, setData] = useState([]);
  
  useEffect(() => {
    async function loadData() {
      try {
        // Load everything at once
        await setupNavigation();
        const result = await fetchInitialData();
        setData(result);
        setIsLoading(false);
      } catch (error) {
        console.error('Failed to load', error);
        setIsLoading(false); // Still hide loading on error
      }
    }
    loadData();
  }, []);
  
  if (isLoading) {
    return <LoadingScreen />;
  }
  
  return <DataList data={data} />;
};

You can enhance this by:

  1. Separating UI rendering from data loading
  2. Using the InitStage.INITIAL_UI for navigation and essential UI setup
  3. Deferring data fetching to the Background stage
  4. Implementing skeleton screens or placeholders during the transition

Progressive Enhancement Pattern

A good migration strategy involves progressive enhancement:

  1. First render the UI shell with skeleton placeholders during the Initial UI stage
  2. Then populate data gradually as Background tasks complete
  3. Finally, enable full interactivity after all initialization is complete

This approach significantly improves perceived performance compared to blocking renders until all data is available.

Component-Level Migration

You can migrate component by component:

// Migration approach for a specific feature
const ProductScreen = () => {
  const { initialized, currentStage } = useInitialization();
  const { data, isLoading } = useProducts();
  
  // Register initialization once
  useEffect(() => {
    initializer.registerTask(InitStage.INITIAL_UI, {
      name: 'products:ui-setup',
      execute: async () => {
        // Only essential UI setup here
        setupProductScreenLayout();
      }
    });
    
    initializer.registerTask(InitStage.BACKGROUND, {
      name: 'products:data-fetch',
      execute: async () => {
        // Move data fetching to background
        queryClient.prefetchQuery(productQueryKeys.list);
      }
    });
  }, []);
  
  // Determine what to show based on initialization state
  const showSkeletons = currentStage === InitStage.INITIAL_UI || 
                        (currentStage === InitStage.BACKGROUND && isLoading);
  
  return (
    <Screen>
      {showSkeletons ? <ProductSkeleton /> : <ProductList data={data} />}
    </Screen>
  );
};

This progressive approach lets you adopt the new initialization pattern one feature at a time.

Summary

The Initial UI rendering stage is focused on providing immediate visual feedback to users while the application continues to initialize background services. By following the patterns in this guide, you can create a smooth, responsive user experience from the moment your app launches.

Once the Initial UI stage completes, your app can proceed to the background for non-critical tasks, and finally the finalization to complete the initialization process.

Key principles to remember:

  • Focus on perceived performance with immediate visual feedback
  • Use skeleton screens and placeholders for content that's still loading
  • Set up navigation and routing based on authentication state
  • Register initialization tasks specific to UI rendering
  • Create smooth transitions between loading and loaded states

Core Initialization Documentation

Other Initialization Stages