Glad of the interest. I'll answer my own question for now, as research has been fruitful.
Expo offers the AppLoading package, which appears to 'hold onto' the initial splash screen while other, usually asynchronous operations are carried out.
This enables the following pattern:
- App.js (or your initial component) invokes AppLoading in the render/return method based on application state. State need not be tied to a context, so it works fine in App.js before context is created.
- AppLoading blocks component rendering at the splash screen and invokes a function containing the work to be done before render.
- In the invoked function do whatever you need to do to initialize the application. In this particular example, we want to retrieve a modifiable theme from cold storage before App.js accesses and wraps the app in a context. The hydrating operations in the invoked function are usually asynchronous, but I think need not be.
- When the promises are fulfilled, e.g. you've hydrated whatever you wanted to fetch from the web or cold storage, flip the state that's holding the splash screen in suspense, which allows further rendering, and consume the results you hydrated. In this particular case, use the retrieved theme to initialize a navigator
I'm using it in Expo 40 and this pattern works really well. Architecturally it's a nice pattern ... it inserts a "before render anything" in the lifecycle, which isn't quite as nice as the absent "before render this component" event, but for the purposes of pre-execution hydration it's perfect.
It doesn't depend on context at all, so it can be used in App.js before the context is invoked to wrap the app.
Couple of notes, then some examples:
- I've only tested in Expo, but the component notes state it can be
used in vanilla react-native by importing an initial dependency or
two
- The package documentation and example are great, but refer to class-based components. If you love functional components, as I do, here's a great functional component example.
- AppLoading component appears to be a convenience wrapper around the Expo SplashScreen component, which adds some extra functionality I don't need now. You might.
- Incredibly cool ... can be used in more than one component. For example you might have a component that loads theme, probably in App.js per our main use case. The App.js work doesn't need access to Context, and in fact must happen before context is created, per the bulk of this article.
- But you might possibly have another component that needs to load some preferences from the web or cold storage, and it does need access to Context. No problem. Put another implementation of AppLoading in the second component and hydrate away.
Don't be confused ... both AppLoading invocations are waiting on the same set of events to release the splash screen, so this is not a way around the 'no before render' general problem. But it does look like a solution to the 'before rendering anything problem.
One last ... there seem to be lots of solutions to this problem that use Redux to carry out the hydration -- and they are probably all fine. However this case didn't rise to the level of rewriting the app that heavily and happily uses Context hooks instead of Redux. If I ever learn and rewrite with Redux I'll probably hydrate there.
OK, enough gab. Here's the basics of the pattern. getColorTheme is a custom method to do whatever is needed to get the cold storage data.
In App.js or whatever loads first:
import React, { useState } from 'react';
import AppLoading from 'expo-app-loading';
... whatever else you need
function App() {
// This bit of component state controls whether apploading is finished or not
const [isReady, setReady] = useState(false);
// This piece of component state holds the hydrated theme to pass to navigation
const [colorTheme, setColorTheme] = useState({});
...
// Invoked while the splash screen is showed. Initialize resources here
const _cacheResourcesAsync = async () => {
const storedColorTheme = await getColorTheme(); // an app specific method to fetch theme from cold storage
setColorTheme(storedColorTheme) // now stash the theme in state for retrieval in render
return;
}
and eventually render and invoke our method using AppLoading:
return (
// Carry out initializing actions, then release the splash screen
isReady === false ? (<AppLoading
startAsync={_cacheResourcesAsync}
onFinish={() => setReady(true)}
onError={console.warn}
/>) :
<MyContextProvider>
<NavigationContainer theme={colorTheme} ...>