Search code examples
react-nativereduxexpoasyncstorageuse-context

React-Native (expo) Retrieve an Initial Theme (or any initializing value)


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?


Solution

  • 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:

    1. 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.
    2. AppLoading blocks component rendering at the splash screen and invokes a function containing the work to be done before render.
    3. 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.
    4. 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:

    1. 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
    2. 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.
    3. 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.
    4. 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.
    5. 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} ...>