Typed Config via Context in React Native

January 23, 2021 - 4 min read

Typed Config via Context in React Native

Contexts are common in most programming languages or frameworks. They are mostly used to contain and share specific information or functionality across different parts of an application. Sometimes they are used to inject behavior depending on the use case, like testing or production. I explain how I have used React Context to share configuration data across a React Native app with TypeScript support.

In Android app development, the context is used to access application environment specific information. Things like starting new activities (app instances), services/broadcasts (like alarm clocks) or theme data is handled by that.

In Flutter, the purpose of the context property is to localize the widget inside the app's hierarchy tree. With that, you could also perform media queries to get the device size, or to retrieve theme data.

React Context

In React, same applies to React Native, context helps you with sharing data between different parts (components) of your application. It is to say that this has to happen top-down, so you have to provide data "early" in your application in order for child components to consume that data. This explain the two important concepts: Context.Provider and Context.Consumer.

As docs also state that you should only use React Context for global app information, such as user information or language settings. It generally helps you to share data between different nesting levels. Otherwise, you could pass data via props and/or compose different components, so that these components share specific state.

Frequent changes

Important to highlight is the fact of component re-rendering. You have to be careful about unnecessary rendering of components that consume a context that changes often. This could be solved by context splitting, means that you keep rarely changing data in the global context and create further contexts that only contain specific, frequently changing data.

In this example I used react-native-config to add environment specific that I then added to a app config via context. You can then easily put information to the .env file and consume that in the app. This could also be combined with a dynamic replacement of secrets for your application, so that you can keep sensitive data in your CI/CD for example. You can see an example in one of my apps. Please note that secrets will still appear in your application, since they are inside your JavaScript bundle. The benefit is that you can hide it in your version control.

// .env
SOME_API_KEY=4242a4123frdfvsdcv
GOOGLE_MAPS_KEY=REPLACE_GOOGLE_MAPS_KEY

Since we want to use the benefits of TypeScript, we will create an interface for the shared Config data. This will include some arbitrary data and a set of the secrets that we put in the .env file.

// config.ts
import Config from 'react-native-config';
export interface Store {
config: Config;
}
type EnvKeys = 'SOME_API_KEY';
interface Config {
testValue: number;
env: {
[key in EnvKeys]: string;
};
}
const initialStore: Store = {
config: {
testValue: 42,
env: {
SOME_API_KEY: Config.SOME_API_KEY,
},
},
};

When setting up the context data, you want to have specified the initialStore for passing it to the context creator. From there on, we can export and later use the Context.Provider as a component wrapper. You could then export the created context and consume it using React.useContext or make it simpler by creating a wrapper hook called useConfig.

// config.ts
export const setupStore = async (): Promise<Store> => {
// we could perform asynchronously loading operations here
return initialStore;
};
const RootStoreContext = React.createContext<Store>(initialStore);
/**
* The provider to expose the root store
*/
export const RootStoreProvider = RootStoreContext.Provider;
export const useConfig = () => React.useContext(RootStoreContext).config;

Finally, on top of wrapping your app in the context provider, you actually create its state and pass it to the provider. That way the context can be used to share the data. There you could also perform asynchronous loading of config data (often called hydration), for example to load changed theme or language preferences from the device.

// app.tsx
const App = () => {
const [store, setStore] = React.useState<null | Store>(null);
React.useEffect(() => {
setupStore().then(setStore);
}, []);
if (!store) {
return null; // or loading screen
}
return (
<RootStoreProvider value={store}>
<SafeAreaView style={{ flex: 1 }}>
<View>
<Example />
</View>
</SafeAreaView>
</RootStoreProvider>
);
};

And finally, you will see the usage of useConfig in the Example component. This way you can easily access config variables from any component of your React Native app.

// example.tsx
const Example = () => {
const { testValue, env } = useConfig();
return (
<View>
<Text>{testValue}</Text>
{/* 42 */}
<Text>{JSON.stringify(env)}</Text>
{/* { SOME_API_KEY: 4242a4123frdfvsdcv } */}
</View>
);
};

Originally published at https://mariusreimer.com on January 23, 2021.