One of the most painful stages of building an app (but I could be biased since I didn't yet get to the more advanced stages of launch and post launch :)). For me personally and there might be others out there that feel the same, this process is so dragged on for nothing. It should be easy to set it up and running and start developing REAL features and solving REAL problems. But no, my first mobile project took me probably over 50 hours just to set it up. It felt like I had run into every little thing that could have gone wrong and made every mistake that could have been made on the way. But I tried to keep my spirits up by telling myself that it will only be like that the first time. I'm learning everything I need to know so next project setups will go much smoother since I've gained all the required knowledge for doing it the right way. One of the reasons it took me so long besides the fact that I'm incompetent and illiterate is that I really wanted to make sure I prepare the best ground for an efficient and scalable development process. I wanted to invest time to build a strong foundation early on to keep tech debt low in the future and to have a smooth developer experience. I've learned from my first time jumping too early into code and without a clear vision. Quickly, the codebase became such a pain in the ahh to manage I had to start from scratch. I'm writing this piece of crap for my own sake since I don't trust myself to remember this information by the time I'm working on my next project. But also, to help other people save time and energy. I hope this guide can be something I wish had been for me when I first started. I should have mentioned this by now but the tech stack I'm working with and will be explaining is React Native with Expo, Supabase, and NativeWind for styling. Feel free to skip the parts that you find irrelevant. ### Why did I pick up this stack? Well, for React Native, I'm a software developer and am familiar with React. I wanted something cross platform as I intend to launch my app both on Android and iOS and Flutter's syntax was something to get used to. I would recommend choosing either React Native or Flutter simply because they are the most popular frameworks and they are well documented. I use Expo for their ecosystem of packages and development tools that make developing and testing mobile apps not so much of a pain in the ahh. I chose Supabase for my backend as an open-source alternative to Firebase. Never even tried Firebase, I just copied people who were solo building apps like I had wanted to. Its a cloud backend that offers everything you need in one package, including but not limited to a Postgres database, an authentication service, a storage service for storing large files such as images, and a client library that exposes a RESTful API for easy querying. Lastly, I use NativeWind for utility class styling, similar to TailwindCSS on the web. I tried using StyleSheet but find utility classes faster to develop with. > [!tip] One advice on picking a stack > Don't overthink it. Don't waste your time reading about each one's advantages and disadvantages. Both are popular for a reason. Just stick with one and master it. Once you master your stack you can build your app ideas like a factory ([or garden](https://herman.bearblog.dev/my-product-is-my-garden/)...). Now that we have that out of the way, let's dive in to setting up your project. This is how I would have done it if I had to start from scratch again. ### Step 0: Create a high-fidelity mockup of your idea First thing I did when starting my project was the worst thing I could have done -- I jumped right into code, ran ``` npx create-expo-app@latest . ``` and started building what I thought was an mvp of the blurry mess of an idea I held in my mind. Bad mistake. One of the most important aspects of building an app is having a good sense of what you're trying to build, at least for a first version. An uncleared vision will eventually lead you to start from scratch when you reach a point where either your codebase is too chaotic, your design implementation sucks so bad you're ashamed to look at it, or you don't feel aligned with your idea anymore and you have no idea what the f\*\*k are you doing. This is what happened to me at the start of my jouney. So I went back to Figma with the goal of designing a high fidelity mockup that is not fully complete, but gave me enough clarity and confidence to start strong again. And it was a smart decision, because it made my dx a whole lot smoother and consistent. It also forces you to really think things through on what issues your app is trying to solve, exactly how its going to solve them, which features should be emphasized, and how it differentiates from anything that is currently offered in the market. ![[Pasted image 20250705164400.png]] Once you have a mockup you're quite confident in, which also solidifies your conviction for the potential of your app, you can get yourself familiar with the stack you picked, either by reading the official docs, watching tutorials on YouTube, (both have taken me hours), or reading through this guide which strives to review the essentials. ### Step 1: Project initialization Enough beating around the bush, the actual technical setup begins now. Open the terminal of your favorite IDE and run the command: ``` npx create-expo-app@latest . ``` *\*\* You need to have node.js installed on your computer* This will initialize a default expo app in the current folder which comes with few preinstalled dependencies and an example app that uses Expo Router which we will learn to set up next. What I like to do is run ``` npm run reset-project ``` which will delete the example code, leaving us with a bare app. ### Step 2: Configuring Expo Router ==/app== is a unique folder used by the Expo Router library to configure file-based routes and screens in our app. When a file is added to the app directory, it automatically becomes a route in your app's navigation (except for the special \_layout files). All pages have a URL path that matches the file's location in the app directory. An overview of Expo Router core concepts: - **Stack navigator** - in most use cases, you will have this type of navigation in your app. Like the name implies, a stack navigator will organize the screens in a hierarchical manner, allowing to push (new screen is added on top of the stack) or pop (navigate back by removing the top screen) screens. - **\_layout.tsx** - this file is defined in the ==/app== directory and optionally in subdirectories and is rendered before any other route in that directory. - If its the root layout file (/app/\_layout.tsx), you put the initialization code (code you may have previously put inside a App.tsx file if you used react before), such as loading fonts or interacting with the splash screen. You then render the \<Stack/> component to create the stack navigator. - If its any other layout file, you have the option to customize the stack navigator per route group. For example, if we have a (tabs) route group we can create a layout file to render a custom navigation tab bar with the routes in that group. - **Route groups** - directories inside ==/app== that define groups of related screens. They are used for logical organization of the files and they don't count as part of the URL. To create a group you create a folder with a name surrounded by round brackets. For example I can create /app/(public)/ and /app/(protected)/ to create two groups of routes for unauthenticated and authenticated users. - **Tabs** - much like a stack navigator, you can implement a tab navigator in your layout file, and all the routes directly inside that directory will be treated as tabs. - **index.tsx** - this file is the first route in the stack, matching the / URL. This could be the /app/index.tsx or /app/(tabs)/index.tsx file. - **Non-navigation components** live outside of the ==/app== directory. As for where to place other components, we will later on get to define a project folder structure for a clean codebase. - **Square brackets** - files/directories with square brackets in their name are called dynamic routes, and the thing inside the square brackets is called a parameter. This parameter can be used when rendering the page. For example, /app/profile/\[userId]/ will match /profile/1 , /profile/2 , or another user id. You can access that parameter with the ==useLocalSearchParams== hook inside the page and use it to load the data for that specific user. - **Protected routes** - expo router allows you to set routes that should only be accessible to authenticated users. You configure the protected routes in the root layout file in this way: ```JS import { Stack } from 'expo-router'; import { useAuthState } from '@/utils/authState'; export default function RootLayout() { const { isLoggedIn } = useAuthState(); return ( <Stack> <Stack.Protected guard={isLoggedIn}> <Stack.Screen name="(tabs)" /> </Stack.Protected> <Stack.Protected guard={!isLoggedIn}> <Stack.Screen name="sign-in" /> <Stack.Screen name="create-account" /> </Stack.Protected> </Stack> ); } ``` You don't have to register the sign-in and create-account pages which don't require authenticating, it just lets the stack navigator know the order in which to look for routes. If you don't have an index.tsx file and you're unauthenticated, the navigator will choose to render sign-in. But if you define an index.tsx you can let the navigator do the job implicitly. For example in my code I have an index.tsx file that just exports SignIn because thats the first route I want to render for unauthenticated users: ```JS import SignIn from './SignIn'; export default SignIn; ``` \** for complete guide read the [expo router documentation](https://docs.expo.dev/router/) Now that we know the basics of Expo Router we can start implementing this in our project. I like to start first with mapping all the screens I want my app to have and initialize each screen with stub code so that I can test my navigation and see that its configured correctly. Example for stub code: ```JSX import React from 'react'; import { Text } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; const SignIn = () => {   return (     <SafeAreaView>       <Text>SignIn</Text>     </SafeAreaView>   ); }; export default SignIn; ``` I use a VSCode extension called Simple React Snippets that lets me use a shortcut for the stub react code. I type ==rnfe== which stands for React Native Functional Export and it generates a react component with the name of the component as the name of the file. >[!info] View and SafeAreaView >View is the equivalent of div in React Native. SafeAreaView is a View that takes into account the edges of the screen and makes sure the content is rendered within visible boundaries. As a rule of thumb I always use SafeAreaView to wrap each of my screen components and View for the other components. Example for what my routing would look like: ![[Pasted image 20250705172310.png]] This is a lot at first look, but let's break it down based on the concepts of Expo file-based router we just learned. **/(public)** - screens that are accessible to unauthenticated users. **/(public)/\_layout.tsx** - layout file that simply renders ```JS <Stack screenOptions={{ headerShown: false }} /> ``` to hide the navigation header. **/(protected)** - screens that are only accessible to authenticated users. **/(protected)/(tabs)** - screens that I want to be accessed through a bottom tab bar. - index.tsx - the home page - Profile.tsx - profile page - Search.tsx - search page **/(protected)/(tabs)/\_layout.tsx** - layout file that configures the tab bar. For example here I render a custom tab bar: ```JS import { Tabs } from 'expo-router'; export default function TabLayout() { return ( <Tabs tabBar={(props) => (<CustomTabBar {...props} />)}     >       <Tabs.Screen name='index' />       <Tabs.Screen name='Search' />       <Tabs.Screen name='Profile' /> </Tabs> ); } ``` **/(protected)/profile/\[userId].tsx** - a dynamic route that renders a profile screen for that specific user. ```JS import { useLocalSearchParams } from 'expo-router'; const Profile = () => {   const { userId } = useLocalSearchParams<{ userId: string }>();   return ( ...   ); }; export default Profile; ``` **/\_layout.tsx** - the root layout file. ```js import { AuthProvider, useAuth } from '@/context/AuthContext'; import { ThemeProvider } from '@/context/ThemeContext'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; import { useEffect } from 'react'; import { StatusBar, useColorScheme } from 'react-native'; import '../global.css'; SplashScreen.preventAutoHideAsync(); const queryClient = new QueryClient(); const RootLayout = () => (   <AuthProvider>     <ThemeProvider>       <QueryClientProvider client={queryClient}>         <RootNavigator />       </QueryClientProvider>     </ThemeProvider>   </AuthProvider> ); const RootNavigator = () => {   const { session, loading } = useAuth();   const colorScheme = useColorScheme();   useEffect(() => {     const init = async () => {       if (!loading) {         await SplashScreen.hideAsync();         StatusBar.setBarStyle(           colorScheme === 'dark' ? 'light-content' : 'dark-content'         );       }     };     init();   }, [loading, colorScheme]);   if (loading) {     return null;   }   return (     <Stack screenOptions={{ headerShown: false }}>       <Stack.Protected guard={!!session}>         <Stack.Screen name='(protected)' />       </Stack.Protected>     </Stack>   ); }; export default RootLayout; ``` This is how my root layout file looks like. It includes: - Wrapping the app with Auth and Theme context (will be shown later in this guide). - Wrapping the app with React Query's QueryClient provider. I use React Query in my app and suggest you do the same because it improves developer experience (this too, will be touched upon later). - I import global.css as part of NativeWind setup. - When I finish loading a user session or when color scheme has changed: - I hide the splash screen - I set the StatusBar (where you see the time and battery at the top) color depending on color scheme so it has contrast and stays visible. - I render the Stack navigator with protected routes that allows access when a session exists. Now finish implementing your own routes and come back when you're done with the skeleton for your app. I want to quickly go over an example for a project folder structure. - **/app** - the screens we just finished drafting - **/assets** - fonts, icons, svgs... - **/components** - all the components that are not screens, like buttons, lists, modals, headers and so on... - **/context** - where I define the contexts and providers for global state and settings like the app's theme and a user session. - **/hooks** - functions that I want to reuse in numerous components - **/lib** - where I place my supabase configuration and rest api calls. If we compare it to the MVC pattern, this is where I define my controllers, and I create a file for each model. (More on supabase setup and client library later) - **/types** - definitions of types. - **/utils** - pure utility functions. For example, image, time, and string manipulations. ### Step 3: Expo Go and Development Builds You can already run the app locally on your device with Expo Go. 1. Download the Expo Go app from the App Store or Google Play 2. Run in the terminal ``` npx expo start ``` 1. Scan the barcode with your phone and click the link It should open your new app inside of the Expo Go app so you can quickly test your progress so far. Expo Go is a really nice platform that enables you to quickly experiment with your app. No complicated setup required. But it comes with a few limitations. Some features don't work well or don't work at all with Expo Go. For example, some splash screen customization, OAuth, deep linking like in password reset emails that need to direct back to a page in your app, remote push notifications, etc.... Expo Go doesn't accurately simulate real deployed apps. Does it mean you shouldn't use it then? Well no, its a great tool to start with, when you're still in the early stages of development, but consider switching to development builds as soon as possible, since it can take time to setup what is needed to use development builds. If you're an iOS user, you first need to qualify to the Apple Developer Program. It can take a few days to get approved so whether you have iPhone or Android, research what it is you need to setup in order to use development builds and start the process early so you don't waste time waiting to get approved. **What are development builds?** A React Native app consists of two parts: the native app bundle, which is built with native build tools and submitted to the Google Play Store and Apple App Store along with metadata that is shipped with the app (app name, icon, spalsh screen...), then installed on a physical device. It includes native code (code written in platform-specific languages like Swift or Kotlin) that runs outside the JavaScript environment. And then there's the JavaScript bundle that runs inside it. In the case of Expo Go, the Expo Go app itself serves as the native app bundle, the Expo team built and submitted this app so you can get prototyping quickly. It is sandboxed with a number of native libraries but you cannot use a native library that is not already included or change any part of the native app. What you can do is update the app's JavaScript code and see the the changes in Expo Go. This is good for starters, but eventually you should switch to using development builds that allow you to create your own version of Expo Go, tailored with just the native code required to run the React Native app you're developing and closely simulates a real published app. This gives you full control over the native runtime, allowing you to install native libraries, modify project configurations, or even write your own native code. You can read more about Expo Go vs Development build in [this blog](https://expo.dev/blog/expo-go-vs-development-builds) or the [Expo Docs](https://docs.expo.dev/develop/development-builds/introduction/). Expo makes the process of bundling the native app easy, without needing the extra tooling like a whole frigging MacBook for iOS apps. You can build a [local development client](https://docs.expo.dev/guides/local-app-development/) on your machine but I prefer using EAS for building the app on the cloud. For EAS you need to sign up for an Expo account, then install the EAS CLI: `npm install -g eas-cli` **Migrate from Expo Go to a development build** 1. Install the `expo-dev-client` - this library includes the launcher UI with the dev menu. Expo Go has this built in, but here you need to install this manually. `npx expo install expo-dev-client` 2. Build your native app - Log in to the EAS CLI `eas login` - For iOS: `eas build --platform ios --profile development` - For Android: `eas build --platform android --profile development` Once you've built your native app, you won't need to build it again unless you add or update a library with native code, or change any native code or configuration, such as the app name. 3. Install the app, the EAS CLI will prompt you to install the app after the build is finished. You can also install previous builds through the expo.dev dashboard. 4. Start the JavaScript bundler `npx expo start` (this may already be running but if you close the process you only need to restart the JS bundler with this command) ### Step 4: Styles NativeWind is a library that allows you to use TailwindCSS in React Native, which is a tool that lets you add styles to components through predefined CSS classes (called utility classes) (e.g. text-center, pt-4) directly within jsx. In my exerience this improves dx because you don't have to navigate to a different file or even navigate to anywhere else within the same file and create javascript objects (StyleSheet.create), you just add styles immediately to the components you use. If you are not familiar with Tailwind I suggest you watch a quick YouTube video explaining the core concepts. #### NativeWind setup Install NativeWind with the commands: ``` npm i nativewind react-native-reanimated@~3.17.4 [email protected] npm i -D tailwindcss@^3.4.17 ``` Run `npx tailwindcss init` to create a `tailwind.config.js` file. Add the paths to all of your component files in your `tailwind.config.js` file: ```JS /** @type {import('tailwindcss').Config} */ module.exports = {   content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],   presets: [require('nativewind/preset')],   plugins: [], }; ``` Create a `global.css` file and add the Tailwind directives: ```CSS @tailwind base; @tailwind components; @tailwind utilities; ``` Import this file in your root layout file (`app/_layout.tsx`) (don't miss this step). Create a `babel.config.js` file and add the following presets: ```JS module.exports = function (api) {   api.cache(true);   return {     presets: [       ['babel-preset-expo', { jsxImportSource: 'nativewind' }],       'nativewind/babel',     ],   }; }; ``` Create or modify your `metro.config.js` in the root of your project: ```JS const { getDefaultConfig } = require("expo/metro-config"); const { withNativeWind } = require('nativewind/metro'); const config = getDefaultConfig(__dirname) module.exports = withNativeWind(config, { input: './global.css' }) ``` If you're using TypeScript, which is very recommended you do, create a `nativewind-env.d.ts` file and add: ```JS /// <reference types="nativewind/types" /> ``` Now you can add classnames to your components to test your NativeWind setup. ```JS import React from 'react'; import { View } from 'react-native'; const LoadingCircle = () => {   return (     <View className='w-6 h-6 border-2 border-zinc-300 border-t-zinc-600 rounded-full animate-spin' />   ); }; export default LoadingCircle; ``` #### Dark/light theme Tailwind has a `dark:` variant that lets you style components differently when dark mode is enabled. There are two ways for implementing dark mode: 1. System preference (automatic) 2. Manual selection (user toggle) Both ways use `colorScheme` from Nativewind. Here is a theme context I've implemented for my app that uses both system and manual with save to the local storage so that it remembers the theme preference: ```JS import AsyncStorage from '@react-native-async-storage/async-storage'; import { useColorScheme } from 'nativewind'; import {   createContext,   PropsWithChildren,   useContext,   useEffect,   useState, } from 'react'; type Theme = 'light' | 'dark' | 'system'; interface ThemeContextProps {   theme: Theme;   resolvedTheme: Partial<Theme>;   setTheme: (theme: Theme) => void; } const ThemeContext = createContext<ThemeContextProps | undefined>(undefined); export const ThemeProvider = ({ children }: PropsWithChildren) => {   const [theme, setTheme] = useState<Theme>('system');   const systemTheme = useColorScheme();   const resolvedTheme =     theme === 'system' ? systemTheme.colorScheme ?? 'light' : theme;   const setAppTheme = (newTheme: Theme) => {     setTheme(newTheme);     systemTheme.setColorScheme(newTheme);     AsyncStorage.setItem('user-theme', newTheme);   };   useEffect(() => {     (async () => {       const storedTheme = await AsyncStorage.getItem('user-theme');             if (         storedTheme === 'light' ||         storedTheme === 'dark' ||         storedTheme === 'system'       ) {         setTheme(storedTheme);         systemTheme.setColorScheme(storedTheme);       }     })();   }, []);   return (     <ThemeContext.Provider       value={{ theme, resolvedTheme, setTheme: setAppTheme }}     >       {children}     </ThemeContext.Provider>   ); }; export const useTheme = () => {   const context = useContext(ThemeContext); if (!context) throw new Error('useTheme must be used inside ThemeProvider');   return context; }; ``` Wrap your root layout with this `ThemeProvider` and you can access and set the theme with the `useTheme` hook. ```JS const { theme, resolvedTheme, setTheme } = useTheme(); ``` theme - 'light' | 'dark' | 'system' resolvedTheme - 'light' | 'dark' You can define a color palette for each theme in your `tailwind.config.js`: ```JS /** @type {import('tailwindcss').Config} */ module.exports = {   content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],   darkMode: 'class', // <-- add this   presets: [require('nativewind/preset')],   theme: { // <-- and this     extend: {       colors: {         light: {           background: '#f0f0f0',           tab_bg: '#fff',           text: '#000',           subtext: '#424242',           button_bg: '#000',           button_text: '#fff',           second_button_bg: '#E7E7E7',           second_button_text: '#000',         },         dark: {           background: '#121212',           tab_bg: '#fff',           text: '#e0e0e0',           subtext: '#ddd',           button_bg: '#fff',           button_text: '#000',           second_button_bg: '#222',           second_button_text: '#fff',         },       },     },   },   plugins: [], }; ``` Then use it in your jsx: ```JS import React, { PropsWithChildren } from 'react'; import { View } from 'react-native'; const EmptyBox = ({ children }: PropsWithChildren) => {   return (     <View className=' w-full p-12 rounded-2xl bg-light-background dark:bg-dark-background'>       {children}     </View>   ); }; export default EmptyBox; ``` Nativewind knows when to pick the styles from the `dark:` variant based on the `colorScheme`. There is another example in the official NativeWind docs that uses multiple themes, [you can check it out](https://www.nativewind.dev/docs/guides/themes). ### Step 5: Setting up Supabase Go to your Supabase dashboard and create a new project. Now to connect to Supabase from the React Native app create a `supabase.ts` file inside `/lib` : ```TS import { Database } from '@/types/supabase.types'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { createClient, processLock } from '@supabase/supabase-js'; import { AppState } from 'react-native'; import 'react-native-url-polyfill/auto'; const supabaseUrl = 'copy your project URL from the dashboard, this is the RESTful endpoint'; const supabaseAnonKey = 'copy the anon API key'; export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {   auth: {     storage: AsyncStorage,     autoRefreshToken: true,     persistSession: true,     detectSessionInUrl: false,     lock: processLock,   }, }); AppState.addEventListener('change', (state) => {   if (state === 'active') {     supabase.auth.startAutoRefresh();   } else {     supabase.auth.stopAutoRefresh();   } }); ``` I have a `types/` folder where I keep my Supabase types as well as other types in my app. You can quickly generate your supabase types file by 1. Installing the Supabase CLI - `npm i supabase@">=1.8.1" --save-dev` 2. Logging in - `npx supabase login` 3. Running the command - `npx supabase gen types typescript --project-id <Project ID> --schema public > types/database.types.ts` You can find your project ID in dashboard -> project settings. What I like to do first is setup authentication for my app, then define the database schema and Row Level Security (RLS) access rules. #### User Authentication - supabase user management - google and apple oauth - context #### Database - schema - views - Row Level Security #### Storage #### Rest API ### Step 6: DX tools #### React Query #### React Hook Form ### Bonus #### Troubleshooting common bugs #### Development tools - Figma - Cursor - Notion project manager - V0 for prototyping - Git #### Resources & further read - Articles - Youtube videos - Docs