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-variant
to 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