How I Built My First React Native App for My First Freelance Client

I‘ve been a full-stack web developer for the past 5 years, working primarily with the MERN stack (MongoDB, Express, React, Node). I‘ve built dozens of web apps ranging from small side projects to large enterprise software.

Recently, I decided to expand my skillset by learning mobile development. I was drawn to the idea of being able to build apps that could be installed directly on a user‘s device and take advantage of native capabilities like push notifications, camera, and offline storage.

When a former colleague reached out about an opportunity to build a cross-platform mobile app for one of his freelance clients, I knew it was the perfect chance to put my new skills to the test. He had heard I was learning React Native and thought I might be a good fit for the project.

Why React Native?

React Native has quickly become one of the most popular frameworks for cross-platform mobile development since its release by Facebook in 2015. According to the 2020 StackOverflow Developer Survey, React Native is the 6th most popular framework overall.

StackOverflow Dev Survey 2020

Some of the key benefits of React Native are:

  • Allows you to use JavaScript and React to build native Android and iOS apps
  • Provides access to platform APIs for capabilities like touch input, location, camera
  • Renders using real native UI views, not webviews, for better look and feel
  • Supports over-the-air updates to deploy fixes without app store re-submission
  • Has a large ecosystem of open source components, tools, and learning resources

Of course, there are many other great options for cross-platform mobile development like Flutter, Xamarin, NativeScript, and PWAs. Ultimately, I chose React Native for this project because:

  1. I was already very comfortable with React from my web development experience
  2. I knew RN would allow me to work quickly by reusing my existing JS skills
  3. The client‘s app was well-suited to RN‘s strengths and didn‘t require advanced native integrations
  4. I was impressed by the maturity and momentum of the RN community and ecosystem

Building the App

Setup and Development Environment

The first step was setting up my local development environment for React Native. I followed the official React Native docs to install the required dependencies:

  • Node.js
  • Watchman
  • Xcode
  • Android Studio

I used the react-native-cli to initialize a new project:

npx react-native init MyApp

This bootstrapped a new React Native app with a basic file structure, entrypoint files, and configs for iOS and Android.

To run the app locally in development, I used:

npx react-native run-ios
npx react-native run-android

These commands built and bundled the JavaScript, and launched the app in an iOS Simulator or Android Virtual Device.

One of the biggest differences coming from web development was the debugger and error handling. Rather than using the browser devtools, RN uses its own in-app developer menu and standalone React Native Debugger.

I also had to get comfortable with the iOS and Android build tools and logs for diagnosing issues. When I encountered the infamous RN "red screen of death", I learned to systematically check for things like:

  • Incorrect imports or requires
  • Missing dependencies or native modules
  • Syntax errors or undefined variables
  • Malformed or incompatible JavaScript
  • iOS signing certificate issues

Inevitably, I spent a good chunk of time in the early days troubleshooting my local environment and learning how to efficiently debug. But I found the RN CLI and docs to be fairly helpful, and soon I had a functional dev setup.

Navigation

The first key architectural decision I made was how to handle in-app navigation. RN apps use a paradigm of tabs, stacks, and drawers to navigate between screens, rather than URLs like on the web.

I evaluated several popular navigation libraries and settled on React Navigation due to its:

  • Declarative, component-based API
  • Pre-built navigators for tabs, stacks, and drawers
  • Extensive documentation and examples
  • Large community and well-maintained codebase

I sketched out a basic navigation structure:

  • Authentication stack (SignIn, SignUp, ForgotPassword)
  • Main tabs (Feed, Explore, Notifications, Profile)
    • Each tab has its own stack for drilling into detail views
  • Settings drawer

Here‘s an example of how I set up the main tab navigator:

import { createBottomTabNavigator } from ‘@react-navigation/bottom-tabs‘;

const Tab = createBottomTabNavigator();

function MainTabNavigator() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Feed" component={FeedStackNavigator} />
      <Tab.Screen name="Explore" component={ExploreStackNavigator} />
      <Tab.Screen name="Notifications" component={NotificationsScreen} />
      <Tab.Screen name="Profile" component={ProfileStackNavigator} />
    </Tab.Navigator>
  );
}

And an example of a stack navigator for the Explore tab:

import { createStackNavigator } from ‘@react-navigation/stack‘;

const ExploreStack = createStackNavigator();

function ExploreStackNavigator() {
  return (
    <ExploreStack.Navigator>
      <ExploreStack.Screen name="Explore" component={ExploreScreen} />
      <ExploreStack.Screen name="Search" component={SearchScreen} />
      <ExploreStack.Screen name="Category" component={CategoryScreen} />
      <ExploreStack.Screen name="Item" component={ItemScreen} />
    </ExploreStack.Navigator>
  );
}

This structure provided a clear separation of concerns and made it easy to navigate between related screens. By nesting stacks within tabs, I could create intuitive drill-down flows without having to pass data between distant components.

State Management

The next key decision was how to handle application state. Coming from Redux on the web, I knew I wanted a single, centralized store to manage data from APIs and share it across the app.

I considered using Redux again for RN, but I decided to try MobX-State-Tree (MST) as an alternative. Some of the advantages of MST are:

  • Provides a mutable, OOP-like API for defining models with typed actions and views
  • Offers strong typing and runtime type checks out of the box
  • Supports patterns like props drilling, dependency injection, and composable models
  • Has first-class async action support with generators, flow, and coroutines
  • Integrates with the MobX reactive system for efficient, fine-grained updates

Here‘s an example MST model definition for a portion of my app state:

import { types, flow } from ‘mobx-state-tree‘

const Item = types.model({
  id: types.identifier,
  title: types.string,
  description: types.string,
  img: types.optional(types.string, ‘‘),
  createdAt: types.Date,
})

const Feed = types.model({
  items: types.array(Item),
  page: types.optional(types.number, 0),
  isFetching: types.optional(types.boolean, false),
}).actions(self => ({
  fetchItems: flow(function* fetchItems() {
    self.isFetching = true
    try {
      const response = yield fetch(`/api/feed?page=${self.page}`)
      self.items.push(...response)
      self.page += 1
    } catch (err) {
      console.error(‘Failed to fetch feed‘, err)
    }
    self.isFetching = false
  }),
})).views(self => ({
  get hasMore() {
    return self.page < MAX_PAGES
  }
}))

This model represents a paginated social media feed of item posts. It has typed properties with default values and a fetchItems async action using a generator. The hasMore computed view lets me conditionally render a loading spinner or "load more" button.

MST models participate in MobX‘s reactivity system. That means I can connect them to React components similar to Redux selectors:

import { observer } from ‘mobx-react-lite‘;
import { useStore } from ‘../stores/hooks‘;

const FeedList = observer(() => {
  const { feed } = useStore()

  useEffect(() => {
    if (feed.items.length === 0) {
      feed.fetchItems()
    }
  }, [])

  return <>
    {feed.items.map(item =>
      <FeedItem key={item.id} item={item} />
    )}
    {feed.isFetching 
      ? <Spinner />
      : feed.hasMore && <Button title="Load More" onPress={feed.fetchItems} />
    }
  </>
}

The observer HOC automatically tracks which parts of the MST model are accessed and re-renders only those components when the data changes. This helps keep the UI fast and avoids unnecessary re-renders.

I really enjoyed using MST and found it to be a great fit for the needs of this app. By defining a single source of truth for my core models, I was able to keep my component logic clean and maintainable.

UI Components and Style

With navigation and data management in place, the next step was building out the actual screens and UI components. React Native provides a core set of built-in components like View, Text, Image, TextInput, Button, etc.

For the overall look and feel, I decided to adopt a component library called react-native-paper. It offers a set of customizable, Material Design-inspired UI components like:

  • Cards
  • Buttons
  • Text fields
  • Dialogs
  • Lists and list items
  • Snackbars
  • Tooltips

By building with a UI library, I was able to create a polished, cohesive design without having to build everything from scratch. Of course, I still customized the components to match the client‘s branding.

Here‘s an example of a custom Card component I built for the Explore tab:

import { Card, Title, Paragraph } from ‘react-native-paper‘;

function ExploreCard({ item }) {
  const { title, description, img } = item
  return (
    <Card style={styles.card}>
      <Card.Cover 
        source={{ uri: img }} 
        resizeMode="cover"
        style={styles.cardCover} 
      />
      <Card.Content style={styles.cardContent}>
        <Title>{title}</Title>
        <Paragraph>{description}</Paragraph>
      </Card.Content>
    </Card>
  )
}

And the corresponding styles:

import { StyleSheet } from ‘react-native‘;

const styles = StyleSheet.create({
  card: {
    margin: 16,
    elevation: 4,
  },
  cardCover: {
    height: 120,
  },
  cardContent: {
    paddingTop: 8,
  },
});

One of the most challenging parts of styling RN components coming from the web was the lack of CSS. Instead, styles are defined as JavaScript objects and then passed to components via the style prop.

RN uses a subset of CSS properties with some differences (e.g. no em or rem units, everything is in pixels). The biggest change was using Flexbox for layout — while the core concepts are the same as CSS Flexbox, the default values and behavior are different.

It took some practice, but eventually I got the hang of creating responsive layouts and styling components. I found Flexbox to be really expressive and powerful once I understood how it worked in RN.

Conclusion

After about 6 weeks of development, I had a functional app that met all of the key requirements:

  • Cross-platform iOS and Android support
  • Authentication flow with sign in, sign up, and password reset
  • Dynamically rendered feed of content loaded from the API
  • Explore tab with search, categories, and item details
  • Notifications inbox with real-time updates
  • User profile and settings

I learned a huge amount in the process of building this app. Coming into it with zero prior mobile development experience, RN allowed me to get up and running incredibly quickly by leveraging my existing web skills.

Some of the key challenges I faced were:

  • Debugging: The RN dev menu and error stack traces took some getting used to. I had to learn new techniques for diagnosing issues in the native Android and iOS logs.

  • Performance: The performance problems I encountered were very different from the web. Optimizing things like list rendering, image loading, and navigation transitions required some RN-specific patterns and best practices.

  • Cross-platform differences: While RN‘s "learn once, write everywhere" philosophy abstracts many of the platform differences, I still ran into styling and behavioral inconsistencies between iOS and Android. Thoughtful platform checks and conditional logic were often necessary.

  • Native modules: For a few features like location tracking and camera access, I had to drop down to native code and bridge it back to JavaScript. This required learning some Objective-C/Swift and Java/Kotlin, which was challenging but also rewarding.

  • Upgrading versions: The RN ecosystem moves quickly, and sometimes upgrading to a new version caused unexpected breakages. I learned to carefully read the release notes and changelogs before installing updates.

Despite the challenges, I‘m really happy with how the app turned out. The client was thrilled to have a single app that worked across both mobile platforms and could be updated instantly via over-the-air CodePush updates.

Since completing this project, I‘ve continued to use React Native for new mobile apps. I believe it‘s a great fit for small-to-medium size apps that need to target both iOS and Android with limited resources. The developer experience is delightful, and the community support is fantastic.

Of course, RN isn‘t a silver bullet. There are some cases where you‘re better off building separate native apps, such as:

  • Apps with very complex UI interactions or performance-critical animations
  • Apps that need deep integrations with proprietary SDKs or hardware APIs
  • Apps targeting the latest platform-specific features and designs
  • Apps that require a specific native look and feel (Material Design or Human Interface)

Ultimately, the choice of technology should be driven by the specific needs of the app and the available resources. But if you‘re a web developer looking to expand your skillset or a startup looking to ship quickly on mobile, I highly recommend giving React Native a try.

Here are some of my favorite learning resources for getting started with RN:

If you have any questions or just want to chat about RN, feel free to reach out! You can find me on Twitter @yournamehere or on my website at yourwebsite.com.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *