ScaleRocket/Mobile

Loading

ActivityIndicator, skeleton screens, and splash screen configuration for loading states.

Loading

ScaleRocket Mobile provides multiple loading patterns for different contexts — inline spinners, skeleton placeholders, and the initial splash screen.

ActivityIndicator

The simplest loading state uses React Native's built-in ActivityIndicator:

import { ActivityIndicator, View } from "react-native";

function LoadingScreen() {
  return (
    <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
      <ActivityIndicator size="large" color="#3B82F6" />
    </View>
  );
}

Use the themed version for consistency:

import { Loading } from "@/components/ui/Loading";

// Full screen loading
<Loading />

// Inline loading with text
<Loading size="small" message="Saving..." />

Loading Wrapper

Wrap data-dependent screens with a loading guard:

import { Loading } from "@/components/ui/Loading";

export default function DashboardScreen() {
  const { data, isLoading } = useQuery();

  if (isLoading) return <Loading />;

  return (
    <ScreenLayout>
      <Text>{data.title}</Text>
    </ScreenLayout>
  );
}

Skeleton Screens

Skeleton screens provide a better user experience than spinners by showing the layout shape while data loads:

import { View, StyleSheet } from "react-native";
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withRepeat,
  withTiming,
} from "react-native-reanimated";

function SkeletonBox({ width, height }: { width: number; height: number }) {
  const opacity = useSharedValue(0.3);

  React.useEffect(() => {
    opacity.value = withRepeat(withTiming(1, { duration: 800 }), -1, true);
  }, []);

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
  }));

  return (
    <Animated.View
      style={[
        { width, height, backgroundColor: "#E5E7EB", borderRadius: 8 },
        animatedStyle,
      ]}
    />
  );
}

Use skeletons to mirror the real layout:

function CardSkeleton() {
  return (
    <View style={{ padding: 16, gap: 12 }}>
      <SkeletonBox width={120} height={16} />
      <SkeletonBox width={200} height={32} />
      <SkeletonBox width={160} height={14} />
    </View>
  );
}

function DashboardSkeleton() {
  return (
    <ScreenLayout>
      <CardSkeleton />
      <CardSkeleton />
      <CardSkeleton />
    </ScreenLayout>
  );
}

Pull to Refresh

Add pull-to-refresh on list screens:

import { RefreshControl, FlatList } from "react-native";

function ItemList() {
  const [refreshing, setRefreshing] = useState(false);

  const onRefresh = async () => {
    setRefreshing(true);
    await refetchData();
    setRefreshing(false);
  };

  return (
    <FlatList
      data={items}
      renderItem={({ item }) => <ItemRow item={item} />}
      refreshControl={
        <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
      }
    />
  );
}

Splash Screen

Configure the app splash screen in app.json:

{
  "expo": {
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    }
  }
}

Keep the splash visible while loading initial data:

import * as SplashScreen from "expo-splash-screen";

SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const { isLoading } = useSession();

  useEffect(() => {
    if (!isLoading) {
      SplashScreen.hideAsync();
    }
  }, [isLoading]);

  if (isLoading) return null;

  return <Slot />;
}

When to Use What

ContextPattern
Initial app loadSplash screen
Screen data loadingSkeleton screen
Button actionActivityIndicator inside button
List refreshPull-to-refresh
Background saveToast notification on complete

On this page