skip to content

In an Expo app, what does expo-splash-screen's preventAutoHideAsync() do, where should you call it, and how can it make startup feel slower?

level: juniorimportance: should knowfreq 42%

answer

  1. splash auto-hides by default
  2. call it at module top level
  3. hide() or hideAsync() when ready
  4. release the splash in finally
  5. never wait on the network

basics

~10 s

preventAutoHideAsync() keeps the native splash screen up until you call SplashScreen.hide(). Call it at module top level, hide as soon as the first screen can render, and never make it wait on slow work.

solid answer

~50 s

By default the Expo splash screen hides automatically when the app is ready. `SplashScreen.preventAutoHideAsync()` from `expo-splash-screen` turns that off, so the native splash stays until you call `SplashScreen.hide()` (or `hideAsync()`). The docs call it at the **top level** of the root module - `app/_layout.tsx` with Expo Router - so it runs before the first render. Use it to avoid a flash of half-ready UI while fonts or a cached session load. The risk is that everything you await before hiding becomes **visible startup time**: waiting on a network request ties time-to-interactive to the slowest connection, and an error path that never hides leaves users stuck on the splash. Load only what the first screen needs, hide in a `finally`, and render cached or skeleton content for the rest. Check the result on a release build, since Expo Go and development builds do not fully reproduce the splash.

code

tsx · 34 lines
tsx
import { Stack } from 'expo-router';
import * as SecureStore from 'expo-secure-store';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect, useState } from 'react';

// Module top level: runs before the first render, while the native splash is up.
SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const [ready, setReady] = useState(false);
  const [signedIn, setSignedIn] = useState(false);

  useEffect(() => {
    async function prepare() {
      try {
        // Only what the first screen needs: a local session, not network data.
        const token = await SecureStore.getItemAsync('session');
        setSignedIn(token !== null);
      } catch (e) {
        console.warn(e);
      } finally {
        setReady(true); // always release the splash, even on error
      }
    }
    prepare();
  }, []);

  useEffect(() => {
    if (ready) SplashScreen.hide();
  }, [ready]);

  if (!ready) return null;
  return <Stack screenOptions={{ headerShown: signedIn }} />;
}

go deeper

for a junior

Know that the Expo splash hides automatically, that preventAutoHideAsync keeps it up until SplashScreen.hide(), and that it is called at the top of the root module.

for a middle

Explain why the call belongs at module top level, what is worth awaiting before hiding, and why the release build is where splash timing is checked.

for a senior

Show you keep network work off the splash, guarantee release in a finally, and design the first screen to render from cached or skeleton data.

for a principal

Discuss how splash behaviour fits the team's startup target and who decides what may block the first screen.

## What the splash screen is doing When a React Native app cold-starts, there is a gap between the process starting and your first screen being ready. The native **splash screen** covers that gap. In an Expo project it is configured through the `expo-splash-screen` config plugin (image, background colour, dark-mode variant) and controlled at runtime through the `SplashScreen` module. The default is simple: the splash **hides automatically** when the app is ready. For most apps that is enough. ## What `preventAutoHideAsync()` changes `SplashScreen.preventAutoHideAsync()` switches auto-hide off. The splash then stays visible until your code calls `SplashScreen.hide()` (the docs also show `hideAsync()`). The companion `SplashScreen.setOptions({ duration, fade })` controls the hide animation. Where to call it matters: - The docs call it at the **top level of the root module**, outside any component: `app/_layout.tsx` in an Expo Router app, or `App.tsx` without it. - That placement runs it when the module is evaluated, **before the first render**. - Calling it inside an effect runs it after the first render, by which point the automatic hide may already have happened. This is one of the few module side effects that belongs at the top level on purpose. ## Legitimate reasons to hold the splash Keep it only for things the **first screen cannot render correctly without**: - custom fonts, to avoid text jumping from a system font; - a locally stored session, to decide between the sign-in flow and the home screen; - a small amount of locally cached state. ## How it makes startup slower Everything awaited before `hide()` adds directly to the time the user stares at a static image: | Pattern | Effect on perceived startup | |---|---| | Await fonts and a local session read | small and predictable | | Await a network request for home-screen data | startup now depends on the connection | | Await several requests in sequence | latencies add up | | An error path that never calls `hide()` | the app appears frozen on the splash | | A long fade `duration` | extra time with nothing interactive | In a supermarket app, holding the splash until this week's offers arrive from the server can turn a quick launch into a multi-second wait on a weak connection. The better design is to hide once fonts and the cached session are ready, then show the home screen with cached or skeleton content and fill in offers as they arrive. The docs state the goal directly: hide the splash screen as soon as possible. ## Splash, skeleton or cached content Holding the splash is one of three ways to cover the moments before real content exists, and each fits a different wait: - **The native splash**: for the short, predictable, local work that decides what the first screen is, such as fonts and a stored session. - **A skeleton screen**: for content whose layout you know but whose data is still loading; the app is already interactive around it. - **Cached content**: for data that changes slowly, like a store's product categories, shown immediately and refreshed in the background. Moving network-bound waits from the first option to the second or third is usually the single biggest improvement in perceived startup. ## Doing it safely 1. Call `preventAutoHideAsync()` at the top level of the root module. 2. In an effect, `await` only local, fast work. 3. Set a `ready` flag in a **`finally`** block so errors still release the splash. 4. Call `SplashScreen.hide()` when `ready` becomes true, and render the first screen. 5. Test on a **release build**. Since SDK 52, Expo Go shows the app icon instead of your splash and development builds do not reflect every property, so only a release build shows what users see. ## Pitfalls - Treating the splash as a loading screen for network data. - Forgetting the error path, so a failed read keeps the splash forever. - Calling `preventAutoHideAsync()` from a screen deep in the tree instead of the root module. - Judging splash timing in Expo Go, which does not show the real splash. - Returning `null` for longer than needed after hiding, which swaps a splash for a blank screen.

  • Why call preventAutoHideAsync() at module top level instead of inside a useEffect?
    Top-level code runs when the root module is evaluated, before React renders anything. An effect runs only after the first render, by which point the splash may already have hidden automatically, so the call would come too late to keep it up.
  • How would you test that the splash hides at the right moment?
    Build and install a release build, since Expo Go shows the app icon instead of your splash and development builds do not reproduce every property. Cold-start it several times on a slow device and on a poor connection to confirm the splash never waits on the network.

Holding the splash is like keeping a shop's shutters down until the shelves are stocked. Worth it for the price tags on the door; not worth it for a delivery truck stuck in traffic, when you could open and restock as it arrives.

saying these in an interview costs you the question

  • preventAutoHideAsync is required or the splash never hides
  • Calling it inside a component effect works just as well
  • Holding the splash until API data arrives makes startup faster
  • Expo Go shows exactly the splash users will see
  • An error while loading is fine because the splash hides eventually