Home - kitiya/rn-tasty-recipes GitHub Wiki
$ expo init rn-testy-recipes
$ cd rn-testy-recipes
$ npm start
- CategoriesScreen.js
- CategoryRecipesScreen.js
- RecipeDetailScreen.js
- FavoritesScreen.js
- FiltersScreen.js
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;- Create a "fonts" folder inside the "assets" folder
- Copy
.ttffile into the fonts folderOpenSans-Bold.ttfOpenSans-Regular.ttf
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 allows loading fonts from the web and using them in React Native components.
$ 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
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.
-
{[fontFamily:string]: FontSource }-- A map of fontFamilys to FontSource
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' }} />;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.
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.
import { AppLoading } from 'expo';The following props are recommended, but optional for the sake of backwards compatibility.
-
startAsync(function) -- Afunctionthat returns aPromise, and thePromiseshould resolve when the app is done loading required data and assets. -
onError(function) -- IfstartAsyncthrows an error, it is caught and passed into the function provided toonError. -
onFinish(function) -- (Required if you providestartAsync). Called whenstartAsyncresolves or rejects. This should be used to set state and unmount theAppLoadingcomponent. -
autoHideSplash(boolean) -- Whether to hide the native splash screen as soon as you unmount theAppLoadingcomponent.
React Navigation is made up of some core utilities and those are then used by navigators to create the navigation structure in your app.
$ npm install react-navigation
- 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
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
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);-
createStackNavigatorprovides a way for your app to transition between screens where each new screen is placed on top of a stack. -
createStackNavigatoris 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).
npm install react-navigation-stack @react-native-community/masked-view
import { createStackNavigator } from 'react-navigation-stack';
createStackNavigator(RouteConfigs, StackNavigatorConfig);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,
});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;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.navigatewith 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" });- 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 withprops.navigation.popToTop().
class Category {
constructor(id, title, color) {
this.id = id;
this.title = title;
this.color = color;
}
}
export default Category;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"),
];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}
/>
);
};In this tutorial we added:
-
TouchableOpacitycomponent -
navigationOptions- headerTitle
- headerStyle
- headerTintColor
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
-
navigateandpushaccept 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 useprops.navigation.state.params. It isnullif no parameters are specified.
- Passing
categoryIdparam fromCategoriesScreentoCategoryRecipesScreen
<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>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>
);
};-
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.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,
};
};The createStackNavigator function has 2 arguments:
-
RouteConfigs(lesson#113) StackNavigatorConfig
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
-
- Setting up header styling using
defaultNavigationOptions
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.
$ expo install react-native-screens
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();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
},
});class Recipe {
constructor(
id,
categoryIds,
title,
/// more args
) {
this.id = id;
this.categoryIds = categoryIds;
this.title = title;
/// more code
}
}
export default Recipe;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
];- Filtered recipe in the selected category.
- Used
FlatListto 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>
);
};- Filtered
RECIPESdata to get recipes in a selected category - Rendering
RecipeIteminside theFlatListcomponent
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;- Create a new
RecipeItemcomponent to be rendered inside theCategoryRecipesScreen
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;- Passing
recipeIdas a parameter in the navigation
const renderRecipeItem = (itemData) => {
return (
<RecipeItem
// ... other args
onSelectRecipe={() => {
props.navigation.navigate({
routeName: "RecipeDetail",
params: {
recipeId: itemData.item.id,
},
});
}}
/>
);
};- 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
- Installing the
react-navigation-header-buttonsthat helps you set up some buttons in your header.
$ npm install react-navigation-header-buttons
- Also, install an Expo vector icon package
$ npm install @expo/vector-icons
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;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>
),
};
};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.
To use this navigator, ensure that you have react-navigation and its dependencies installed, then install react-navigation-tabs.
$ npm install react-navigation-tabs
import { createBottomTabNavigator } from 'react-navigation-tabs';
createBottomTabNavigator(RouteConfigs, TabNavigatorConfig);For a complete usage guide please visit Tab Navigation
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.
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);- Adding
navigationOptionsfor screens inside the navigator
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,
},
}
);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.
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.
npm install react-navigation-material-bottom-tabs
npm install react-native-paper
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
);The route configs object is a mapping from route name to a route config.
export default createMaterialBottomTabNavigator(
{
Album: { screen: Album },
Library: { screen: Library },
History: { screen: History },
Cart: { screen: Cart },
},
{
initialRouteName: 'Album',
activeColor: '#f0edf6',
inactiveColor: '#3e2465',
barStyle: { backgroundColor: '#694fad' },
}
);// 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,
},
});- Adding a Favorite Stack
- Created the
RecipeListcomponent that can be used inside bothCategoryRecipeScreenandFavoritesScreen
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>
);
};- Refactored code to use the new
RecipeListcomponent - We need to forward
pros.navigationproperty to theRecipeListcomponent because thenavigationprop 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}
/>
);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;Drawer navigation is a common pattern in navigation is to use a drawer from left (sometimes right) side for navigating between screens.
$ npm install react-navigation-drawer
import { createDrawerNavigator } from 'react-navigation-drawer';
createDrawerNavigator(RouteConfigs, DrawerNavigatorConfig);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.
- Created
FiltersNavigatorandcreateDrawerNavigator
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);- 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>
);
},
};
};Your can update DrawerNavigatorConfig which is the 2nd argument of the createDrawerNavigator function.
-
contentOptions for
DrawerItems
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,
}
);- Making sure that
fontFamilyis set up on:- Stack navigator
- Bottom tab navigator
- iOS: using
createBottomTabNavigator - android: using
createMaterialBottomTabNavigator(using Text component for styling text)
- iOS: using
- 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",
},
},
}
);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;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;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.
valueonValueChangetrackColorthumbColor
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>
);
};- Passing Data Between Component & Navigation Options (Header)
saveFiltersuseEffects()
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>
),
};
};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() 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.
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!
