React Native Basics

Based on the NetNinja Course

From the Net Ninja's React-Native Course on YouTube

Introduction

React native allows us to use React to develop native mobile applications. We're going through this course using the Expo CLI along with Typescript instead of just plain RN with JS

Init App

To create a new RN app we use the expo-cli using the following:

npm install -g expo-cli
expo init rn-net-ninja

And select the blank (TypeScript) as the application type. Then, cd into the created directory and run yarn start

cd rn-net-ninja
yarn start

You can then scan the barcode from the terminal via the Expo Go app which you can download from the app store on your test device

In the scaffolded code you will see the following structure:

rn-net-ninja
  |- .expo
  |- .expo-shared
  |- assets
  |- app.json
  |- App.tsx
  |- babel.config.js
  |- package.json
  |- tsconfig.json
  |- yarn.lock

Views and Styles

The App.tsx file contains a View with some Text and a StyleSheet

App.tsx

import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Hello, World!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

In RN we use the Text component to represent any plain text view as we would in typical JSX, additionally, the View can be likened to a div and the StyleSheet to a CSS File

In terms of the way RN uses styles it uses a CSS-like API but doesn't actually render to CSS but does magic in the background to get these to be like native styles

An important distinction when compared CSS is that RN styles aren't inherited by children elements, this means that if we want to apply a specific font to each component for example, we would actually have to apply this to every element individually

Text Inputs

RN Provides some other basic components, one of these are the TextInput component which we can use like so:

export default function App() {
  const [name, setName] = useState<string>("Bob");

  const handleTextChange = useCallback(setName, [name]);

  return (
    <View style={styles.inputWrapper}>
      <Text style={styles.inputLabel}>Enter Name</Text>
      <TextInput
        style={styles.input}
        value={name}
        onChangeText={handleTextChange}
      />
  );
}

Lists and ScrollView

ScrollView allows us to render a list of items in a scrollable area, this can be rendered as follows:

// people: { id: number; name: string; age: number;}[]
<ScrollView>
  {people.map((p, i) => (
    <View style={styles.person} key={i}>
      <Text>{p.name}</Text>
      <Text>{p.age}</Text>
    </View>
  ))}
</ScrollView>

FlatList

Additionally, there's another way to render a scrollable list of elements: using a FlatList which looks like so:

// people: { id: number; name: string; age: number;}[]
<FlatList
  data={people}
  renderItem={({ item }) => (
    <View style={styles.person}>
      <Text>{item.name}</Text>
      <Text>{item.age}</Text>
    </View>
  )}
  keyExtractor={({ id }) => id.toString()}
/>

Additionally, FlatList also supports a numColumns prop which lets us set the number of columns to use for the layout

The FlatList will only render the components as they move into view, and this is useful when we have a really long list of items that we want to render

Pressable

We can use the Pressable component to make any element able to handle touch events, for example:

In general, we can surround an element by Pressable and then handle the onPress event:

<Pressable onPress={() => onPersonSelected(item)}>
  <View style={styles.person}>
    <Text>{item.name}</Text>
    <Text>{item.age}</Text>
  </View>
</Pressable>

And then, somewhere higher up we can have onPersonSelected defined:

const onPersonSelected = useCallback<PersonSelectedCallback>(
  (p) => setName(p.name),
  []
);

We can set some app props by applying this to the FlatList components above.

Without too much detail, some of the types being referenced by our setup are:

interface Person {
  id: number;
  name: string;
  age: number;
}

type PersonSelectedCallback = (p: Person) => void;

And our person display component will then look like this:

const PeopleView: React.FC<{ onPersonSelected: PersonSelectedCallback }> =
  function ({ onPersonSelected }) {
    const people = [
      { name: "Jeff", age: 1 },
      { name: "Bob", age: 2 },
      // ...
    ].map((p, i) => ({ ...p, id: i }));

    return (
      <FlatList
        data={people}
        renderItem={({ item }) => (
          <Pressable onPress={() => onPersonSelected(item)}>
            <View style={styles.person}>
              <Text>{item.name}</Text>
              <Text>{item.age}</Text>
            </View>
          </Pressable>
        )}
        keyExtractor={({ id }) => id.toString()}
      />
    );
  };

Keyboard Dismissing

We'll notice that the way the app works at the moment, some basic interactions, such as the keyboard automatically being dismissed don't work. In order to do this we need to add a handler onto the component which causes the keyboard t be dismissed.

This can be done using a combination of a Pressable or TouchableWithoutFeedback and then Keyboard.dismiss() in the onPress handler

Flexbox

Just a short note here - Views in RN behave like flexboxes, so we can do normal flexboxy things for layout in our components.

Something that's commonly done is to use flex: 1 on components so that they fill the available space on a page. This is especially useful for limiting the size of a ScrollView or FlatList

Additionally, applying flex: 1 to the root component will cause the app to fill our entire screen

Fonts

To use custom fonts you can make use of the Expo CDK, here's the doc

expo install expo-font

And we can use a font from our app with the useFonts hook:

import * as React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useFonts } from 'expo-font';

export default function App() {
  const [loaded] = useFonts({
    koho: require('./assets/fonts/KoHo-Regular.ttf'),
  });
  
  if (!loaded) {
    return null;
  }

  return (
    <View style={styles.container}>
      <Text style=>KoHo</Text>
    </View>
  );
}

React Navigation

We're going to use the react-navigation library for building out the navigation and routing.

We can to install the required packages with:

yarn add @react-navigation/native

expo install does some additional version checks, etc.

expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view

Additionally, react-navigation supports a few different types of navigation. For our first case we're going to use a Stack navigation. So we need to install that with:

yarn add @react-navigation/stack

Stack Navigator

We can create a Stack Navigator using some of the constructs provided by react-navigation. These work by using createStackNavigator and composing the navigation within a NavigationContainer. Note that we also use the AppStackParamList to list the params required for each screen

routes/HomeNavigationContainer.tsx

import React from "react";
import {
  createStackNavigator,
} from "@react-navigation/stack";
import { NavigationContainer } from "@react-navigation/native";
import Home from "../screens/Home";
import ReviewDetails from "../screens/ReviewDetails";

export type AppStackParamList = {
  Home: undefined;
  ReviewDetails: undefined;
};

const Stack = createStackNavigator<AppStackParamList>();

export default function HomeNavigationContainer() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={Home} />
        <Stack.Screen name="ReviewDetails" component={ReviewDetails} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

We can then use this from the App component with:

App.tsx

// other imports
import HomeNavigationContainer from "./routes/HomeNavigationContainer";

export default function App() {
  // other stuff

  return <HomeNavigationContainer />;
}

In order to navigate we need to use the navigation prop that's passed to our component by react-navigation. In order to do this we need to configure our screen to use the AppStackParamList type with StackScreenProps

screens/Home.tsx

import { StackScreenProps } from "@react-navigation/stack";
import React from "react";
import { View, Text, Button } from "react-native";
import { AppStackParamList } from "../routes/HomeNavigationContainer";
import { globalStyles } from "../styles";

type HomeProps = StackScreenProps<AppStackParamList, "Home">;

const Home: React.FC<HomeProps> = function ({ navigation }) {
  const navigateToReviews = () => {
    navigation.navigate("ReviewDetails");
  };

  return (
    <View style={globalStyles.container}>
      <Text style={globalStyles.titleText}>Home Page</Text>
      <Button title="To Reviews" onPress={navigateToReviews} />
    </View>
  );
};

export default Home

Send data to screen

We can send some data to each screen by defining the data in our routing params:

export type AppStackParamList = {
  Home: undefined;
  ReviewDetails: { title: string; rating: number; body: string };
};

And then our Reviews screen will look like this:

type ReviewDetailsProps = StackScreenProps<AppStackParamList, "ReviewDetails">;

const ReviewDetails: React.FC<ReviewDetailsProps> = function ({ navigation, route }) {  
  const params = route.params
  
  // ... render stuff, etc.

And lastly, we can update the Home component to send the data that this screen requires using the navigator.navigate function:

const Home: React.FC<HomeProps> = function ({ navigation }) {
  const navigateToReviews = () => {
    navigation.navigate("ReviewDetails", { // screen props/data
      title: 'that racing movie',
      rating: 1,
      body: 'it was terrible'
    });
  };

  // ... render stuff, etc.

Drawer Navigation

Now, since we've got all our navigation working, we're going to add some complexity by including a drawer based navigation that wraps our overall application. To do this we will need to change when we're using our stack and creating the NavigationContainer

To do this, we'll first return just the stack from the HomeNavigationContainer and then create the container at the App component level. It also may be useful to rename the HomeNavigationContainer file to HomeStack.tsx

routes/HomeStack.tsx

import { StackScreenProps } from "@react-navigation/stack";
import React from "react";
import { View, Text, Button } from "react-native";
import { AppStackParamList } from "../routes/HomeStack";
import { globalStyles } from "../styles";

type HomeProps = StackScreenProps<AppStackParamList, "Home">;

const Home: React.FC<HomeProps> = function ({ navigation }) {
  const navigateToReviews = () => {
    navigation.navigate("ReviewDetails", {
      title: 'that racing movie',
      rating: 1,
      body: 'it was terrible'
    });
  };

  return (
    <View style={globalStyles.container}>
      <Text style={globalStyles.titleText}>Home Page</Text>
      <Button title="To Reviews" onPress={navigateToReviews} />
    </View>
  );
};

export default Home

Next, we need to add the About page content into a stack, like so:

routes/AboutStack.tsx

import React from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { NavigationContainer } from "@react-navigation/native";
import About from "../screens/About";

export type AppStackParamList = {
  About: undefined;
};

const Stack = createStackNavigator<AppStackParamList>();

export default function AboutStack() {
  return (
    <Stack.Navigator initialRouteName="About">
      <Stack.Screen name="About" component={About} />
    </Stack.Navigator>
  );
}

Then, we will include these components/stacks as the component that needs to be rendered. Since we're using a DrawerNavigator, we do this by first creating the navigator, then providind our screens as the routes:

routes/DrawerNavigator.tsx

import React from "react";
import {
  createDrawerNavigator,
  DrawerScreenProps,
} from "@react-navigation/drawer";
import HomeStack from "./HomeStack";
import AboutStack from "./AboutStack";
import { NavigationContainer } from "@react-navigation/native";

type DrawerParamList = {
  Home: undefined;
  About: undefined;
};

const Navigator = createDrawerNavigator<DrawerParamList>();

export default function DrawerNavigator() {
  return (
    <NavigationContainer>
      <Navigator.Navigator drawerType="front">
        <Navigator.Screen component={HomeStack} name="Home" />
        <Navigator.Screen component={AboutStack} name="About" />
      </Navigator.Navigator>
    </NavigationContainer>
  );
}

And lastly, we can use this from the App.tsx file like so:

App.tsx

import React from "react";
import { useFonts } from "expo-font";
import AppLoading from "expo-app-loading";
import { Stack } from "./routes/HomeStack";
import { NavigationContainer } from "@react-navigation/native";
import Home from "./screens/Home";
import ReviewDetails from "./screens/ReviewDetails";
import DrawerNavigator from "./routes/DrawerNavigator";

export default function App() {
  const [loaded] = useFonts({
    "koho-regular": require("./assets/fonts/KoHo-Regular.ttf"),
  });

  if (!loaded) {
    return <AppLoading />;
  }

  return <DrawerNavigator />;
}

Now that we've got the drawer working, it's a matter of finding a way to trigger it from our code, we can use a custom header component to do this. A special consideration here is that this component should have a definition for a composite navigation type in order to access and work with the drawer:

import { useNavigation } from "@react-navigation/core";
import {
  DrawerNavigationProp,
  DrawerScreenProps,
  useIsDrawerOpen,
} from "@react-navigation/drawer";
import { CompositeNavigationProp } from "@react-navigation/native";
import { StackNavigationProp, StackScreenProps } from "@react-navigation/stack";
import React from "react";
import { View, StyleSheet, Text, Button } from "react-native";
import { DrawerParamList } from "../routes/DrawerNavigator";

// composite navigation type to state that the screen 
// is within two types of navigators
type HeaderScreenNavigationProp = CompositeNavigationProp<
  StackNavigationProp<DrawerParamList, "Home">,
  DrawerNavigationProp<DrawerParamList>
>;

const Header = function () {
  const navigation = useNavigation<HeaderScreenNavigationProp>();

  function openDrawer() {
    navigation.openDrawer();
  }

  return (
    <View style={styles.header}>
      <Button title="Menu" onPress={openDrawer} />
      <Text>Header Text</Text>
    </View>
  );
};

export default Header;

We can then implement the Header in the HomeStack component in the options param for the stack:

routes/HomeStack.tsx

export default function HomeStack() {
  return (
    <Stack.Navigator initialRouteName="Home">
      <Stack.Screen
        name="Home"
        component={Home}
        options=
      />
      <Stack.Screen name="ReviewDetails" component={ReviewDetails} />
    </Stack.Navigator>
  );
}

And the exact same for About

routes/AboutStack.tsx

export default function AboutStack() {
  return (
    <Stack.Navigator initialRouteName="About">
      <Stack.Screen
        name="About"
        component={About}
        options=
      />
    </Stack.Navigator>
  );
}

Custom Title for Stack-Generate Header

Using the Stack Navigation, we are also provided with a title, this is still used by the ReviewDetails screen as it doesnt have any additional options. In addition to the options param as an object, we can also set it to a function which calculates the values from the data shared to the component. So we can use the title prop from our component to set this:

routes/HomeStack.tsx

<Stack.Screen name="ReviewDetails" component={ReviewDetails} 
  options={({route}) => ({
    title: route.params.title
  })}
/>

Images

To use images in React-Native, we make use of the Image component with the source prop with a require call

import { Image } from 'react-native'

//... do stuff

return <Image source={require('../assets/my-image.png')}>

Note that the require data must be a constant string, so we can't calculate this on the fly

If we have a set of images that we would like to dynamically select from, we can however still do something like this:

const images = {
  'car': require('../assets/car.png'),
  'bike': require('../assets/bike.png'),
  'truck': require('../assets/truck.png'),
  'boat': require('../assets/boat.png'),
}
const selectedImage = 'truck'

return <Image source={images[selectedImage]}>

Additionally, if we want to call an image from a URL, we can do this as we normally would, for example:

const imageUrl = getUserProfileUrl('my-user')

return <Image source={imageUrl}>

BackgroundImage

In RN we can't set image backgrounds using a normal background style prop, instead we need to use a BackgroundImage component from react-native, this works similar to the Image component but with the element we're adding the background to contained in it

<BackgroundImage source={require('../assets/truck.png')}>
  <View>
    {/* view content here */}
  </View>
</BackgroundImage>

Modals

RN comes with a handy modal component which allows us to render modals, Modals are controlled using a visible prop. A modal in use could look something like this:

screens/About.tsx

import React, { useCallback, useState } from "react";
import { StyleSheet, View, Text, Button, Modal } from "react-native";
import { globalStyles } from "../styles";

export default function About() {
  const [isOpen, setIsOpen] = useState<boolean>(false);

  const toggleModal = useCallback(() => {
    setIsOpen(!isOpen);
  }, [isOpen]);

  return (
    <View style={globalStyles.container}>
      <Text>About Page</Text>
      <Button title="Show Modal" onPress={toggleModal} />
      <Modal visible={isOpen} animationType="slide">
        <Text>Hello from the modal</Text>
        <Button title="Close Modal" onPress={toggleModal} />
      </Modal>
    </View>
  );
}