Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Splash Screen Orchestration #56

Merged
merged 3 commits into from
Oct 8, 2022
Merged

Conversation

EvanBacon
Copy link
Contributor

@EvanBacon EvanBacon commented Oct 5, 2022

Motivation

Developers either don't set up any splash screen management which results in a white flash, or they add partial support which results in frustrating cases like "splash screen stays open endlessly" when there's some error starting the app. Developers also usually don't account for custom splash screen handling on deep links, this results in a suboptimal experience when opening any other route besides the initial route.

Solution

This PR introduces Splash Screen Orchestration which automatically does most of the splash screen loading behind the scenes.

splash-compare.mp4

Loading a single route before / after. No splash screen-specific code was added to the demo app.

Features

  • To prevent UI flash, Expo Router will automatically suspend the splash screen timeout until the first route (this can be any route) has finished mounting. This is only enabled when using expo-router/entry to start the app.
  • Developers can extend the default splash screen timeout by mounting a SplashScreen component in their route, this enables custom asset loading/data fetching behavior. This customization shouldn't break any of the Expo Router safeguards.
  • Default behavior is optimal, developers only need to extend the splash screen behavior if they want to load something like fonts before dismissing the splash screen.
  • Splash Screen Orchestration can be modularized into individual components and automatically scheduled to ensure any combination of routes has an optimal loading policy.
  • The entry component is wrapped in a try/catch which hides the splash screen when the initial javascript or any side-effects have thrown an exception.
  • Uncaught exception handling is installed to ensure errors thrown in hooks or event handlers automatically force the splash screen to be hidden. This ensures the user has the highest chance of seeing any relevant error information or UI.

Uncaught Exceptions

Consider the following, where the splash has no chance to unmount. In this case, the global error handler will force dismiss the splash screen to show whatever UI is loaded behind it.

import { Tabs, SplashScreen } from "expo-router";
import React from "react";

export { ErrorBoundary } from "expo-router";

export default function RootLayout() {
  const [isReady, setReady] = React.useState(false);

  React.useEffect(() => {
    setTimeout(() => {
      throw new Error("hey");
      setReady(true);
    }, 1000);
  }, []);

  return (
    <>
      {!isReady && <SplashScreen />}
      <Tabs />
    </>
  );
}

Component Errors

  • We now force dismiss the splash screen in the ErrorBoundaries so any component error will show the UI. If there is no error handling UI then Expo Router doesn't know what to do -- white UI is shown behind error overlay.
  • If the root layout route has export { ErrorBoundary } from "expo-router"; then the default error component can be shown for root component errors.

Example

Consider we have a font demo app that lists a collection of fonts with a master/detail structure. This would require two distinct leaf nodes /index and /[font]:

app/
  (root).tsx
  (root)/
    index.tsx
    [font].tsx

In a traditional native app, we would probably delay the rendering of every route until all the fonts in the list view (app/(root)/index.tsx) have loaded. With Splash Screen Orchestration we can co-location font loading logic into each individual route providing much faster results when the user deep links into app://custom-font (app/(root)/[font].tsx) which only needs to load a single font.

We can configure the app/(root).tsx to prevent rendering anything until some fonts have loaded:

import { Stack } from "expo-router";

export default function RootLayout() {
  // No special fonts are required for the layout route so we don't need any splash screen logic.
  return <Stack />
}

In our app/(root)/index.tsx we can add the heavy loading required for this specific route:

import React from "react";
import { SplashScreen } from 'expo-router';
import { Text } from 'react-native';

export default function ListView() {
  const [isReady, setReady] = React.useState(false);

  React.useEffect(() => {
    setTimeout(() => {
      // Fake font loading for 1 second.
      setReady(true);
    }, 1000);
  }, []);

  if (!isReady) return <SplashScreen />

  return <MyFontList />
}

Now consider a user deep links directly into a font detail page app://my-font (app/(root)/[details].tsx) which requires a single font:

import React from "react";
import { SplashScreen } from 'expo-router';
import { Text } from 'react-native';

export default function DetailView({ route }) {
  const [isReady, setReady] = React.useState(false);
  const { details } = route.params
  React.useEffect(() => {
    setTimeout(() => {
      // Fake font loading for 100ms.
      setReady(true);
    }, 100);
  }, [details]);

  if (!isReady) return <SplashScreen />

  return <FontDetails />
}

The data fetching policy is now:

  • User starts the app from the home screen or with app://: Router loads the following in parallel <VirtualNavigationContainer />app/(root).tsxapp/(root)/index.tsx. When all data fetching requirements have been met, the splash screen will be dismissed. In our mock scenario, this will take ~1 second.

  • User starts the app with a deep link to app://helvetica: Router loads the following in parallel <VirtualNavigationContainer />app/(root).tsxapp/(root)/[detail].tsx. In our mock scenario, this will take ~100 ms.

Further work

This feature is clearly analogous to data fetching/suspense in modern web frameworks but targeted toward native app launches. In the mock scenario, we don't account for the case where the user navigates back to the list (/) from the detail page after deep linking directly into it. We can extend Splash Screen Orchestration to be more generalized and account for segueing between routes after the initial load. This could also account for loading/prefetching bundle split routes on the web (not implemented in Metro yet).

The first and most important feature is to automatically provide a reasonable splash screen policy for the majority of routes that have no special data fetching/asset loading requirements. This PR currently does this, but the question is whether or not the current proposal blocks a more generalized suspense integration.

@trin4ik
Copy link
Contributor

trin4ik commented Oct 5, 2022

looks like expo/router wants to be new president )) cool!

expo-splash-screen also great, but no customization on both.

i mean i want replace system splash screen fast as it possible by my custom component (with same screen, like splash, but with status loading, like: check updates, loading fonts, loading data, etc). it external component for router (external for app directory), but SplashScreen render it like system splash screen.
what you think?

@EvanBacon
Copy link
Contributor Author

looks like expo/router wants to be new president

I'd like to keep Expo Router as flexible as possible. This feature is a side-effect of having NavigationController be maintained in the framework rather than the app. We need a solution for all the properties that users would normally have easy access to. One of the main reasons to use the onReady prop is to hide the splash screen after the route has loaded. This proposed feature eliminates the general need for using onReady while still exposing some meaningful, but awkward ways of extending onReady manually (<RootContainer /> context).

Other props, like theme, are well suited for cases where you may want to configure the global value from a nested component like a settings screen -- for these, we can skip having some opinionated system built-in.

expo-splash-screen is also great, but no customization ...

We can improve the API to better suit the needs of the developers. One thing that would be good to add is the ability to determine if the splash is currently open or not.

I want replace the system splash screen fast as possible by my custom component

This management by default will keep the splash screen visible during the time the native app starts and the time it takes to mount the UI (consider this the first contentful paint). You can still immediately present a custom screen as soon as possible. You can also force dismiss the splash screen before React has mounted by adding a global side-effect to hide the splash screen (same as before) but you won't be able to render a new component during this time, it'll just be a flash.

@trin4ik
Copy link
Contributor

trin4ik commented Oct 5, 2022

I'd like to keep Expo Router as flexible as possible.

i means, i like how easy expo packages turns on in router.

You can still immediately present a custom screen as soon as possible.

I would like replace system splash screen as soon as possible with my own component, but continue the current behavior, like the system splash screen. I would put it in the configuration of the expo router. like:

  • auto splash screen enable by default:
import "./wdyr";
import "@bacons/expo-metro-runtime";

import "expo-router/entry";
  • disable auto splash screen
import "./wdyr";
import "@bacons/expo-metro-runtime";

import ExpoRouter from "expo-router/entry";

ExpoooRouter({
  splashScreen: false
});
  • custom splash screen with behavior system splash screen (hide onReady, etc)
import "./wdyr";
import "@bacons/expo-metro-runtime";
import CustomSplashScreen from "./src/component/CustomSplashScreen";
import ExpoRouter from "expo-router/entry";

ExpoooRouter({
  splashScreen: CustomSplashScreen
});

i.e. if splashScreen !== false, expo-router manage splash screen, hide it when app onReady. but if splashScreen is react component, then expo-router replace system splash screen onReady to CustomSplashScreen and hide CustomSplashScreen, when app onReady. I hope I was able to explain the idea.

We can improve the API to better suit the needs of the developers. One thing that would be good to add is the ability to determine if the splash is currently open or not.

need more hooks ;)

@EvanBacon EvanBacon marked this pull request as ready for review October 6, 2022 21:14
@EvanBacon EvanBacon merged commit ae9d721 into main Oct 8, 2022
@EvanBacon EvanBacon deleted the @evanbacon/splash-override branch October 8, 2022 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants