Home - kitiya/rn-tasty-recipes GitHub Wiki

Section 6: Navigation with React Navigation

107. Planning the App

planning the app

Creating a new app

$ expo init rn-testy-recipes
$ cd rn-testy-recipes
$ npm start

108. Adding Screens

Created 5 screens

  1. CategoriesScreen.js
  2. CategoryRecipesScreen.js
  3. RecipeDetailScreen.js
  4. FavoritesScreen.js
  5. FiltersScreen.js

Basic Template

import React from "react";
import { View, Text, StyleSheet } from "react-native";

const FavoritesScreen = (props) => {
  return (
    <View style={styles.screen}>
      <Text>THE FAVORITES SCREEN...</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
});

export default FavoritesScreen;

109. Adding Fonts

Adding "fonts" file to our project

  • Create a "fonts" folder inside the "assets" folder
  • Copy .ttf file into the fonts folder
    • OpenSans-Bold.ttf
    • OpenSans-Regular.ttf

Code example in the App.js file

import React, { useState } from "react";
import { Text, View } from "react-native";
import * as Font from "expo-font";
import { AppLoading } from "expo";

const fetchFonts = () => {
  return Font.loadAsync({
    "open-sans": require("./assets/fonts/OpenSans-Regular.ttf"),
    "open-sans-bold": require("./assets/fonts/OpenSans-Bold.ttf"),
  });
};

export default function App() {
  const [fontLoaded, setFontLoaded] = useState(false);

  if (!fontLoaded) {
    return (
      <AppLoading
        startAsync={fetchFonts}
        onFinish={() => setFontLoaded(true)}
      />
    );
  }

  return (
    <View>
      <Text>Open up App.js to start working on your app!!!</Text>
    </View>
  );
}

Expo Font

expo-font allows loading fonts from the web and using them in React Native components.

Installation

$ expo install expo-font

We use expo instead of npm to guarantee that we're installing the right version of the package for our Expo version

expo-font API

import * as Font from 'expo-font';

A highly efficient method for loading fonts from static or remote resources which can then be used with the platform's native text elements.

Arguments of loadAsync() method
  • {[fontFamily:string]: FontSource } -- A map of fontFamilys to FontSource
Example of loadAsync() method
await loadAsync({
  // Load a font `Montserrat` from a static resource
  Montserrat: require('./assets/fonts/Montserrat.ttf'),

  // Any string can be used as the fontFamily name. Here we use an object to provide more control
  'Montserrat-SemiBold': {
    uri: require('./assets/fonts/Montserrat-SemiBold.ttf'),
    fontDisplay: FontDisplay.FALLBACK,
  },
});

// Use the font with the fontFamily property
return <Text style={{ fontFamily: 'Montserrat' }} />;
Returns

The loadAsync() method returns a promise that resolves when the font has loaded. Often you may want to wrap the method in a try/catch/finally to ensure the app continues if the font fails to load.

isLoaded (another API)

isLoading (another API)

AppLoading Component

AppLoading is a React component that tells Expo to keep the app loading screen open if it is the first and only component rendered in your app. Unless autoHideSplash prop is set to false, the loading screen will disappear and your app will be visible when the component is removed.

This is incredibly useful to let you download and cache fonts, logos, icon images, and other assets that you want to be sure the user has on their device for an optimal experience before rendering and they start using the app.

AppLoading API

import { AppLoading } from 'expo';

AppLoading Props

The following props are recommended, but optional for the sake of backwards compatibility.

  • startAsync (function) -- A function that returns a Promise, and the Promise should resolve when the app is done loading required data and assets.
  • onError (function) -- If startAsync throws an error, it is caught and passed into the function provided to onError.
  • onFinish (function) -- (Required if you provide startAsync). Called when startAsync resolves or rejects. This should be used to set state and unmount the AppLoading component.
  • autoHideSplash (boolean) -- Whether to hide the native splash screen as soon as you unmount the AppLoading component.

111. Installing React Navigation & Adding Navigation to the App

React Navigation is made up of some core utilities and those are then used by navigators to create the navigation structure in your app.

Installation

$ npm install react-navigation

Installing dependencies into an Expo managed project

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

112. Installing Different Navigators

If you're using React Navigation v4 or higher, you need to install the different navigators which we'll use in this tutorial (StackNavigator, DrawerNavigator, TabsNavigator) separately with v3 and lower, it was part of react-navigation itself).

npm install --save react-navigation-stack
npm install --save react-navigation-tabs
npm install --save react-navigation-drawer

113. Creating a StackNavigator

Create "navigation/RecipesNavigator.js" file inside the project

// navigation/RecipesNavigator.js
import { createAppContainer } from "react-navigation";
import { createStackNavigator } from "react-navigation-stack";

import CategoriesScreen from "../screens/CategoriesScreen";
import CategoryRecipesScreen from "../screens/CategoryRecipesScreen";
import RecipeDetailScreen from "../screens/RecipeDetailScreen";

const RecipesNavigator = createStackNavigator({
  Categories: CategoriesScreen,
  CategoryRecipes: {
    screen: CategoryRecipesScreen,
  },
  RecipeDetail: RecipeDetailScreen,
});

export default createAppContainer(RecipesNavigator);

createStackNavigator

  • createStackNavigator provides a way for your app to transition between screens where each new screen is placed on top of a stack.
  • createStackNavigator is a function that takes a route configuration object and an options object and returns a navigation container which is a React component.
  • The keys in the route configuration object are the route names and the values are the configuration for that route. The only required property on the configuration is the screen (the component to use for the route).

Installation

npm install react-navigation-stack @react-native-community/masked-view

API Definition

import { createStackNavigator } from 'react-navigation-stack';

createStackNavigator(RouteConfigs, StackNavigatorConfig);

RouteConfigs

The route configs object is a mapping from route name to a route config, which tells the navigator what to present for that route.

createStackNavigator({
  // For each screen that you can navigate to, create a new entry like this:
  Profile: {
    // `ProfileScreen` is a React component that will be the main content of the screen.
    screen: ProfileScreen,
    // When `ProfileScreen` is loaded by the StackNavigator, it will be given a `navigation` prop.

    // ... More optional configurations
    }),
  },

  ...MyOtherRoutes,
});

createAppContainer

App Containers are responsible for managing your app state and linking your top-level navigator to the app environment.

import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';

const RootStack = createStackNavigator({ /* your routes here */ });
const AppContainer = createAppContainer(RootStack);

// Now AppContainer is the main component for React to render
export default AppContainer;

114. Navigating Between Screens

Project Code: Navigating to a new screen

import React from "react";
import { View, Text, Button, StyleSheet } from "react-native";

const CategoriesScreen = (props) => {
  console.log(props);
  return (
    <View style={styles.screen}>
      <Text>THE CATEGORIES SCREEN!</Text>
      <Button
        title="Go to Recipes"
        onPress={() => {
          props.navigation.navigate("CategoryRecipes");
          // props.navigation.navigate({ routeName: "CategoryRecipes" });
        }}
      />
    </View>
  );
};
  • props.navigation: the navigation prop is passed into every screen component (definition) in stack navigator.
  • navigate('CategoryRecipes'): we call the navigate function (on the navigation prop — naming is hard!) with the name of the route that we'd like to move the user to.
  • If we call props.navigation.navigate with a route name that we haven't defined on a stack navigator, nothing will happen. Said another way, we can only navigate to routes that have been defined on our stack navigator — we cannot navigate to an arbitrary component.
  • Note that both syntaxes below will work the same:
props.navigation.navigate("CategoryRecipes");
props.navigation.navigate({ routeName: "CategoryRecipes" });

117. Pushing, Poping, & Replacing

  • The navigation prop is available to all screen components (components defined as screens in route configuration and rendered by React Navigation as a route).

Navigate to a route multiple times | Link

  • props.navigation.navigate('RouteName') pushes a new route to the stack navigator if it's not already in the stack, otherwise, it jumps to that screen.
  • We can call props.navigation.push('RouteName') as many times as we like and it will continue pushing routes.
  • The header bar will automatically show a back button, but you can programmatically go back by calling props.navigation.goBack(). On Android, the hardware back button just works as expected.
  • props.navigation.replace('RouteName') replaces the current route with a new one. You can't go back because the stack is empty otherwise, this is the only screen. It can be useful for example on a login screen after a user did sign in.

Going back | Link

  • You can go back to an existing screen in the stack with props.navigation.navigate('RouteName'), and you can go back to the first screen in the stack with props.navigation.popToTop().

118. Outputting a Grid of Categories

Project Code

models/category.js

class Category {
  constructor(id, title, color) {
    this.id = id;
    this.title = title;
    this.color = color;
  }
}

export default Category;

data.dummy-data.js

import Category from "../models/category";

export const CATEGORIES = [
  new Category("c1", "Italian", "#f5428d"),
  new Category("c2", "Quick & Easy", "#f54242"),
  new Category("c3", "Hamburgers", "#f5a442"),
  new Category("c4", "German", "#f5d142"),
  new Category("c5", "Light & Lovely", "#368dff"),
  new Category("c6", "Exotic", "#41d95d"),
  new Category("c7", "Breakfast", "#9eecff"),
  new Category("c8", "Asian", "#b9ffb0"),
  new Category("c9", "French", "#ffc7ff"),
  new Category("c10", "Summer", "#47fced"),
];

screens/CategoriesScreen.js

import React from "react";
import { View, Text, FlatList, StyleSheet } from "react-native";
import { CATEGORIES } from "../data/dummy-data";

const renderGridItem = (itemData) => {
  return (
    <View style={styles.gridItem}>
      <Text>{itemData.item.title}</Text>
    </View>
  );
};

const CategoriesScreen = (props) => {
  return (
    <FlatList
      keyExtractor={(item, index) => item.id}
      data={CATEGORIES}
      renderItem={renderGridItem}
      numColumns={2}
    />
  );
};

119. Configuring the Header with Navigation Options

In this tutorial we added:

  • TouchableOpacity component
  • navigationOptions
    • headerTitle
    • headerStyle
    • headerTintColor

Project Code

CategoriesScreen.js

import React from "react";
import {
  View,
  Text,
  FlatList,
  TouchableOpacity,
  Platform,
  StyleSheet,
} from "react-native";
import { CATEGORIES } from "../data/dummy-data";
import Colors from "../constants/Colors";

const CategoriesScreen = (props) => {
  const renderGridItem = (itemData) => {
    return (
      <TouchableOpacity
        style={styles.gridItem}
        onPress={() => {
          props.navigation.navigate({ routeName: "CategoryRecipes" });
        }}
      >
        <View>
          <Text style={{ color: Colors.light }}>{itemData.item.title}</Text>
        </View>
      </TouchableOpacity>
    );
  };

  return (
    <FlatList
      keyExtractor={(item, index) => item.id}
      data={CATEGORIES}
      renderItem={renderGridItem}
      numColumns={2}
    />
  );
};

CategoriesScreen.navigationOptions = {
  headerTitle: "Recipe Categories",
  headerStyle: {
    backgroundColor: Platform.OS == "ios" ? Colors.light : Colors.primary,
  },
  headerTintColor: Platform.OS == "ios" ? Colors.primary : Colors.light,
};

120. Passing & Reading Params Upon Navigation | Link

  • navigate and push accept an optional second argument to let you pass parameters to the route you are navigating to. For example: props.navigation.navigate('RouteName', {paramName: 'value'}).
  • You can read the params through props.navigation.getParam
  • As an alternative to getParam, you may use props.navigation.state.params. It is null if no parameters are specified.

Project Code:

  • Passing categoryId param from CategoriesScreen to CategoryRecipesScreen

CategoriesScreen.js

<TouchableOpacity
  onPress={() => {
    // alternative syntax #1:
    props.navigation.navigate({
      routeName: "CategoryRecipes",
      params: {
        // passing value of "categoryId" param to the next screen
        categoryId: itemData.item.id,
      },
    });

    // alternative syntax #2:
    props.navigation.navigate("CategoryRecipes", {
      categoryId: itemData.item.id,
    });
  }}
>
</TouchableOpacity>

CategoryRecipesScreen.js

import { CATEGORIES } from "../data/dummy-data";

const CategoryRecipesScreen = (props) => {

  // "categoryId" has to be the same params name from the `CategoriesScreen`
  const catId = props.navigation.getParam("categoryId");

  const selectedCategory = CATEGORIES.find((item) => item.id === catId);
  return (
    <View style={styles.screen}>
      <Text>{selectedCategory.title}</Text>
    </View>
  );
};

121. Setting Dynamic Navigation Options

The navigationOptions can be:

  • object - if you have static hardcoded configuration values (see code example in the lesson #119).
  • function - if you need a dynamic configuration that depends on changing data.
    • When using a function, React will pass in some navigation data for you.
    • This function should return an object with your navigation options.

CategoryRecipesScreen.js

CategoryRecipesScreen.navigationOptions = (navigationData) => {

  // console.log(navigationData);
  const catId = navigationData.navigation.getParam("categoryId");

  const selectedCategory = CATEGORIES.find((item) => item.id === catId);

  return {
    // the header is dynamically rendered
    headerTitle: selectedCategory.title,
    headerStyle: {
      backgroundColor: Platform.OS == "ios" ? Colors.light : Colors.primary,
    },
    headerTintColor: Platform.OS == "ios" ? Colors.primary : Colors.light,
  };
};

122. Default NavigationOptions & Config

createStackNavigator

The createStackNavigator function has 2 arguments:

API Definition of the createStackNavigator

import { createStackNavigator } from 'react-navigation-stack';

createStackNavigator(RouteConfigs, StackNavigatorConfig);
  • Options for the router:
    • initialRouteName - Sets the default screen of the stack. Must match one of the keys in route configs.
    • initialRouteParams - The params for the initial route
    • initialRouteKey - Optional identifier of the initial route
    • navigationOptions - Navigation options for the navigator itself, to configure a parent navigator
    • defaultNavigationOptions - Default navigation options to use for screens
    • paths - A mapping of overrides for the paths set in the route configs
  • Visual option
    • mode - Defines the style for rendering and transitions:
    • headerMode - Specifies how the header should be rendered:
    • keyboardHandlingEnabled

defaultNavigationOptions

Project Code:

  • Setting up header styling using defaultNavigationOptions

navigation/RecipesNavigator.js

const RecipesNavigator = createStackNavigator(
  {
    // arg#1: RouteConfigs object
    Categories: CategoriesScreen,
    CategoryRecipes: {
      screen: CategoryRecipesScreen,
    },
    RecipeDetail: RecipeDetailScreen,
  },
  {
    // arg#2: StackNavigatorConfig object
    defaultNavigationOptions: {
      headerStyle: {
        backgroundColor: Platform.OS == "ios" ? Colors.light : Colors.primary,
      },
      headerTintColor: Platform.OS == "ios" ? Colors.primary : Colors.light,
    },

    // example of other StackNavigatorConfig
    // initialRouteName: "RecipeDetail",
    // mode: "modal",
  }
);

react-native-screens provides native primitives to represent screens instead of plain <View> components in order to better take advantage of operating system behavior and optimizations around screens.

Installation

$ expo install react-native-screens

API

To use react-native-screens with react-navigation, you will need to enable it before rendering any screens. Add the following code to your main application file (e.g. App.js):

import { enableScreens } from 'react-native-screens';
enableScreens();

123. Grid Styling & Some Refactoring

Project Code:

components/GategoryGridTile.js

import React from "react";
import {
  TouchableOpacity,
  TouchableNativeFeedback, // ripple effect for android
  View,
  Text,
  StyleSheet,
  Platform,
} from "react-native";
import Colors from "../constants/Colors";

const CategoryGridTile = (props) => {
  let TouchableComponent = TouchableOpacity;

  if (Platform.OS === "android" && Platform.Version >= 21) {
    TouchableComponent = TouchableNativeFeedback;
  }
  return (
    <View style={styles.gridItem}>
      <TouchableComponent style={{ flex: 1 }} onPress={props.onSelect}>
        <View style={{ ...styles.container, backgroundColor: props.color }}>
          <Text style={styles.title} numberOfLines={2}>
            {props.title}
          </Text>
        </View>
      </TouchableComponent>
    </View>
  );
};


const styles = StyleSheet.create({
  gridItem: {
    flex: 1,
    margin: 5,
    height: 150,
    borderRadius: 10,   // for android
    overflow: "hidden", // for android
  },
  container: {
    flex: 1,
    alignItems: "flex-end",
    justifyContent: "flex-end",
    padding: 15,
    borderRadius: 10,

    // shadowing only works on iOS
    shadowColor: Colors.dark,
    shadowOpacity: 0.26,
    shadowOffset: { width: 0, height: 2 },
    shadowRadius: 10,
    elevation: 3,
  },
  title: {
    fontFamily: "open-sans-bold",
    fontSize: 20,
    color: Colors.light,
    textAlign: "right", // for Android
  },
});

124. Adding Recipe Models & Data

Project Code:

models/recipe.js

class Recipe {
  constructor(
    id,
    categoryIds,
    title,
    /// more args
  ) {
    this.id = id;
    this.categoryIds = categoryIds;
    this.title = title;
    /// more code
  }
}

export default Recipe;

data/dummy-data.js

import Recipe from "../models/recipe";

export const RECIPE = [
  new Recipe(
    "m1",
    ["c1", "c2"],
    "Spaghetti with Tomato Sauce",
  ),

  new Recipe(
    "m2",
    ["c2"],
    "Toast Hawaii",
  ),

  // more Recipe
];

125. Loading recipes for categories

Project Code:

CategoryRecipeScreen.js

  • Filtered recipe in the selected category.
  • Used FlatList to display recipes.
import React from "react";
import { View, Text, FlatList, StyleSheet } from "react-native";
import { RECIPES } from "../data/dummy-data";

const CategoryRecipesScreen = (props) => {
  const catId = props.navigation.getParam("categoryId");

  const displayedRecipes = RECIPES.filter(
    (recipe) => recipe.categoryIds.indexOf(catId) >= 0
  );

  const renderRecipeItem = (itemData) => {
    return (
      <View>
        <Text>{itemData.item.title}</Text>
      </View>
    );
  };

  return (
    <View style={styles.screen}>
      <FlatList
        keyExtractor={(item, index) => item.id}
        data={displayedRecipes}
        renderItem={renderRecipeItem}
      />
    </View>
  );
};

126. Rendering a Recipes List

Project Code:

CategoryRecipeScreen.js

  • Filtered RECIPES data to get recipes in a selected category
  • Rendering RecipeItem inside the FlatList component
import React from "react";
import { View, FlatList, StyleSheet } from "react-native";
import { CATEGORIES, RECIPES } from "../data/dummy-data";
import RecipeItem from "../components/RecipeItem";

const CategoryRecipesScreen = (props) => {
  const catId = props.navigation.getParam("categoryId");

  const displayedRecipes = RECIPES.filter(
    (recipe) => recipe.categoryIds.indexOf(catId) >= 0
  );

  const renderRecipeItem = (itemData) => {
    return (
      <RecipeItem
        title={itemData.item.title}
        image={itemData.item.imageUrl}
        duration={itemData.item.duration}
        complexity={itemData.item.complexity}
        affordability={itemData.item.affordability}
        onSelectRecipe={() => {}}
      />
    );
  };

  return (
    <View style={styles.screen}>
      <FlatList
        keyExtractor={(item, index) => item.id}
        data={displayedRecipes}
        renderItem={renderRecipeItem}
        style={{ width: "100%" }}
      />
    </View>
  );
};

CategoryRecipesScreen.navigationOptions = (navigationData) => {
  const catId = navigationData.navigation.getParam("categoryId");
  const selectedCategory = CATEGORIES.find((item) => item.id === catId);

  return {
    headerTitle: selectedCategory.title,
  };
};

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
});

export default CategoryRecipesScreen;

component/RecipeItem.js

  • Create a new RecipeItem component to be rendered inside the CategoryRecipesScreen
import React from "react";
import {
  TouchableOpacity,
  View,
  Text,
  ImageBackground,
  StyleSheet,
} from "react-native";
import Colors from "../constants/Colors";

const RecipeItem = (props) => {
  return (
    <View style={styles.recipeItem}>
      <TouchableOpacity onPress={props.onSelectRecipe}>
        <View>
          <View style={{ ...styles.recipeRow, ...styles.recipeHeader }}>
            <ImageBackground
              source={{ uri: props.image }}
              style={styles.bgImage}
            >
              <View style={styles.titleContainer}>
                <Text numberOfLines={1} style={styles.title}>
                  {props.title}
                </Text>
              </View>
            </ImageBackground>
          </View>
          <View style={{ ...styles.recipeRow, ...styles.recipeDetail }}>
            <Text>{props.duration} m</Text>
            <Text>{props.complexity.toUpperCase()}</Text>
            <Text>{props.affordability.toUpperCase()}</Text>
          </View>
        </View>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  recipeItem: {
    height: 200,
    width: "100%",
    marginVertical: 5,
    backgroundColor: Colors.accent,
    borderRadius: 10,
    overflow: "hidden",
  },
  recipeRow: {
    flexDirection: "row",
  },
  recipeHeader: { height: "85%" },
  recipeDetail: {
    paddingHorizontal: 10,
    justifyContent: "space-between",
    alignItems: "center",
    height: "15%",
  },
  bgImage: {
    width: "100%",
    height: "100%",
    justifyContent: "flex-end",
  },
  titleContainer: {
    paddingVertical: 5,
    paddingHorizontal: 12,
    backgroundColor: "rgba(0,0,0,0.5)",
  },
  title: {
    fontFamily: "open-sans-bold",
    fontSize: 20,
    color: Colors.light,
    textAlign: "center",
  },
});
export default RecipeItem;

127. Passing data to the recipe detail screen

Project Code:

screens/CategoryRecipesScreen.js

  • Passing recipeId as a parameter in the navigation
const renderRecipeItem = (itemData) => {
    return (
      <RecipeItem
        // ... other args
        onSelectRecipe={() => {
          props.navigation.navigate({
            routeName: "RecipeDetail",
            params: {
              recipeId: itemData.item.id,
            },
          });
        }}
      />
    );
  };

screens/RecipeDetailScreen.js

  • Extracted selected selectedRecipeId
  • Searched for selected recipe object from the database.
  • Displayed title & image of the selectedRecipe
const RecipeDetailScreen = (props) => {
  const recipeId = props.navigation.getParam("recipeId");
  const selectedRecipe = RECIPES.find((recipe) => recipe.id === recipeId);

  return (
    <View style={styles.screen}>
      <Text style={styles.title} numberOfLines={1}>
        {selectedRecipe.title}
      </Text>
      <Image style={styles.image} source={{ uri: selectedRecipe.imageUrl }} />
    </View>
  );
};

RecipeDetailScreen.navigationOptions = (navigationData) => {
  const recipeId = navigationData.navigation.getParam("recipeId");
  const selectedRecipe = RECIPES.find((recipe) => recipe.id === recipeId);

  return {
    headerTitle: selectedRecipe.title,
  };
};

128. Adding Header Buttons

Installation

react-navigation-header-buttons

  • Installing the react-navigation-header-buttons that helps you set up some buttons in your header.
$ npm install react-navigation-header-buttons

@expo/vector-icons

$ npm install @expo/vector-icons

Project Code:

component/HeaderButton.js

import React from "react";
import { Platform } from "react-native";
import { HeaderButton } from "react-navigation-header-buttons";
import { Ionicons } from "@expo/vector-icons";
import Colors from "../constants/Colors";

const CustomHeaderButton = (props) => {
  return (
    <HeaderButton
      // forward all the props from a parent
      {...props}

      // expecting a vector icon 
      IconComponent={Ionicons}

      // icon styling
      iconSize={23}
      color={Platform.OS === "android" ? Colors.light : Colors.primary}
    />
  );
};

export default CustomHeaderButton;

RecipeDetailScreen.js

import { HeaderButtons, Item } from "react-navigation-header-buttons";
import HeaderButton from "../components/HeaderButton";

RecipeDetailScreen.navigationOptions = (navigationData) => {
  const recipeId = navigationData.navigation.getParam("recipeId");
  const selectedRecipe = RECIPES.find((recipe) => recipe.id === recipeId);

  return {
    headerTitle: selectedRecipe.title,
    headerRight: () => (

      // specified that the `<HeaderButton />` component will be used to rendered inside the `<HeaderButtons />`
      <HeaderButtons HeaderButtonComponent={HeaderButton}>
        <Item
          title="Favorite" // used as a fall back, in case the icon doesn't get loaded
          iconName="ios-star" // an icon name from the `@expo/vector-icons` library
          onPress={() => {
            console.log("Mark as my favorite!");
          }}
        />
        // there can be more than one `<Item />` (or icons) in the header
      </HeaderButtons>
    ),
  };
};

131. Adding Tabs-based Navigation

A simple tab bar on the bottom of the screen that lets you switch between different routes. Routes are lazily initialized -- their screen components are not mounted until they are first focused.

Installation

To use this navigator, ensure that you have react-navigation and its dependencies installed, then install react-navigation-tabs.

$ npm install react-navigation-tabs

API

import { createBottomTabNavigator } from 'react-navigation-tabs';

createBottomTabNavigator(RouteConfigs, TabNavigatorConfig);

For a complete usage guide please visit Tab Navigation

RouteConfigs

The route configs object is a mapping from route name to a route config, which tells the navigator what to present for that route, see example from stack navigator.

Project Code:

navigation/RecipesNavigator.js

import { createAppContainer } from "react-navigation";
import { createStackNavigator } from "react-navigation-stack";
import { createBottomTabNavigator } from "react-navigation-tabs";
import CategoriesScreen from "../screens/CategoriesScreen";
import CategoryRecipesScreen from "../screens/CategoryRecipesScreen";
import RecipeDetailScreen from "../screens/RecipeDetailScreen";
import FavoritesScreen from "../screens/FavoritesScreen";
import Colors from "../constants/Colors";

const RecipesNavigator = createStackNavigator(
  {
    Categories: CategoriesScreen,
    CategoryRecipes: {
      screen: CategoryRecipesScreen,
    },
    RecipeDetail: RecipeDetailScreen,
  },
  {
    defaultNavigationOptions: {
      headerStyle: {
        backgroundColor: Platform.OS == "ios" ? Colors.light : Colors.primary,
      },
      headerTintColor: Platform.OS == "ios" ? Colors.primary : Colors.light,
    },
  }
);

// nested navigator
const RecipesFavTabNavigator = createBottomTabNavigator({
  // RecipesNavigator from the createStackNavigator above
  Recipes: RecipesNavigator,
  Favorites: FavoritesScreen,
});

// root navigator
export default createAppContainer(RecipesFavTabNavigator);

132. Setting Icons and Configuring Tabs

Project Code:

navigation/RecipesNavigator.js

const RecipesFavTabNavigator = createBottomTabNavigator(
  {
    // RecipesNavigator from the createStackNavigator above
    Recipes: {
      screen: RecipesNavigator,
      navigationOptions: {
        tabBarIcon: (tabInfo) => {
          return (
            <Ionicons
              name="ios-restaurant"
              size={25}
              color={tabInfo.tintColor}
            />
          );
        },
      },
    },
    Favorites: {
      screen: FavoritesScreen,
      navigationOptions: {
        // tabBarLabel: "Favorites!",
        tabBarIcon: (tabInfo) => {
          return (
            <Ionicons name="ios-star" size={25} color={tabInfo.tintColor} />
          );
        },
      },
    },
  },
  {
    tabBarOptions: {
      activeTintColor: Colors.accent,
    },
  }
);

133. navigationOptions inside of a Navigator

When defining a navigator, you can also add navigationOptions to it:

const SomeNavigator = createStackNavigator({
    ScreenIdentifier: SomeScreen
}, {
    navigationOptions: {
        // You can set options here!
        // Please note: This is NOT defaultNavigationOptions!
    }
});

Don't mistake this for the defaultNavigationOptions which you could also set there (i.e. in the second argument you pass to createWhateverNavigator()).

The navigationOptions you set on the navigator will NOT be used in its screens! That's the difference to defaultNavigationOptions - those options WILL be merged with the screens.

So what's the use of navigationOptions in that place then?

The options become important once you use the navigator itself as a screen in some other navigator - for example if you use some stack navigator (created via createStackNavigator()) in a tab navigator (e.g. created via createBottomTabNavigator()).

In such a case, the navigationOptions configure the "nested navigator" (which is used as a screen) for that "parent navigator". For example, you can use navigationOptions on the nested navigator that's used in a tab navigator to configure the tab icons.

133. Adding MaterialBottomTabNavigator

createMaterialBottomTabNavigator is a material-design themed tab bar on the bottom of the screen that lets you switch between different routes. Routes are lazily initialized -- their screen components are not mounted until they are first focused.

Installation

npm install react-navigation-material-bottom-tabs 
npm install react-native-paper

API

This API also requires that you install react-native-vector-icons

$ expo install @expo/vector-icons
import { createMaterialBottomTabNavigator } from 'react-navigation-material-bottom-tabs';

createMaterialBottomTabNavigator(
  RouteConfigs,
  MaterialBottomTabNavigatorConfig
);

RouteConfigs

The route configs object is a mapping from route name to a route config.

Example

export default createMaterialBottomTabNavigator(
  {
    Album: { screen: Album },
    Library: { screen: Library },
    History: { screen: History },
    Cart: { screen: Cart },
  },
  {
    initialRouteName: 'Album',
    activeColor: '#f0edf6',
    inactiveColor: '#3e2465',
    barStyle: { backgroundColor: '#694fad' },
  }
);

Project Code:

navigation/RecipesNavigator.js

// from the previous tutorial
const tabScreenConfig = {
  Recipes: {
    screen: RecipesNavigator,
    navigationOptions: {
      tabBarIcon: (tabInfo) => {
        return (
          <Ionicons name="ios-restaurant" size={25} color={tabInfo.tintColor} />
        );
      },
      tabBarColor: Colors.primary,
    },
  },
  Favorites: {
    screen: FavoritesScreen,
    navigationOptions: {
      tabBarIcon: (tabInfo) => {
        return <Ionicons name="ios-star" size={25} color={tabInfo.tintColor} />;
      },
      tabBarColor: Colors.accent,
    },
  },
};

const RecipesFavTabNavigator =
  Platform.OS === "android"
      // using createMaterialBottomTabNavigator() for android
    ? createMaterialBottomTabNavigator(tabScreenConfig, {
        activeColor: Colors.light,
        shifting: true,
      })
    : createBottomTabNavigator(tabScreenConfig, {
        tabBarOptions: {
          activeTintColor: Colors.accent,
        },
      });
  1. Adding a Favorite Stack

Project Code:

component/RecipeList.js

  • Created the RecipeList component that can be used inside both CategoryRecipeScreen and FavoritesScreen
import React from "react";
import { View, FlatList, StyleSheet } from "react-native";
import RecipeItem from "./RecipeItem";

const RecipeList = (props) => {
  const renderRecipeItem = (itemData) => {
    return (
      <RecipeItem
        title={itemData.item.title}
        image={itemData.item.imageUrl}
        duration={itemData.item.duration}
        complexity={itemData.item.complexity}
        affordability={itemData.item.affordability}
        onSelectRecipe={() => {
          props.navigation.navigate({
            routeName: "RecipeDetail",
            params: {
              recipeId: itemData.item.id,
            },
          });
        }}
      />
    );
  };

  return (
    <View style={styles.recipeList}>
      <FlatList
        keyExtractor={(item, index) => item.id}
        data={props.recipeListData}
        renderItem={renderRecipeItem}
        style={{ width: "100%" }}
      />
    </View>
  );
};

CategoryRecipesScreen.js

  • Refactored code to use the new RecipeList component
  • We need to forward pros.navigation property to the RecipeList component because the navigation prop is only available in components that are loaded with the help of a navigator by React Native and only in that component, not in nested components.
  return (
    <RecipeList
      recipeListData={displayedRecipes}
      navigation={props.navigation}
    />
  );

FavoritesScreen.js

import React from "react";
import RecipeList from "../components/RecipeList";
import { RECIPES } from "../data/dummy-data";

const FavoritesScreen = (props) => {
  // dummy data
  const favRecipe = RECIPES.filter(
    (recipe) => recipe.id === "m1" || recipe.id === "m2"
  );
  return (
    <RecipeList recipeListData={favRecipe} navigation={props.navigation} />
  );
};

FavoritesScreen.navigationOptions = {
  headerTitle: "Favorite Recipes",
};

export default FavoritesScreen;

136. Adding a Menu Button & Drawer Navigation

Drawer navigation is a common pattern in navigation is to use a drawer from left (sometimes right) side for navigating between screens.

Installtion

$ npm install react-navigation-drawer
import { createDrawerNavigator } from 'react-navigation-drawer';

createDrawerNavigator(RouteConfigs, DrawerNavigatorConfig);

RouteConfigs

The route configs object is a mapping from route name to a route config, which tells the navigator what to present for that route, see example from createStackNavigator.

DrawerNavigatorConfig

Project Code:

navigation/RecipeNavigator.js

  • Created FiltersNavigator and createDrawerNavigator
const tabScreenConfig = {
  Recipes: RecipesNavigator,
  Filters: FiltersNavigator,
}

const RecipesFavTabNavigator =
  Platform.OS === "android"
    ? createMaterialBottomTabNavigator(tabScreenConfig, {
        activeColor: Colors.light,
        shifting: true,
      })
    : createBottomTabNavigator(tabScreenConfig, {
        tabBarOptions: {
          activeTintColor: Colors.accent,
        },
      });

// added new
const FiltersNavigator = createStackNavigator({ Filters: FiltersScreen });

// added new
const MainNavigator = createDrawerNavigator({
  RecipeFavs: RecipesFavTabNavigator,
  Filters: FiltersNavigator,
});

// root navigator
export default createAppContainer(MainNavigator);

FavoritesScreen.js

  • Added menu button on the headerLeft
import { HeaderButtons, Item } from "react-navigation-header-buttons";
import HeaderButton from "../components/HeaderButton";

FavoritesScreen.navigationOptions = (navData) => {
  return {
    headerTitle: "Favorite Recipes",

    // added new
    headerLeft: () => {
      return (
        <HeaderButtons HeaderButtonComponent={HeaderButton}>
          <Item
            title="Menu"
            iconName="ios-menu"
            onPress={() => {
              navData.navigation.toggleDrawer();
            }}
          />
        </HeaderButtons>
      );
    },
  };
};

137. Configuring the Drawer Navigator

Project Code:

navigation/RecipeNavigator.js

Your can update DrawerNavigatorConfig which is the 2nd argument of the createDrawerNavigator function.

const FiltersNavigator = createStackNavigator(
  { Filters: FiltersScreen },
  {
    // navigationOptions: { drawerLable: "Filters!!" },
    defaultNavigationOptions: defaultStackNavOptions,
  }
);

const MainNavigator = createDrawerNavigator(
  {
    RecipeFavs: {
      screen: RecipesFavTabNavigator,
      // added new
      navigationOptions: { drawerLabel: "Recipes" },
    },
    Filters: FiltersNavigator,
  },
  {
    /// DrawerNavigatorConfig (added new)
    contentOptions: {
      activeTintColor: Colors.accent,
      labelStyle: {
        fontFamily: "open-sans-bold",
      },
    },
    drawerBackgroundColor: Colors.accentGreen,
  }
);

138. More Navigation Config & Styling (fontFamily)

Project Code:

navigation/RecipeNavigator.js

  • Making sure that fontFamily is set up on:
    • Stack navigator
    • Bottom tab navigator
      • iOS: using createBottomTabNavigator
      • android: using createMaterialBottomTabNavigator (using Text component for styling text)
    • Drawer navigator
const defaultStackNavOptions = {
  // other code
  headerTitleStyle: { fontFamily: "open-sans-bold" },
  headerBackTitleStyle: {
    fontFamily: "open-sans",
  },
};


const tabScreenConfig = {
  Recipes: {
    screen: RecipesNavigator,
    navigationOptions: {
      // other config
      tabBarLabel:
        Platform.OS === "android" ? (
          // returning a Text component to add the `fontFamily` styling 
          // because the `createMaterialBottomTabNavigator` doesn't support formatting yet
          <Text style={{ fontFamily: "open-sans" }}>Recipes</Text>
        ) : (
          "Recipes"
        ),
    },
  },
  Favorites: {
    screen: FavNavigator,
    navigationOptions: {
      tabBarLabel:
        Platform.OS === "android" ? (
          <Text style={{ fontFamily: "open-sans" }}>Favorites</Text>
        ) : (
          "Favorites"
        ),
    },
  },
};

const MainNavigator = createDrawerNavigator(
  { /* route config */ },
  {
    contentOptions: {
      // other code...
      labelStyle: {
        fontFamily: "open-sans-bold",
      },
    },
  }
);

139. Adding a DefaultText Component

Project Code:

component/DefaultText.js

import React from "react";
import { Text, StyleSheet } from "react-native";

const DefaultText = (props) => {
  return <Text style={styles.text}>{props.children}</Text>;
};

const styles = StyleSheet.create({
  text: {
    fontFamily: "open-sans",
  },
});

export default DefaultText;

140. Adding the RecipeDetail Screen Content

Project Code:

screen/RecipeDetailScreen.js

import React from "react";
import { ScrollView, View, Text, Image, StyleSheet } from "react-native";
import { HeaderButtons, Item } from "react-navigation-header-buttons";

import HeaderButton from "../components/HeaderButton";
import DefaultText from "../components/DefaultText";
import { RECIPES } from "../data/dummy-data";
import Colors from "../constants/Colors";

const ListItem = (props) => {
  return (
    <View style={styles.listItem}>
      <DefaultText>{props.children}</DefaultText>
    </View>
  );
};

const RecipeDetailScreen = (props) => {
  const recipeId = props.navigation.getParam("recipeId");
  const selectedRecipe = RECIPES.find((recipe) => recipe.id === recipeId);

  return (
    <ScrollView>
      <View style={styles.screen}>
        <Image style={styles.image} source={{ uri: selectedRecipe.imageUrl }} />
      </View>
      <View style={styles.detailsContainer}>
        <DefaultText style={styles.details}>
          {selectedRecipe.duration} m
        </DefaultText>
        <DefaultText style={styles.details}>
          {selectedRecipe.complexity.toUpperCase()}
        </DefaultText>
        <DefaultText style={styles.details}>
          {selectedRecipe.affordability.toUpperCase()}
        </DefaultText>
      </View>
      <Text style={styles.title}>Ingredients</Text>
      {selectedRecipe.ingredients.map((ingredientItem, index) => {
        return <ListItem key={index}>{ingredientItem}</ListItem>;
      })}
      <Text style={styles.title}>Steps</Text>
      {selectedRecipe.steps.map((stepItem, index) => {
        return <ListItem key={index}>{stepItem}</ListItem>;
      })}
    </ScrollView>
  );
};

RecipeDetailScreen.navigationOptions = (navigationData) => {
  const recipeId = navigationData.navigation.getParam("recipeId");
  const selectedRecipe = RECIPES.find((recipe) => recipe.id === recipeId);

  return {
    headerTitle: selectedRecipe.title,
    headerRight: () => (
      <HeaderButtons HeaderButtonComponent={HeaderButton}>
        <Item
          title="Favorite"
          iconName="ios-star"
          onPress={() => {
            console.log("Fav");
          }}
        />
      </HeaderButtons>
    ),
  };
};

const styles = StyleSheet.create({
  image: {
    width: "100%",
    height: 200,
    borderRadius: 10,
  },
  detailsContainer: {
    flexDirection: "row",
    justifyContent: "space-around",
    padding: 10,
    backgroundColor: Colors.accent,
  },
  details: {
    color: Colors.light,
  },
  title: {
    marginVertical: 10,
    fontSize: 22,
    fontFamily: "open-sans-bold",
    textAlign: "center",
    color: Colors.primary,
  },
  listItem: {
    marginVertical: 5,
    marginHorizontal: 20,
    padding: 10,
    borderColor: Colors.borderLight,
    borderWidth: 1,
    borderRadius: 10,
  },
});

export default RecipeDetailScreen;

141. FiltersScreen Content (Using the Switch Component)

Switch Component

The Switch component renders a boolean input.

This is a controlled component that requires an onValueChange callback that updates the value prop in order for the component to reflect user actions. If the value prop is not updated, the component will continue to render the supplied value prop instead of the expected result of any user actions.

props

  • value
  • onValueChange
  • trackColor
  • thumbColor

Project Code:

screen/FiltersScreen.js

const FilterSwitch = (props) => {
  return (
    <View style={styles.filterContainer}>
      <Text>{props.label}</Text>
      <Switch
        value={props.value}
        onValueChange={props.onChange}
        trackColor={{ true: Colors.primary }}
        thumbColor={Platform.OS === "android" ? Colors.primary : ""}
      />
    </View>
  );
};

const FiltersScreen = (props) => {
  const [isGluetenFree, setIsGlutenFree] = useState(false);

  return (
    <View style={styles.screen}>
      <Text style={styles.title}>Available Filters / Restrictions</Text>
      <FilterSwitch
        label="Gluten-free"
        value={isGluetenFree}
        onChange={(newValue) => setIsGlutenFree(newValue)}
      />
    </View>
  );
};
  1. Passing Data Between Component & Navigation Options (Header)

Project Code:

screen/FiltersScreen.js

  • saveFilters
  • useEffects()
const FiltersScreen = (props) => {
  const { navigation } = props;

  const [isGluetenFree, setIsGlutenFree] = useState(false);
  const [isLactoseFree, setIsLactoseFree] = useState(false);
  const [isVegan, setIsVegan] = useState(false);
  const [isVegetarian, setIsVegetarian] = useState(false);

  // wrapping inside `useCallback()` to make sure that `saveFilters` only updates when our state changes.
  const saveFilters = useCallback(() => {

    // an object which gaters our filters
    const appliedFilters = {
      glutenFree: isGluetenFree,
      lactoseFree: isLactoseFree,
      vegan: isVegan,
      vegetarian: isVegetarian,
    };

    // we'd like to give access to this function which is a part of our component to our navigation options
    // so that we can trigger this function from inside the navigation options
    // for this, we can use `params`
    console.log(appliedFilters);
  }, [isGluetenFree, isLactoseFree, isVegan, isVegetarian]);


  // using `useEffect` to executing code whenever our state changes.
  // so we can forward this updated function which basically captures our current state to our navigation.
  useEffect(() => {

    // using `setParams` to update the `params` value for the currently loaded screen.
    // `setParams` causes the component to rebuild because its props (the props.navigation) change.
    navigation.setParams({ save: saveFilters });
  }, [saveFilters]);

  return (
    <View style={styles.screen}>
      <Text style={styles.title}>Available Filters / Restrictions</Text>
      <FilterSwitch
        label="Gluten-free"
        value={isGluetenFree}
        onChange={(newValue) => setIsGlutenFree(newValue)}
      />
      <FilterSwitch /* Lactose-free */  />
      <FilterSwitch /* Vegan */  />
      <FilterSwitch /* Vegetarian */ />
    </View>
  );
};

FiltersScreen.navigationOptions = (navData) => {
  return {
    headerTitle: "Filter Recipes",
    headerLeft: () => { /* Menu Button */ },
    headerRight: () => (
      <HeaderButtons HeaderButtonComponent={HeaderButton}>
        <Item
          title="Save"
          iconName="ios-save"

          // retrieve the `save` param that is set from the `setParam` function inside the `useEffect()`
          // this is a valid way of communicating between components and navigation options 
          // which typically needed when having action items in our navigation options.
          onPress={navData.navigation.getParam("save")}
        />
      </HeaderButtons>
    ),
  };
};

143. React Refresher - useEffect() & useCallback()

useEffect()

useEffect() is a so-called React Hook that allows you to handle side effects in your functional (!) React components.

You can use it to do anything that doesn't directly impact your UI/ JSX code (it might eventually impact it, for example, if you're fetching data from some server, but for the current render cycle, it will not).

useEffect() allows you to register a function which executes AFTER the current render cycle.

In the previous lecture, we use that to set new navigation params for this screen (= for this component).

useEffect() runs after every render cycle (i.e. whenever your functional component re-runs/ re-renders), unless you pass a second argument to useEffect(): An array of dependencies of the effect.

With such a dependency array provided, useEffect() will only re-run the function you passed as a first argument, whenever one of the dependencies changed.

useCallback()

useCallback() often is used in conjunction with useEffect() because it allows you to prevent the re-creation of a function. For this, it's important to understand that functions are just objects in JavaScript.

Therefore, if you have a function (A) inside of a function (B), the inner function (=A) will be recreated (i.e. a brand-new object is created) whenever the outer function (B) runs.

That means that in a functional component, any function you define inside of it is re-created whenever the component rebuilds.

Example:

const MyComponent = props => {
    const innerFunction = () => {
        // a function in a function!
        // this function object (stored in the 'innerFunction' constant) is constantly re-built
        // to be precise: It's re-built when MyComponent is re-built 
        // MyComponent is re-built whenever its 'props' or 'state' changes
    };
};

Normally, it's no problem, that innerFunction is re-created for every render cycle.

But it becomes a problem if innerFunction is a dependency of useEffect():

const MyComponent = props => {
    const innerFunction = () => {
        // do something!
    };
 
    useEffect(() => {
        innerFunction();
        // The effect calls innerFunction, hence it should declare it as a dependency
        // Otherwise, if something about innerFunction changes (e.g. the data it uses), the effect would run the outdated version of innerFunction
    }, [innerFunction]);
};

Why is this code problematic?

The effect re-runs whenever innerFunction changes. As stated, it is re-created whenever MyComponent re-builds.

Because functions are objects and objects are reference types, that means that the effect will re-run for every render cycle.

That might still not be a huge problem but it definitely is, if innerFunction does something that causes MyComponent to re-build (i.e. if it either does something that changes the props or the state).

Now, you would have an infinite loop!

useCallback() helps you prevent this.

By wrapping it around a function declaration and defining the dependencies of the function, it ensures that the function is only re-created if its dependencies changed.

Hence the function is NOT re-built on every render cycle anymore => You break out of the infinite loop!

⚠️ **GitHub.com Fallback** ⚠️