React Native
Table of contents
Getting Started
// installsudo npm install expo-cli --global// start new project, can choose typescript templateexpo init project-namecd project-name// install typescript types laternpm install --save-dev typescript @types/jest @types/react @types/react-native @types/react-test-renderernpm start
Styling is done via JS. CSS-like property names are offered by RN.
Install Android Studio to emulate development on the pc.
- Download from: developer.android.com/st...dio
- Follow installation instructions
- Once installed, configure SDK Manager
- Install appropriate SDK platforms
- Install SDK Tools
- Android SDK Built-in Tools
- Android Emulator
- Android SDK Platform Tools
- Google Play Services
- Configure AVD Manager 5. Create Virtual Device 6. Select a device (preferable with Google Play Services) 7. Create the device 8. Can start the device from AVD manager
- To start expo in the emulator, run expo project and press 'a'
- To reload the project, on Android press 'rr' (Cmd + r on iOS)
Working With Core Components
<View> is like a div. It's used for layout and styling.
<View> uses Flexbox to organize its children. A <View> can hold as many child components as you need and it also works with any kind of child component - it can hold <Text> components, other <View>s (for nested containers/ layouts), <Image>s, custom components etc.
To make area scrollable, use <ScrollView> component. Also, if wrapping <ScrollView> with <View>, remember to set flex: 1 on Android to make it scrollable. To style <ScrollView>, use contentContainerStyle prop with flexGrow.
However, note that <ScrollView> will render all items in advance, which can affect performance with very long lists. For 'infinite' lists, use:
<FlatList data={inputData} renderItem={itemData => <Text>{itemData.item.value}</Text>}>//note, use keyExtractor for unique key properties.
<Touchable> and its child components allow to listen and respond to touch events.
Text can only be put inside a <Text> component. <Text> components can also be nested inside each other and will also inherit styling. Actually, you can also have nested <View>s inside of a <Text> but that comes with certain caveats/bugs you should watch out for.
Unlike <View>, <Text> does NOT use Flexbox for organizing its content (i.e. the text or nested components). Instead, text inside of <Text> automatically fills a line as you would expect it and wraps into a new line if the text is too long for the available <Text> width.
You can avoid wrapping by setting the numberOfLines prop, possibly combined with ellipsizeMode.
Text numberOfLines={1} ellipsizeMode="tail">This text will never wrap into a new line, instead it will be cut off like this if it is too lon...</Text>
Also important: When adding styles to a <Text> (no matter if that happens via inline styles or a StyleSheet object), the styles will actually be shared with any nested <Text> components.
This differs from the behavior of <View> (or actually any other component - <Text> is the exception): There, any styles are only applied to the component to which you add them. Styles are never shared with any child component!s
Styling
Can either use inline styling or as a stylesheet object (preferred).
All elements have display flex by default, with flex-direction 'column'.
// inline styling<View><View style={styles.screen}><TextInputplaceholder="Enter Goal"style={{borderBottomColor: "black",borderBottomWidth: 1,}}/></View></View>// stylesheet objectconst styles = StyleSheet.create({screen: {padding: 50,},});
Adding fonts
Add fonts into a dedicated folder (./assets/fonts).
import * as Font from "expo-font";import { AppLoading } from "expo";function 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 [dataLoaded, setDataLoaded] = useState(false);if (!dataLoaded) {return <AppLoadingstartAsync={fetchFonts}onFinish={() => setDataLoaded(true)}onError={(err) => console.log(err)}></AppLoading>;}}
Adding icons
import { Ionicons } from "@expo/vector-icons";<Ionicons name="md-remove" size={24} color="white"/>
Setting Global Styles
- Can create a custom component wrapper with the desired styled attached. (i.e. <BodyText>)
- Have a globally managed StyleSheet that you import into a component and apply where needed.
import React from "react";import { StyleSheet, Text, TextStyle } from "react-native";interface Props {style?: TextStyle;}const BodyText: React.FC<Props> = ({ style, children }) => {return <Text style={{ ...styles.text, ...style }}>{children}</Text>;};export default BodyText;const styles = StyleSheet.create({text: {fontFamily: "open-sans",},});
Styling Images
import { StyleSheet, View, Image } from "react-native";const GameOverScreen: React.FC<Props> = () => {return (<View style={styles.imageContainer}><Imagesource={require("../assets/success.png")}style={styles.image}resizeMode="contain"></Image></View>);};export default GameOverScreen;const styles = StyleSheet.create({imageContainer: {width: 300,height: 300,borderRadius: 150,borderWidth: 3,borderColor: "black",overflow: "hidden",marginVertical: 30,},image: {width: "100%",height: "100%",},});
Note, to load an image from the web, use source={{uri: 'link'}} and explicitly set width and height (RN is unable to determine the right size).
Responsive Interfaces
For flexile interfaces, use Dimensions API. Import it from react-native and use its object.
const styles = StyleSheet.create({button: {width: Dimensions.get('window').width / 4,// note, can also apply conditional styles in rendermarginTop: Dimensions.get("window").height > 600 ? 20 : 5,}});
Dimensions only runs on component render. Thus, if you change orientation, the layout won't update until refresh. If you have a property that needs to orientation layout changes, instead of managing it with Dimensions like above, manage it with state:
const [buttonWidth, setButtonWidth] = useState(Dimensions.get("window").width / 4);useEffect(() => {function updateLayout() {setButtonWidth(Dimensions.get("window").width / 4);}Dimensions.addEventListener("change", updateLayout);return () => {Dimensions.removeEventListener("change", updateLayout);};}, []);// <View style={{ width: buttonWidth }}>// <Button// title="Reset"// color={Colors.accent}// onPress={resetInputHandler}// ></Button>// </View>
Note, you can also render different layouts based on the Dimensions state.
Orientation
You can adjust "locked in" orientation in expo in app.json. Change it to either portrait, landscape, default (supports both). Then you will be able to rotate the screen in an emulator.
To prevent keyboard from covering content, use KeyboardAvoidingView inside ScrollView:
<ScrollView><KeyboardAvoidingViewbehavior="position"keyboardVerticalOffset={30}></KeyboardAvoidingView></ScrollView>
For layouts that don't depend on width and height but only depend on screen orientation, use ScreenOrientation API.
Platform
To style based on a platform, use Platform API.
const styles = StyleSheet.create({button: {backgroundColor: Platform.OS === 'android' ? 'green' : 'red',borderBottomWidth: Platform.OS === 'ios' ? 2 : 5,}});// OR<View style={{...styles.headerBaseStyle, ...Platform.select({ios: styles.headerIOS, android: styles.headerAndroid})}}></View>// ORlet ButtonComponent = TouchableOpacity;if (Platform.OS === 'android' && Platform.version >= 21) {ButtonComponent = TouchableNativeFeedback;}
Another way to handle different platforms is to use platform specific files by giving either .android or .ios file extensions: Button.android.jsx or Button.ios.jsx. Note, when importing these files, do not provide the extension. Expo will automatically use the right file for each platform.
Avoiding Notches And Native Buttons
Wrap your top (outer) content with SafeAreaView to prevent screen notches and other native ui elements from covering your app screen space.
Error Handling
Debugging Logic
To use remote debugger, open Expo menu overlay by pressing Ctrl + M on Android (Cmd + D on iOS) and click on 'Debug JS Remotely'.
A new tab will open in a browser that you can use for debugging. In the browser, go to Sources tab -> choose debuggerWorker.js -> In there, will see your project folder structure. Then, navigate to the file you need to debug and use the browser to set breakpoints. Remember to stop the debugger after you're done.
Debugging Layout
Open Expo menu overlay and click on 'Toggle Inspector'. This will enable a menu showing styling information about components.
Another option is to install React Native Debugger. Note, you will need to enable Remote Debugging for it to work.
Navigation
Install with this command:
npm install react-navigationexpo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
Also, install react-native-screens for better optimization. npm install react-native-screens:
// in top componentimport { enableScreens } from "react-native-screens";enableScreens();
Stack Navigation
npm install --save react-navigation-stack
Used to navigate back and forth between screens where screen is stacked on top.
// example: https://medium.com/@vdelacou/add-react-navigation-to-react-native-typescript-app-d1cf855b3fe7// ROOT NAVIGATORimport { createStackNavigator } from "react-navigation-stack";import CategoriesScreen from "../screens/CategoriesScreen";import CategoriesMealScreen from "../screens/CategoryMealsScreen";import MealDetailScreen from "../screens/MealDetailScreen";import { createAppContainer } from "react-navigation";export enum ROUTES {Categories = "Categories",CategoryMeals = "CategoryMeals",MealDetail = "MealDetail",}// set screensconst MealNavigator = createStackNavigator({[ROUTES.Categories]: {screen: CategoriesScreen,// note, specific options always win over default// navigationOptions: {// headerTitle: 'some title'// }},[ROUTES.CategoryMeals]: {screen: CategoriesMealScreen,},[ROUTES.MealDetail]: {screen: MealDetailScreen,},},{defaultNavigationOptions: {headerStyle: {backgroundColor:Platform.OS === "android" ? Colors.primaryColor : "",},headerTintColor: "white",},});// always wrap root/most important navigatorexport default createAppContainer(MealNavigator);// SCREEN COMPONENTimport React from "react";import { StyleSheet, Text, View, Button } from "react-native";import { CATEGORIES } from "../data/data";import Category from "../models/category";import { NavigationScreenComponent } from "react-navigation";import {NavigationStackScreenProps,NavigationStackOptions,} from "react-navigation-stack";type Props = {navigation: NavigationStackProp}type Params = {};type ScreenProps = {};const CategoriesMealScreen: NavigationScreenComponent<Params, ScreenProps> = (props: Props) => {const catId = props.navigation.getParam("categoryId");const selectedCategory: Category | undefined = CATEGORIES.find((cat) => cat.id === catId);return (<View style={styles.screen}><Text>The CategoriesMealScreens Screen!</Text><Text>{selectedCategory?.title}</Text><Buttontitle="Go To Details!"onPress={() =>// alternative syntax// props.navigation.navigate('SomeIdentifier');// can also use push, pop, replace, popToTop, goBackprops.navigation.navigate({ routeName: "MealDetail" })}></Button><Buttontitle="Go Back!"onPress={() => props.navigation.goBack()}></Button></View>);};CategoriesMealScreen.navigationOptions = (navigationData: NavigationStackScreenProps): NavigationStackOptions => {const catId = navigationData.navigation.getParam("categoryId", "None");const selectedCategory: Category | undefined = CATEGORIES.find((cat) => cat.id === catId);return {headerTitle: selectedCategory?.title,};};
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 option WILL be merged with the screens.
These 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()).
Navigation Buttons
- Install react-navigation-header-buttons package
- Use the button:
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";interface Props {title: string;}const CustomHeaderButton = (props: Props) => {return (<HeaderButton{...props}IconComponent={Ionicons}iconSize={23}color={Platform.OS === "android" ? "white" : Colors.primaryColor}></HeaderButton>);};export default CustomHeaderButton;// then use in a component:MealDetailScreen.navigationOptions = (navigationData: NavigationStackScreenProps): NavigationStackOptions => {const mealId = navigationData.navigation.getParam("mealId");const selectedMeal = MEALS.find((meal) => meal.id === mealId);return {headerTitle: selectedMeal?.title,headerRight: () => (<HeaderButtons HeaderButtonComponent={CustomHeaderButton}><Itemtitle="Favorite"iconName="ios-star"onPress={() => {}}></Item></HeaderButtons>),};};
Tabs Navigation
const MealsFavTabNavigator = createBottomTabNavigator({Meals: {screen: MealNavigator,navigationOptions: {tabBarIcon: (tabInfo) => {return (<Ioniconsname="ios-restaurant"size={25}color={tabInfo.tintColor}></Ionicons>);},},},Favorites: {screen: FavoritesScreen,navigationOptions: {tabBarLabel: "Favorites!",tabBarIcon: (tabInfo) => {return (<Ioniconsname="ios-star"size={25}color={tabInfo.tintColor}></Ionicons>);},},},});
Fot Android native looking tabs, use packages react-navigation-material-bottom-tabs and react-native-paper.
Drawer Navigation
const MainNavigator = createDrawerNavigator({MealsFavs: {screen: MealsFavTabNavigator,navigationOptions: {drawerLabel: 'Meals'}},Filters: {screen: FiltersNavigator,navigationOptions: {drawerLabel: 'Filters'},}}, {contentOptions: {activeTintColor: Colors.accentColor,labelStyle: {fontFamily: 'open-sans-bold'}}});
Passing Data Between Component and navigationOptions
const saveFilters = useCallback(() => {const appliedFilters = {glutenFree: isGlutenFree,lactoseFree: isLactoseFree,vegan: isVegan,isVegeterean: isVegeterean,};}, [isGlutenFree, isLactoseFree, isVegan, isVegeterean]);useEffect(() => {navigation.setParams({ save: saveFilters });}, [saveFilters]);type navOptions = NavigationStackScreenProps & NavigationDrawerScreenProps;FiltersScreen.navigationOptions = (navData: navOptions): NavigationStackOptions => {return {headerTitle: "Filter Meals",headerLeft: () => (<HeaderButtons HeaderButtonComponent={CustomHeaderButton}><Itemtitle="Menu"iconName="ios-menu"onPress={() => navData.navigation.toggleDrawer()}></Item></HeaderButtons>),headerRight: () => (<HeaderButtons HeaderButtonComponent={CustomHeaderButton}><Itemtitle="Save"iconName="ios-save"onPress={navData.navigation.getParam("save")}></Item></HeaderButtons>),};};
Store Management
Setting up Redux
npm install redux react-redux
Create store folder => reducers + actions folders
Create a reducer:
import { MEALS } from "../../../data/data"const initialState = {meals: MEALS,filteredMeals: MEALS,favoriteMeals: []}const mealsReducer = (state = initialState, action) {return state}
Create store
import { createStore, combineReducers } from "redux";import { mealsReducer } from "./store/reducers";import { Provider } from "react-redux";const rootReducer = combineReducers({meals: mealsReducer,});const store = createStore(rootReducer);return (<Provider store={store}><MealsNavigator></MealsNavigator></Provider>);