I'd like to introduce a simple configurable theme to my (expo augmented) react-native app, with initial focus on the background color. I've had some success implementing the dynamic aspects, e.g. the user can pick a theme/background color in the app, and the color affects all the screens. I used a fairly typical techinique where the App.js wraps its (V5) NavigationContainer and its theme in a Context provider, e.g.
return (
<SomeProvider>
<NavigationContainer theme={AppDefaultTheme} ref={navigationRef}>
... stuff including navigators
<NavigationContainer>
</SomeProvider>
)
On the component side we can retrieve the theme (initially the default) using the useTheme hook, then change the background or whatever, and (temporarily) persist the mutated theme to state in the Provider's reducer. The reducer can also persist the mutated theme to something like AsyncStorage for the next application start.
The application restart is where the problem comes in.
There seems to be no way to retrieve the current theme from cold storage (AsyncStorage or other) in App.js in time to supply it to the NavigationContainer
(remember this)
<NavigationContainer theme={AppDefaultTheme} ref={navigationRef}>
Numerous posts point out that there is no beforeRender hook or capability in RN/expo. And by the time we haul back AppDefaultTheme in a useEffect hook, the App.js NavigationContainer has long since left the station.
I'm guessing that retrieving initial state just isn't possible unless perhaps using an external state manager like Redux. Is Redux how people have solved this problem? I've been very happy with "nonReduxLight", e.g. redux patterns using Provider wrappers per the above, and a color theme feature isn't quite enough to get me to dive into that deep swimming pool, but it's getting close.
Will Redux do what I want? Is there an easier way?
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:
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:
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} ...>