Creating a real world React Native app from scratch with Expo and Metricalp (2024)
We know that you do not like to read documentations, boilerplates that much. So, we prepared a real world example for you. In this post, we will create a real world React Native app from scratch with Expo and Metricalp.
Introduction
We are going to develop a ToDo application in React Native. The user can add, update and delete todos. But, while we are thinking this as like a real world application we will have some annoying ads in screen 😬 User can upgrade to PRO account to get rid of ads. So, in our app will have a second screen /upgrade-pro screen. In this screen we will have a button to upgrade pro. We will use Metricalp to track user behaviours to analyze our app. Trust me it is gonna be fun.
To make it like a real world application, we will use some awesome libraries in our app like Zustand store (to manage state) and MMKV store (to store data in local storage in blazing fast way). Here some final screenshots from our app:
Okay enough to talk, let's go and build it 🚀
Setup Project
First, we need to create a new React Native project with Expo:
npx create-expo-app@latest
In this example, we will use mostly iOS simulator. So, you need to have Xcode installed on your machine. If you do not have Xcode, you can install it from App Store. Also we will install watchman to work in simulator properly
brew update
brew install watchman
Then we will install expo dev client to work with our app in simulator:
npx expo install expo-dev-client
Please be sure about that you have latest version simulator installed. You can check it XCode -> Preferences -> Components
Let's run our app first time, inside of our new applications folder run:
npx expo run:ios
Install Libraries
Now, we will install some libraries to make our life easier. First, we will install react-native-mmkv store library from mrousavy. It works based on new architecture of React Native so you can write/read to local storage in sync and fast way.:
npx expo install react-native-mmkv
At this point we need to run also expo prebuild
npx expo prebuild
To track our users and their todos we are going to use randomly created uuids. We are going to install crypto library to generate these uuids from expo:
npx expo install expo-crypto
We will keep todos in Zustand store (a state management library), so we need to install it:
yarn add zustand
Finally, we will install Metricalp to track user behaviours in our app (while we are writing this article the latest version is 1.0.12 but always install the latest):
yarn add @metricalp/react-native
App Structure
Here the final app structure. We will tell and explain all used files below:
Let's start with some basics. Firstly create a folder with name 'utils' in the root of project.
Then create a file name zustand-store.ts in utils folder. This file will be used to manage our state in the app:
import { unstable_batchedUpdates } from 'react-native'; // or 'react-native'
import { create } from 'zustand';
import * as Crypto from 'expo-crypto';
import { storage } from './mmkv-store';
interface Todo {
id: string;
text: string;
}
interface ZustandStore {
todos: Todo[];
}
export const useZustandStore = create<ZustandStore>((set) => ({
todos: [],
}));
export const setTodos = (todos: Todo[]) => {
unstable_batchedUpdates(() => {
useZustandStore.setState({ todos });
});
storage.set('USER_TODOS_KEY', JSON.stringify(todos));
};
export const updateTodo = (id: string, text: string) => {
const todos = useZustandStore.getState().todos;
const updatedTodos = todos.map((todo) => {
if (todo.id === id) {
return { ...todo, text };
}
return todo;
});
setTodos(updatedTodos);
};
export const addTodo = (text: string) => {
const todos = useZustandStore.getState().todos;
const newTodo = { id: Crypto.randomUUID(), text };
setTodos([...todos, newTodo]);
};
export const deleteTodo = (id: string) => {
const todos = useZustandStore.getState().todos;
const updatedTodos = todos.filter((todo) => todo.id !== id);
setTodos(updatedTodos);
};
export const resetTodos = () => {
setTodos([]);
};
Basically we defined a Zustand store with todos. We have some functions to add, update, delete and reset todos. We are using mmkv-store to store todos in local storage. We are also updating this local storage copy on every update of the state. We are generating random uuids for every new todo.
Now lets generate a file named mmkv-store.ts in utils folder:
import { MMKV } from 'react-native-mmkv';
export const storage = new MMKV();
Now lets generate a file named constants.ts in utils folder. We will keep constants in here:
export const METR_UUID_KEY = 'metr.uuid';
export const USER_TODOS_KEY = 'user.todos';
export const METRICALP_TID = 'mam48'; // Replace with your Metricalp TID
export const SCREENS = {
INDEX: 'index',
UPGRADE_PRO: 'upgrade-pro',
};
Lastly let's generate a file named metricalp-events.ts in utils folder. We will keep our custom events for Metricalp in this file:
export const MetricalpCustomEvents = {
UPGRADE_PRO_NAVIGATOR: 'upgrade_pro_navigator',
TODO_DELETE_CLICK: 'todo_delte_click',
TODO_EDIT_CLICK: 'todo_edit_click',
ADD_TODO_CLICK: 'add_todo_click',
UPDATE_TODO_CLICK: 'update_todo_click',
UPGRADE_PRO_CLICK: 'upgrade_pro_click',
};
We will track edit, delete clicks of todos, add todo click, update todo click and upgrade pro click events in our app. Also, we want to track how user navigated to Upgrade screen so we have another event upgrade_pro_navigator which we will trigger it everytime a user navigates to upgrade screen. We will attach a prop to this event to keep how user navigated through to upgrade. Also, we will keep that user had how many todos when he decide to upgrade. Because this may be an important metric (marketing team said as that). Also, we want keep day time (morning, afternoon, evening or night) for every these events. We want to understand user behaviour, maybe they are more aim to use app in nights but have more aim to upgrade in morning? Again, marketing team asked for this data. In Metricalp we can keep any, we can have unlimited scenarios 🤝
Now, lets check app/_layout.tsx file which is our main layout file, we will initialize Metricalp and our app here:
import {
DarkTheme,
DefaultTheme,
ThemeProvider,
} from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack, usePathname } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect, useRef, useState } from 'react';
import 'react-native-reanimated';
import * as Crypto from 'expo-crypto';
import { useColorScheme } from '@/hooks/useColorScheme';
import { storage } from '@/utils/mmkv-store';
import {
METRICALP_TID,
METR_UUID_KEY,
USER_TODOS_KEY,
} from '@/utils/constants';
import { Metricalp } from '@metricalp/react-native';
import { setTodos } from '@/utils/zustand-store';
import { AppState, NativeEventSubscription, Platform } from 'react-native';
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const colorScheme = useColorScheme();
const [loaded] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
});
const [initializationCompleted, setInitializationCompleted] = useState(false);
const pathname = usePathname();
const lastPathnameRef = useRef<string>('/');
lastPathnameRef.current = pathname;
useEffect(() => {
let subscription: NativeEventSubscription;
let userUUID = storage.getString(METR_UUID_KEY);
if (!userUUID) {
userUUID = Crypto.randomUUID();
storage.set(METR_UUID_KEY, userUUID);
}
let todos = [];
const userTODOsFromMMKVStore = storage.getString(USER_TODOS_KEY);
if (userTODOsFromMMKVStore) {
todos = JSON.parse(userTODOsFromMMKVStore);
}
setTodos(todos);
const osWithVersion =
(Platform.OS === 'ios' ? 'iOS' : 'Android') + ' ' + Platform.Version;
Metricalp.init({
platform: Platform.OS,
app: '[email protected]',
language: 'English-US',
os: osWithVersion,
uuid: userUUID,
tid: METRICALP_TID,
});
setInitializationCompleted(true);
subscription = AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'background' || nextAppState === 'inactive') {
Metricalp.appLeaveEvent();
} else if (nextAppState === 'active') {
Metricalp.screenViewEvent(lastPathnameRef.current);
}
});
return () => {
subscription?.remove();
};
}, []);
useEffect(() => {
if (!initializationCompleted) {
return;
}
Metricalp.screenViewEvent(pathname);
}, [pathname, initializationCompleted]);
useEffect(() => {
if (loaded && initializationCompleted) {
SplashScreen.hideAsync();
}
}, [loaded, initializationCompleted]);
if (!loaded || !initializationCompleted) {
return null;
}
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
</ThemeProvider>
);
}
We are initializing our todos from local storage first (mmkv store). Then we are initializing Metricalp with some basic information like platform, app name, language, os, uuid and tid. We are also tracking app state changes to understand when app goes to background or foreground. We are also tracking screen views in our app, we are also using the hook usePathname to get current pathname of our app. We are hiding splash screen when all of these initialization completed.
We are generating a user UUID per user and keeping this in local storage. Metricalp uses this UUID to identify user and count unique views / events. You can also use user_id etc if you wish.
When app goes background or inactive we are generating appLeaveEvent events from Metricalp library. It helps to us to keep screen view durations etc.
We generated a custom hook useDayTime inside hooks folder (hooks/useDayTime.ts) to get day time of user:
export function useDayTime() {
const now = new Date();
const hours = now.getHours();
if (hours >= 6 && hours < 12) {
return 'morning';
}
if (hours >= 12 && hours < 18) {
return 'afternoon';
}
if (hours >= 18 && hours < 24) {
return 'evening';
}
return 'night';
}
Now let's edit (tabs)/_layout.tsx file to have upgrade tab menu and trigger upgrade_pro_navigator event when click to this tab menu:
import { Tabs } from 'expo-router';
import React from 'react';
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { SCREENS } from '@/utils/constants';
import { Metricalp } from '@metricalp/react-native';
import { MetricalpCustomEvents } from '@/utils/metricalp-events';
import { useZustandStore } from '@/utils/zustand-store';
import { useDayTime } from '@/hooks/useDayTime';
export default function TabLayout() {
const colorScheme = useColorScheme();
const dayTime = useDayTime();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
}}
screenListeners={{
tabPress: (e) => {
if (e.target?.startsWith(SCREENS.UPGRADE_PRO)) {
Metricalp.customEvent(MetricalpCustomEvents.UPGRADE_PRO_NAVIGATOR, {
day_time: dayTime,
placement: 'bottom-tab',
current_todos_count: useZustandStore.getState().todos.length,
});
}
},
}}
>
<Tabs.Screen
name={SCREENS.INDEX}
options={{
title: 'Home',
tabBarIcon: ({ color, focused }) => (
<TabBarIcon
name={focused ? 'home' : 'home-outline'}
color={color}
/>
),
}}
/>
<Tabs.Screen
name={SCREENS.UPGRADE_PRO}
options={{
title: 'Upgrade Pro',
tabBarIcon: ({ color, focused }) => (
<TabBarIcon
name={focused ? 'arrow-up-circle' : 'arrow-up-circle-outline'}
color={color}
/>
),
}}
/>
</Tabs>
);
}
See that we also attaching placement, current_todos_count and day_time props to our custom event. How easy and awesome right?
Now edit the (tabs)/index.tsx file to have our app main screen:
import { SCREENS } from '@/utils/constants';
import {
addTodo,
deleteTodo,
updateTodo,
useZustandStore,
} from '@/utils/zustand-store';
import { useRouter } from 'expo-router';
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
FlatList,
StyleSheet,
} from 'react-native';
import { Metricalp } from '@metricalp/react-native';
import { MetricalpCustomEvents } from '@/utils/metricalp-events';
import { useDayTime } from '@/hooks/useDayTime';
export default function HomeScreen() {
const todos = useZustandStore((state) => state.todos);
const [todoInput, setTodoInput] = useState('');
const [editId, setEditId] = useState('');
const dayTime = useDayTime();
const router = useRouter();
const handleAddTask = () => {
if (editId) {
Metricalp.customEvent(MetricalpCustomEvents.UPDATE_TODO_CLICK, {
day_time: dayTime,
});
updateTodo(editId, todoInput);
} else {
Metricalp.customEvent(MetricalpCustomEvents.ADD_TODO_CLICK, {
day_time: dayTime,
});
addTodo(todoInput);
}
setEditId('');
setTodoInput('');
};
const handleEditTask = (id: string) => {
const taskToEdit = todos.find((todo) => todo.id === id);
Metricalp.customEvent(MetricalpCustomEvents.TODO_EDIT_CLICK, {
day_time: dayTime,
});
setTodoInput(taskToEdit?.text || '');
setEditId(id);
};
const handleDeleteTask = (id: string) => {
Metricalp.customEvent(MetricalpCustomEvents.TODO_DELETE_CLICK, {
day_time: dayTime,
});
deleteTodo(id);
};
const renderItem = ({ item }: any) => (
<View style={styles.task}>
<Text style={styles.itemList}>{item.text}</Text>
<View style={styles.taskButtons}>
<TouchableOpacity onPress={() => handleEditTask(item.id)}>
<Text style={styles.editButton}>Edit</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleDeleteTask(item.id)}>
<Text style={styles.deleteButton}>Delete</Text>
</TouchableOpacity>
</View>
</View>
);
const renderAds = (adsPlace: 'top' | 'bottom') => {
return (
<>
<View style={styles.adsContainer}>
<Text style={styles.adsText}>Here some ads</Text>
</View>
<TouchableOpacity
style={styles.upgradeButton}
onPress={() => {
Metricalp.customEvent(MetricalpCustomEvents.UPGRADE_PRO_NAVIGATOR, {
placement: adsPlace,
day_time: dayTime,
current_todos_count: todos.length,
});
router.push(SCREENS.UPGRADE_PRO);
}}
>
<Text style={styles.upgradeButtonText}>
Upgrade to Pro now for non-ads experience
</Text>
</TouchableOpacity>
</>
);
};
return (
<View style={styles.container}>
<Text style={styles.title}>ToDo List App</Text>
{renderAds('top')}
<TextInput
style={styles.input}
placeholder="Enter Todo"
value={todoInput}
onChangeText={(text) => setTodoInput(text)}
/>
<TouchableOpacity style={styles.addButton} onPress={handleAddTask}>
<Text style={styles.addButtonText}>
{editId ? 'Update Todo' : 'Add Todo'}
</Text>
</TouchableOpacity>
<FlatList
data={todos}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
{renderAds('bottom')}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 40,
marginTop: 40,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
heading: {
fontSize: 30,
fontWeight: 'bold',
marginBottom: 7,
color: 'green',
},
adsContainer: {
borderWidth: 2,
borderColor: 'purple',
marginTop: 10,
padding: 5,
borderRadius: 10,
},
adsText: {
fontSize: 14,
fontWeight: 'bold',
color: 'purple',
},
input: {
borderWidth: 3,
borderColor: '#ccc',
padding: 10,
marginBottom: 10,
marginTop: 20,
borderRadius: 10,
fontSize: 18,
},
addButton: {
backgroundColor: 'green',
padding: 10,
borderRadius: 5,
marginBottom: 10,
},
addButtonText: {
color: 'white',
fontWeight: 'bold',
textAlign: 'center',
fontSize: 18,
},
task: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 15,
fontSize: 18,
},
itemList: {
fontSize: 19,
},
taskButtons: {
flexDirection: 'row',
},
editButton: {
marginRight: 10,
color: 'green',
fontWeight: 'bold',
fontSize: 18,
},
deleteButton: {
color: 'red',
fontWeight: 'bold',
fontSize: 18,
},
upgradeButton: {
backgroundColor: 'purple',
padding: 10,
marginVertical: 10,
borderRadius: 5,
marginBottom: 10,
},
upgradeButtonText: {
color: 'white',
},
});
So we have a standart ToDo app. We can add/edit/delete todos. We have annoying ads and upgrade buttons next to ads.
We are tracking every add/edit/delete clicks through Metricalp and we are sticking day_time prop to all of these events.
But we alsoa attaching placement and current_todos_count props to upgrade_pro_navigator event. Because they are important for us from marketing perspective
Finally lets create (tabs)/upgrade-pro.tsx file to have our upgrade pro screen:
import { useDayTime } from '@/hooks/useDayTime';
import { MetricalpCustomEvents } from '@/utils/metricalp-events';
import { useZustandStore } from '@/utils/zustand-store';
import { Metricalp } from '@metricalp/react-native';
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
export default function UpgradeProScreen() {
const dayTime = useDayTime();
return (
<View style={styles.container}>
<Text style={styles.title}>ToDo List App</Text>
<Text>
You will never see ads in the application when you upgrade to PRO.
</Text>
<TouchableOpacity
style={styles.upgradeButton}
onPress={() => {
Metricalp.customEvent(MetricalpCustomEvents.UPGRADE_PRO_CLICK, {
day_time: dayTime,
current_todos_count: useZustandStore.getState().todos.length,
});
alert('Upgraded to Pro now');
}}
>
<Text style={styles.upgradeButtonText}>Upgrade to Pro now</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 40,
marginTop: 40,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
upgradeButton: {
backgroundColor: 'purple',
padding: 10,
marginVertical: 20,
borderRadius: 5,
marginBottom: 10,
},
upgradeButtonText: {
color: 'white',
},
});
Here we are generating a custom event upgrade_pro_click when user clicks to upgrade button. We are also attaching day_time and current_todos_count props to this event. We are also showing an alert to user about he is upgraded.
current_todos_count can be important because we can measure that, a user is deciding to upgrade when he has how many todos. Maybe we can show a discount to users who has more than 10 todos? 🤔 See, how Metricalp helpful to taking marketing actions.
Okay we almost ready but we need to define this custom props to custom events in Metricalp dashboard. Let's go to Metricalp dashboard tracker settings to do it:
Here all events:
Let's attach custom props to these events:
Well, we added all our custom events. We attached 'day_time' as custom_prop1 alias to all events. We also attached placement as custom_prop2 and current_todos_count as custom_prop3 to upgrade_pro_navigator event. We also attached current_todos_count to upgrade_pro_click event as custom_prop2. We are ready to go 🚀
Let's check our dashboard:
Well, we have screen_view events with location, path, user language, operating system version, app version informations but additionally we have custom events for add/update/delete todo and upgrade actions. We have day time info, current todo count info and upgrade navigator placement info. Which that is awesome. We are collecting very valuable data as like a real A/B test. We can decide to show upgrade button to users for example who has more than 10 todos and we can decide show discounts in night time. We can remove bottom upgrade navigator button and keep only top one. I mean data will determine our marketing direction which like a real business
Well, all is easy with these powerful technologies like React Native, Expo and of course Metricalp 💜 Thanks if you read until here, do not hesitate to reach us if you have any 🤝