Using variant in React Native π€π
Setting up a monorepo for one app with different declination
Published Mar 05, 2021 
Tags: React Native React TypeScript
For some reasons, your React Native project might be used for multiple applications, each one having its own logic such as authentication or visual identity. Itβs possible to use variant (for Android) or schemes (for iOS) to build apps with differents app id but what about logic and visual differences ?
In this blog post we will build a React Native app that can be build for 2 differents contexts.
Creating the project
First of all we will create a new React Native project using Typescript.
npx react-native init AppDeclination --template react-native-template-typescript
see commit senorihl/AppDeclination#80dd7e2 for current state
We will now add some code that display a simple list of person.
A simple app
First of all we will change directory structure using src/ directory as follow :
.
βββ android/
βββ ios/
βββ src/
β   βββ component/
β   β   βββ /* We will put components here */
β   βββ helpers/
β   β   βββ /* We will put helpers here */
β   βββ interface/
β   β   βββ /* We will put object interfaces here */
β   βββ App.tsx
βββ index.js
In the app we will display a simple list using react-native-elements and FlatList :
// src/App.tsx
import React from "react";
import {StyleSheet, Platform, StatusBar, FlatList} from "react-native";
import {Person} from "./interfaces/Person";
import {Header} from "react-native-elements";
import {SafeAreaView, SafeAreaProvider} from "react-native-safe-area-context";
import PersonItem from "./component/PersonItem";
const App = () => {
    const [source, setSource] = React.useState<string>("empty");
    const [persons, setPersons] = React.useState<Person[]>([]);
    React.useEffect(() => {
    }, []);
    return (
        <>
            <Header
                centerComponent={{
                    text: `Users [${source}]`,
                    style: {color: "#fff", fontSize: 30},
                }}
            />
            <SafeAreaProvider>
                <StatusBar barStyle="light-content"/>
                <SafeAreaView>
                    <FlatList
                        data={persons}
                        renderItem={({item}) => <PersonItem person={item}/>}
                        keyExtractor={(item) => `${item.id}`}
                    />
                </SafeAreaView>
            </SafeAreaProvider>
        </>
    );
};
export default App;
And using interface:
// src/interfaces/Person.ts
export interface Person {
    id: number;
    email: string;
    first_name: string;
    last_name: string;
    avatar?: string;
}
And PersonItem component:
// src/component/PersonItem.tsx
import * as React from "react";
import {Linking} from "react-native";
import {Person} from "../interfaces/Person";
import {Avatar, ListItem} from "react-native-elements";
const PersonItem: React.FunctionComponent<{ person: Person }> = ({
                                                                     person,
                                                                 }) => {
    return (
        <ListItem
            onPress={() => {
                Linking.openURL(`mailto:${person.email}`).catch((e) => console.warn(e));
            }}
        >
            {person.avatar && (
                <Avatar rounded size="medium" source={{uri: person.avatar}}/>
            )}
            <ListItem.Content>
                <ListItem.Title>
                    {person.first_name} {person.last_name}
                </ListItem.Title>
                <ListItem.Subtitle style={{color: "#aaa"}}>
                    Tap to send email to {person.email}
                </ListItem.Subtitle>
            </ListItem.Content>
        </ListItem>
    );
};
export default PersonItem;
see commit senorihl/AppDeclination#756ea64 for current state
Ok now that we have an empty list of users, whatβs next ?
The real work
We need to declare some variants, letβs use one and two (simpler is better Β―\_(γ)_/Β―). Lets create a new directory at root which exposes all variants and declaration:
.
βββ android/
βββ ios/
βββ variants/
β   βββ one/
β   β   βββ index.ts
β   βββ two/
β   β   βββ index.ts
β   βββ definition.ts
βββ src/
β   βββ component/
β   β   βββ /* We will put components here */
β   βββ helpers/
β   β   βββ /* We will put helpers here */
β   βββ interface/
β   β   βββ /* We will put object interfaces here */
β   βββ App.tsx
βββ current-variant.d.ts
βββ index.js
For the example these two variants will only expose a fetchUsers method and a source property.
// variants/one/index.ts
import {Person} from "../../src/interfaces/Person";
const definition = {
    source: "one", // or 'two' for the other variant
    fetchUsers: async () => {
        const res = await fetch("https://reqres.in/api/users?page=1"); // or 'https://reqres.in/api/users?page=2' for the other variant
        const data = await res.json();
        return data.data as Person[];
    },
};
export default definition;
In order to prevents excessive load of javascript files using a wrapper like a setCurrentVariant and getCurrentVariant directly inside App.tsx we will optimize imports with babel.
So, in the babel.config.js file we add a plugin:
const path = require("path");
const VARIANT = process.env.VARIANT || "one";
if (!VARIANT) {
    throw new Error(
        `You must define a VARIANT environment variable when bundling typescript module`
    );
}
const plugins = [
    [
        require.resolve("babel-plugin-module-resolver"),
        {
            extensions: [".ts", ".tsx"],
            alias: {
                "current-variant": ([, requirePath]) =>
                    path.resolve(__dirname, "variants/" + VARIANT + requirePath),
            },
        },
    ],
];
module.exports = {
    presets: ["module:metro-react-native-babel-preset"],
    plugins,
};
Explanation: We add an alias for babel with
babel-plugin-module-resolver. This will map any import ofcurrent-variantto the corresponding variant directory
Now we can use an false current-variant package in App.tsx to populate users:
// src/App.tsx
import React from "react";
import {StyleSheet, Platform, StatusBar, FlatList} from "react-native";
import {Person} from "./interfaces/Person";
import {Header} from "react-native-elements";
import {SafeAreaView, SafeAreaProvider} from "react-native-safe-area-context";
import PersonItem from "./component/PersonItem";
import variant from "current-variant";
const App = () => {
    const [source, setSource] = React.useState<string>("empty");
    const [persons, setPersons] = React.useState<Person[]>([]);
    React.useEffect(() => {
        variant.fetchUsers().then((users) => {
            setPersons(users);
            setSource(variant.source);
        });
    }, []);
    return (
        <>
            <Header
                centerComponent={{
                    text: `Users [${source}]`,
                    style: {color: "#fff", fontSize: 30},
                }}
            />
            <SafeAreaProvider>
                <StatusBar barStyle="light-content"/>
                <SafeAreaView>
                    <FlatList
                        data={persons}
                        renderItem={({item}) => <PersonItem person={item}/>}
                        keyExtractor={(item) => `${item.id}`}
                    />
                </SafeAreaView>
            </SafeAreaProvider>
        </>
    );
};
export default App;
see commit senorihl/AppDeclination#a6d7ca0 for current state
Further improvement
With some IDEs you can enable autocomplete by adding a current-variant.d.ts file at root.
// current-variant.d.ts
import {VariantDefinition} from "./variants/definition";
declare const definition: VariantDefinition;
export default definition;
// variants/definition.ts
import {Person} from "../src/interfaces/Person";
export interface VariantDefinition {
    source: string;
    fetchUsers: () => Promise<Person[]>;
}
Conclusion
You can now run any app variant using VARIANT environment variable when using react-native start.
VARIANT=two react-native start --reset-cache
# Default is VARIANT=one react-native start
# And then simply run
react-native ios
# or
react-native android