Installation Notes are based primarily on https://www.udemy.com/course/react-native-the-practical-guide/ Follow official instructions Install Homebrew brew install node brew install watchman \curl -sSL https://get.rvm.io | bash -s stable install ruby version manager reboot terminal rvm install 2.7.6 install specific ruby version with RVM , same version as here rvm use 2.7.6 or rvm --default use 2.7.6 switch ruby version XCode to be installed Choose latest version in Xcode - File - Settings - Location - Command line tools Instal an iOS Simulator in Xcode npm uninstall -g react-native-cli @react-native-community/cli uninstall perv versions npx react-native init appName Dev start cd appName npx react-native run-ios Simulator will be started and app build, takes around minute or more Port Choose different port if needed. default port is 8081 npx react-native start --port=8088 configure port Also change port in file ios/__App_Name__.xcodeproj/project.pbxproj Expo Despite on common environment setup above provided by react native team we can use Expo CLI It is easier https://reactnative.dev/docs/environment-setup?guide=quickstart npx create-expo-app react_native_heeros_learning_week create project with Expo cli cd react_native_heeros_learning_week npx expo start With phone Install for your phone https://apps.apple.com/us/app/expo-go/id982107779 With phone just redirect from the terminal to the link by barcode photo app to open the app directly on the phone With Android simulator Install https://developer.android.com/studio There you can create an emulator under projects --> more actions --> virtual device manager From the terminal just press A to open the app in android simulator With iOS simulator Install Xcode Choose latest version in Xcode - File - Settings - Location - Command line tools Instal an iOS Simulator in Xcode From the terminal just press I to open the app in iOS simulator Docs React Native docs Hello world // App.js import React from 'react' import {Text, View} from 'react-native' const HelloWorldApp = () => ( <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}> <Text style={{ fontSize: 20}}>Hello, world!</Text> </View> ) export default HelloWorldApp State import React, { useState } from 'react' import { View, Text, Button, StyleSheet } from 'react-native' const App = () => { const [count, setCount] = useState(0) return ( <View style={styles.container}> <Text>You clicked {count} times</Text> <Button onPress={() => setCount(count + 1)} title="Click me!" /> </View> ) } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center' }, }) export default App Prop import React from 'react' import { Text, View, StyleSheet } from 'react-native' const styles = StyleSheet.create({ center: { alignItems: 'center' }, }) const Greeting = ({name}) => ( <View style={styles.center}> <Text>Hello {name}!</Text> </View> ) const LotsOfGreetings = () => ( <View style={[styles.center, {top: 50}]}> <Greeting name="Rexxar" /> <Greeting name="Jaina" /> <Greeting name="Valeera" /> </View> ) export default LotsOfGreetings View Should have other components inside, can not have just a pure text It is kind of div element where we can group other things Text Text component can contain another Text But not View for ex. <Text style={styles.summaryText}> Your phone needed <Text style={styles.highlight}>{roundsNumber}</Text>{' '} rounds to guess the number{' '} <Text style={styles.highlight}>{userNumber}</Text>. </Text> ... const styles = StyleSheet.create({ summaryText: { fontFamily: 'open-sans', fontSize: 24, textAlign: 'center', marginBottom: 24, }, highlight: { fontFamily: 'open-sans-bold', color: Colors.primary500, }, }); TextInput onChangeText prop that takes a function to be called every time the text changed onSubmitEditing prop that takes a function to be called when the text is submitted import React, { useState } from 'react' import { Text, TextInput, View } from 'react-native' const PizzaTranslator = () => { const [text, setText] = useState('') return ( <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}> <TextInput style={{height: 40}} placeholder="Type here" onChangeText={newText => setText(newText)} defaultValue={text} value={text} /> <Text style={{padding: 10}}> Number of chars: {text.length} </Text> </View> ) } export default PizzaTranslator Custom TextInput import { StyleSheet, Text, TextInput, View } from 'react-native'; import { GlobalStyles } from '../../constants/styles'; function Input({ label, invalid, style, textInputConfig }) { const inputStyles = [styles.input]; if (textInputConfig && textInputConfig.multiline) { inputStyles.push(styles.inputMultiline) } if (invalid) { inputStyles.push(styles.invalidInput); } return ( <View style={[styles.inputContainer, style]}> <Text style={[styles.label, invalid && styles.invalidLabel]}>{label}</Text> <TextInput style={inputStyles} {...textInputConfig} /> </View> ); } export default Input; const styles = StyleSheet.create({ inputContainer: { marginHorizontal: 4, marginVertical: 8 }, label: { fontSize: 12, color: GlobalStyles.colors.primary100, marginBottom: 4, }, input: { backgroundColor: GlobalStyles.colors.primary100, color: GlobalStyles.colors.primary700, padding: 6, borderRadius: 6, fontSize: 18, }, inputMultiline: { minHeight: 100, textAlignVertical: 'top' }, invalidLabel: { color: GlobalStyles.colors.error500 }, invalidInput: { backgroundColor: GlobalStyles.colors.error50 } }); import { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import Input from './Input'; import Button from '../UI/Button'; import { getFormattedDate } from '../../util/date'; import { GlobalStyles } from '../../constants/styles'; function ExpenseForm({ submitButtonLabel, onCancel, onSubmit, defaultValues }) { const [inputs, setInputs] = useState({ amount: { value: defaultValues ? defaultValues.amount.toString() : '', isValid: true, }, date: { value: defaultValues ? getFormattedDate(defaultValues.date) : '', isValid: true, }, description: { value: defaultValues ? defaultValues.description : '', isValid: true, }, }); function inputChangedHandler(inputIdentifier, enteredValue) { setInputs((curInputs) => { return { ...curInputs, [inputIdentifier]: { value: enteredValue, isValid: true }, }; }); } function submitHandler() { const expenseData = { amount: +inputs.amount.value, date: new Date(inputs.date.value), description: inputs.description.value, }; const amountIsValid = !isNaN(expenseData.amount) && expenseData.amount > 0; const dateIsValid = expenseData.date.toString() !== 'Invalid Date'; const descriptionIsValid = expenseData.description.trim().length > 0; if (!amountIsValid || !dateIsValid || !descriptionIsValid) { // Alert.alert('Invalid input', 'Please check your input values'); setInputs((curInputs) => { return { amount: { value: curInputs.amount.value, isValid: amountIsValid }, date: { value: curInputs.date.value, isValid: dateIsValid }, description: { value: curInputs.description.value, isValid: descriptionIsValid, }, }; }); return; } onSubmit(expenseData); } const formIsInvalid = !inputs.amount.isValid || !inputs.date.isValid || !inputs.description.isValid; return ( <View style={styles.form}> <Text style={styles.title}>Your Expense</Text> <View style={styles.inputsRow}> <Input style={styles.rowInput} label="Amount" invalid={!inputs.amount.isValid} textInputConfig={{ keyboardType: 'decimal-pad', onChangeText: inputChangedHandler.bind(this, 'amount'), value: inputs.amount.value, }} /> <Input style={styles.rowInput} label="Date" invalid={!inputs.date.isValid} textInputConfig={{ placeholder: 'YYYY-MM-DD', maxLength: 10, onChangeText: inputChangedHandler.bind(this, 'date'), value: inputs.date.value, }} /> </View> <Input label="Description" invalid={!inputs.description.isValid} textInputConfig={{ multiline: true, // autoCapitalize: 'none' // autoCorrect: false // default is true onChangeText: inputChangedHandler.bind(this, 'description'), value: inputs.description.value, }} /> {formIsInvalid && ( <Text style={styles.errorText}> Invalid input values - please check your entered data! </Text> )} <View style={styles.buttons}> <Button style={styles.button} mode="flat" onPress={onCancel}> Cancel </Button> <Button style={styles.button} onPress={submitHandler}> {submitButtonLabel} </Button> </View> </View> ); } export default ExpenseForm; const styles = StyleSheet.create({ form: { marginTop: 40, }, title: { fontSize: 24, fontWeight: 'bold', color: 'white', marginVertical: 24, textAlign: 'center', }, inputsRow: { flexDirection: 'row', justifyContent: 'space-between', }, rowInput: { flex: 1, }, errorText: { textAlign: 'center', color: GlobalStyles.colors.error500, margin: 8, }, buttons: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', }, button: { minWidth: 120, marginHorizontal: 8, }, }); Numbers keyboard const [enteredNumber, setEnteredNumber] = useState(''); function numberInputHandler(val) { setEnteredNumber(val) } return ( <TextInput style={styles.numberInput} maxLength={2} keyboardType="number-pad" autoCapitalize="none" autoCorrect={false} value={enteredNumber} onChangeText={numberInputHandler} /> ) Image image to be put into assets/images path to an image should be done with require function Local image... import { View, TextInput, Button, StyleSheet, Modal, Image, } from 'react-native'; ... return ( <Modal visible={props.visible} animationType="slide"> <View style={styles.inputContainer}> <Image style={styles.image} source={require('../assets/images/goal.png')} /> </View> </Modal> ); } export default GoalInput; const styles = StyleSheet.create({ inputContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 16, backgroundColor: '#311b6b', }, image: { width: 100, height: 100, margin: 20, } }); for network image we do not use require for web images should provide width / height styles <Image style={styles.image} source={{uri: 'https://...'}} /> ImageBackground import { StyleSheet, ImageBackground, SafeAreaView } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; export default function App() { return ( <LinearGradient colors={[Colors.primary700, Colors.accent500]} style={styles.rootScreen} > <ImageBackground source={require('./assets/images/background.png')} resizeMode="cover" style={styles.rootScreen} imageStyle={styles.backgroundImage} > <SafeAreaView style={styles.rootScreen}>{screen}</SafeAreaView> </ImageBackground> </LinearGradient> ); } const styles = StyleSheet.create({ rootScreen: { flex: 1, }, backgroundImage: { opacity: 0.15, }, }); Button onPress same as on onClick in dom <Button onPress={() => setCount(count + 1)} title="Click me!" /> Custom button for ripple effect for android we provide android_ripple prop for iOS we apply pressed styles in callback function at style prop note that styles are combined in array when button is pressed import { View, Text, Pressable, StyleSheet } from 'react-native'; import Colors from '../../constants/colors'; function PrimaryButton({ children, onPress }) { return ( <View style={styles.buttonOuterContainer}> <Pressable style={({ pressed }) => pressed ? [styles.buttonInnerContainer, styles.pressed] : styles.buttonInnerContainer } onPress={onPress} android_ripple={{ color: Colors.primary600 }} > <Text style={styles.buttonText}>{children}</Text> </Pressable> </View> ); } export default PrimaryButton; const styles = StyleSheet.create({ buttonOuterContainer: { borderRadius: 28, margin: 4, overflow: 'hidden', }, buttonInnerContainer: { backgroundColor: Colors.primary500, paddingVertical: 8, paddingHorizontal: 16, elevation: 2, }, buttonText: { color: 'white', textAlign: 'center', }, pressed: { opacity: 0.75, }, }); Icon button import { Pressable, StyleSheet } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; function IconButton({ icon, color, onPress }) { return ( <Pressable onPress={onPress} style={({ pressed }) => pressed && styles.pressed} > <Ionicons name={icon} size={24} color={color} /> </Pressable> ); } export default IconButton; const styles = StyleSheet.create({ pressed: { opacity: 0.7, }, }); Outlined button import { Pressable, StyleSheet, Text } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { Colors } from '../../constants/colors'; function OutlinedButton({ onPress, icon, children }) { return ( <Pressable style={({ pressed }) => [styles.button, pressed && styles.pressed]} onPress={onPress} > <Ionicons style={styles.icon} name={icon} size={18} color={Colors.primary500} /> <Text style={styles.text}>{children}</Text> </Pressable> ); } export default OutlinedButton; const styles = StyleSheet.create({ button: { paddingHorizontal: 12, paddingVertical: 6, margin: 4, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', borderWidth: 1, borderColor: Colors.primary500, }, pressed: { opacity: 0.7, }, icon: { marginRight: 6, }, text: { color: Colors.primary500, }, }); ScrollView Main scrolling container that can contain multiple components and views Can scroll both vertically and horizontally import React from 'react' import { ScrollView, Text } from 'react-native' const App = () => ( <ScrollView> {[...Array(100).keys()].map( item => <Text key={item}>Line {item}</Text> )} </ScrollView> ) export default App FlatList & SectionList Common use is displaying list data that you fetch from a server Advantage is that it is lazy loaded <FlatList> component displays a scrolling list of changing data <SectionList> is the same, but broken into logical sections import React from 'react' import { FlatList, StyleSheet, Text, View } from 'react-native' const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 22 }, item: { padding: 10, fontSize: 18, height: 44 }, }) const FlatListBasics = () => ( <View style={styles.container}> <FlatList data={[ {key: 'Devin'}, {key: 'Dan'}, {key: 'Dominic'}, {key: 'Jackson'}, {key: 'James'}, {key: 'Joel'}, {key: 'John'}, {key: 'Jillian'}, {key: 'Jimmy'}, {key: 'Julie'}, ]} renderItem={({item}) => <Text style={styles.item}>{item.key}</Text>} /> </View> ) export default FlatListBasics In react we usually provide a key prop in map function In FlatList key should be integrated into the data items Or special keyExtractor prop should be used with callback returning something unique, see ex. below import React from 'react' import {SectionList, StyleSheet, Text, View} from 'react-native' const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 22 }, sectionHeader: { paddingTop: 2, paddingLeft: 10, paddingRight: 10, paddingBottom: 2, fontSize: 14, fontWeight: 'bold', backgroundColor: 'rgba(247,247,247,1.0)' }, item: { padding: 10, fontSize: 18, height: 44 }, }) const SectionListBasics = () => { return ( <View style={styles.container}> <SectionList sections={[ { title: 'D', data: ['Devin', 'Dan', 'Dominic'], }, { title: 'J', data: [ 'Jackson', 'James', 'Jillian', 'Jimmy', 'Joel', 'John', 'Julie' ], }, ]} renderItem={({ item }) => <Text style={styles.item}>{item}</Text>} renderSectionHeader={({section}) => ( <Text style={styles.sectionHeader}>{section.title}</Text> )} keyExtractor={(item) => `basicListEntry-${item.title}` } /> </View> ) } export default SectionListBasics Flatlist supports columns view with help of numColumns prop import { FlatList } from 'react-native'; import CategoryGridTile from '../components/CategoryGridTile'; import { CATEGORIES } from '../data/dummy-data'; function CategoriesScreen({ navigation }) { function renderCategoryItem(itemData) { function pressHandler() { navigation.navigate('MealsOverview', { categoryId: itemData.item.id, }); } return ( <CategoryGridTile title={itemData.item.title} color={itemData.item.color} onPress={pressHandler} /> ); } return ( <FlatList data={CATEGORIES} keyExtractor={(item) => item.id} renderItem={renderCategoryItem} numColumns={2} /> ); } export default CategoriesScreen; Pressable Button component is not much customizable Better to make a pressable component with text inside <Pressable onPress={props.onDeleteItem.bind(this, props.id)}> <View style={styles.goalItem}> <Text style={styles.goalText}>{props.text}</Text> </View> </Pressable> Pressable with ripple effect for android there is just a prop for iOS we do it manually providing a callback to the style prop import { StyleSheet, View, Text, Pressable } from 'react-native'; function GoalItem(props) { return ( <View style={styles.goalItem}> <Pressable android_ripple={{ color: '#210644' }} onPress={props.onDeleteItem.bind(this, props.id)} style={({ pressed }) => pressed && styles.pressedItem} > <Text style={styles.goalText}>{props.text}</Text> </Pressable> </View> ); } export default GoalItem; const styles = StyleSheet.create({ goalItem: { margin: 8, borderRadius: 6, backgroundColor: '#5e0acc', }, pressedItem: { opacity: 0.5, }, goalText: { color: 'white', padding: 8, }, }); Modal <Modal visible={props.visible} animationType="slide"> <View style={styles.inputContainer}> ... </View> </Modal> SafeAreaView <SafeAreaView> provides usable phone area which does not include notch Should wrap the app content import { StyleSheet, ImageBackground, SafeAreaView } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; export default function App() { return ( <LinearGradient colors={[Colors.primary700, Colors.accent500]} style={styles.rootScreen} > <ImageBackground source={require('./assets/images/background.png')} resizeMode="cover" style={styles.rootScreen} imageStyle={styles.backgroundImage} > <SafeAreaView style={styles.rootScreen}>{screen}</SafeAreaView> </ImageBackground> </LinearGradient> ); } const styles = StyleSheet.create({ rootScreen: { flex: 1, }, backgroundImage: { opacity: 0.15, }, }); Style There is no CSS in React Native Styling does not cascade style prop That is inline styles Not all elements supports it Multiple styles can be combined in array, with this approach we also can apply styles conditionally // App.js import React from 'react' import {Text, View} from 'react-native' const someStyles = { marin: 5} const HelloWorldApp = () => ( <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}> <Text style={{ fontSize: 20}}>Hello, world!</Text> <Text style={[{ fontSize: 20}, someStyles]}>Hello, world!</Text> </View> ) export default HelloWorldApp StyleSheet.create Use StyleSheet.create to create multiple styles at ones Good thing is that VSCode autocompletion works in StyleSheet.create import React from 'react' import { StyleSheet, Text, View } from 'react-native' const LotsOfStyles = () => ( <View style={styles.container}> <Text style={styles.red}>just red</Text> <Text style={styles.bigBlue}>just bigBlue</Text> <Text style={[styles.bigBlue, styles.red]}>bigBlue, then red</Text> <Text style={[styles.red, styles.bigBlue]}>red, then bigBlue</Text> </View> ) const styles = StyleSheet.create({ container: { marginTop: 50 }, bigBlue: { color: 'blue', fontWeight: 'bold', fontSize: 30 }, red: { color: 'red' } }) export default LotsOfStyles App background we may manually put background color for main views but that is annoying with expo just may add backgroundColor into the app.json file and it will be applied to all components except modals export default function App() { return ( <> <View style={styles.appContainer}> ... </View> </> ); } const styles = StyleSheet.create({ appContainer: { flex: 1, backgroundColor: '#ddb52f' }, }) { "expo": { "name": "RNCourse", "slug": "RNCourse", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", "backgroundColor": "#1e085a", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#ffffff" }, "updates": { "fallbackToCacheTimeout": 0 }, "assetBundlePatterns": [ "**/*" ], "ios": { "supportsTablet": true }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#FFFFFF" } }, "web": { "favicon": "./assets/favicon.png" } } } Linear gradient image expo provides the package for linear gradient colors install it with expo install expo-linear-gradient import { StyleSheet, ImageBackground, SafeAreaView } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; export default function App() { return ( <LinearGradient colors={['#4e0329', '#ddb52f']} style={styles.rootScreen} > ... </LinearGradient> ); } const styles = StyleSheet.create({ rootScreen: { flex: 1, } }); Status bar in the upper part of phone we have battery icon, provider name, etc... we can change the color of elements with a special expo component <StatusBar /> import { StyleSheet, View, FlatList, Button } from 'react-native'; import { StatusBar } from 'expo-status-bar'; export default function App() { return ( <> <StatusBar style="light" /> <View style={styles.appContainer}> ... </View> </> ); } Shadow For android use elevation prop For iOS use variety of shadow... props do not forget to provide backgroundColor for iOS import { View, Text, StyleSheet } from 'react-native'; import Colors from '../../constants/colors'; function GuessLogItem({ roundNumber, guess }) { return ( <View style={styles.listItem}> <Text style={styles.itemText}>#{roundNumber}</Text> <Text style={styles.itemText}>Opponent's Guess: {guess}</Text> </View> ); } export default GuessLogItem; const styles = StyleSheet.create({ listItem: { borderColor: Colors.primary800, borderWidth: 1, borderRadius: 40, padding: 12, marginVertical: 8, backgroundColor: Colors.accent500, flexDirection: 'row', justifyContent: 'space-between', width: '100%', elevation: 4, // for android shadowColor: 'black', // for iOS shadowOffset: { width: 0, height: 0 }, // for iOS shadowOpacity: 0.25, // for iOS shadowRadius: 3, // for iOS }, itemText: { fontFamily: 'open-sans' } }); Debugging Some info can be found here Terminal there are different options error comes automatically to the terminal console logging also comes to the terminal expo shows some error messages at the bottom of the emulator JS dev tools we can also open Chrome like console with expo by j in the terminal, there we also can see network requests can do the same by opening the menu by m (or Cmd + D in emulator) and the press Open JS debugger React dev tools to see the component tree and monitor state values we may use react dev tools install it globally with npm i -g react-devtools open dev tools by react-devtools in a separate terminal window may require to reload the app with r key Alert, prompt alert and prompt can be invoked from the special object provided by react-native Alert import { TextInput, View, StyleSheet, Alert } from 'react-native'; ... if (isNaN(chosenNumber) || chosenNumber <= 0 || chosenNumber > 99) { Alert.alert( 'Invalid number!', 'Number has to be a number between 1 and 99.', [{ text: 'Okay', style: 'destructive', onPress: resetInputHandler }] ); return; } Icons Expo comes with icons set No need to install, just import it for ex. may find a proper icon here https://icons.expo.fyi/ import { View, StyleSheet, Alert, Text, FlatList } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; function GameScreen() { return ( <View style={styles.buttonContainer}> <PrimaryButton onPress={nextGuessHandler.bind(this, 'greater')}> <Ionicons name="md-add" size={24} color="white" /> </PrimaryButton> </View> ); } export default GameScreen; const styles = StyleSheet.create({ buttonContainer: { flex: 1, }, }); Fonts custom fonts can be installed from Expo npm i expo-font to utilize a font we need to bring a hook import { useFonts } from 'expo-font' google fonts usage guide can be found here also may add any font manually put font .ttf file into for ex. assets/fonts/OpenSans-Regular.ttf then include it via hook as useFonts({ 'open-sans': include('./assets/fonts/OpenSans-Regular.ttf') 'open-sans-bold': include('./assets/fonts/OpenSans-Regular-Bold.ttf') }) fonts need some time to be loaded and we need to show some loading screen meanwhile add another package for that expo install expo-app-loading check how to use a loading package in snippet below to use a font put its name property as a value for fontFamily prop of a style // App.js import { StyleSheet, ImageBackground, SafeAreaView } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; import { useFonts } from 'expo-font'; import AppLoading from 'expo-app-loading'; export default function App() { const [fontsLoaded] = useFonts({ 'open-sans': require('./assets/fonts/OpenSans-Regular.ttf'), 'open-sans-bold': require('./assets/fonts/OpenSans-Bold.ttf'), }); if (!fontsLoaded) { return <AppLoading />; } return ( <LinearGradient colors={[Colors.primary700, Colors.accent500]} style={styles.rootScreen} > <ImageBackground source={require('./assets/images/background.png')} resizeMode="cover" style={styles.rootScreen} imageStyle={styles.backgroundImage} > <SafeAreaView style={styles.rootScreen}>{screen}</SafeAreaView> </ImageBackground> </LinearGradient> ); } const styles = StyleSheet.create({ rootScreen: { flex: 1, }, backgroundImage: { opacity: 0.15, }, }); // NumberContainer.js import { View, Text, StyleSheet } from 'react-native'; function NumberContainer({ children }) { return ( <View style={styles.container}> <Text style={styles.numberText}>{children}</Text> </View> ); } export default NumberContainer; const styles = StyleSheet.create({ container: { ... }, numberText: { color: Colors.accent500, fontSize: 36, fontFamily: 'open-sans-bold' }, }); Dimensions Instead of using width with numbers Use minWidth , maxWidth and width with percents like '80% We can set values dynamically based on device screen dimensions with Dimensions api import { View, StyleSheet, Dimensions } from 'react-native'; function Card({ children }) { return <View style={styles.card}>{children}</View>; } export default Card; const deviceWidth = Dimensions.get('window').width; const styles = StyleSheet.create({ card: { justifyContent: 'center', alignItems: 'center', marginTop: deviceWidth < 380 ? 18 : 36, marginHorizontal: 24, padding: 16, }, }); Rotation to lock the vertical rotation add "orientation": "portrait" prop into app.json file "orientation": "landscape" for horizontal view "orientation": "default" to make it rotatable but design might be broken if we flip the device and we need to fix it manually we may use Dimensions.get('window').height but as I understood it will not response to the rotation for that reason there is a useWindowDimensions() hook useWindowDimensions useWindowDimensions() hook does respond to the phone rotations import { TextInput, View, StyleSheet, Alert, KeyboardAvoidingView, ScrollView, } from 'react-native'; function StartGameScreen({ onPickNumber }) { const [enteredNumber, setEnteredNumber] = useState(''); return ( <ScrollView style={styles.screen}> <KeyboardAvoidingView style={styles.screen} behavior="position"> <View style={[styles.rootContainer, { marginTop: marginTopDistance }]}> <TextInput style={styles.numberInput} maxLength={2} keyboardType="number-pad" autoCapitalize="none" autoCorrect={false} onChangeText={numberInputHandler} value={enteredNumber} /> ... </View> </KeyboardAvoidingView> </ScrollView> ); } export default StartGameScreen; KeyboardAvoidingView with input interaction the keyboard appears and may break the style app style can be configured for the keyboard with conjunction of 3 components: ScrollView , import { useState } from 'react'; import { TextInput, View, StyleSheet, Alert, useWindowDimensions, KeyboardAvoidingView, ScrollView, } from 'react-native'; function StartGameScreen({ onPickNumber }) { const [enteredNumber, setEnteredNumber] = useState(''); const { width, height } = useWindowDimensions(); const marginTopDistance = height < 380 ? 30 : 100; return ( <ScrollView style={styles.screen}> <KeyboardAvoidingView style={styles.screen} behavior="position"> <View style={[styles.rootContainer, { marginTop: marginTopDistance }]}> ... </View> </KeyboardAvoidingView> </ScrollView> ); } export default StartGameScreen; const styles = StyleSheet.create({ screen: { flex: 1, }, rootContainer: { flex: 1, // marginTop: deviceHeight < 380 ? 30 : 100, alignItems: 'center', }, numberInput: { height: 50, width: 50, fontSize: 32, borderBottomColor: Colors.accent500, borderBottomWidth: 2, color: Colors.accent500, marginVertical: 8, fontWeight: 'bold', textAlign: 'center', }, buttonsContainer: { flexDirection: 'row', }, buttonContainer: { flex: 1, }, }); Platform specific code We can target not only the device screen dimensions with useWindowDimensions / Dimensions but also the device models itself with platform api import { Platform, StyleSheet } from 'react-native' const styles = StyleSheet.create({ height: Platform.OS === 'ios' ? 200 : 100 }) There is also a Platform.select method available, that gives an object where keys can be one of 'ios' | 'android' | 'native' | 'default', returns the most fitting value for the platform you are currently running on. import { Platform, StyleSheet } from 'react-native' const styles = StyleSheet.create({ height: Platform.select({ ios: 200, android: 100 }) }) const Component = Platform.select({ ios: () => require('ComponentIOS'), android: () => require('ComponentAndroid') })() <Component /> Detecting the os version import { Platform } from 'react-native'; if (Platform.Version === 25) { console.log('Running on Nougat!'); } import { Platform } from 'react-native'; const majorVersionIOS = parseInt(Platform.Version, 10); if (majorVersionIOS <= 9) { console.log('Work around a change in behavior'); } Platform-specific components Consider splitting the code into separate files for ios and android if needed React Native will detect when a file has a .ios. or .android. extension Or even platform specific components applies not only to components, but to any javascript files, like constants, utils, styles etc... // files Container.js # picked up by Webpack, Rollup or any other Web bundler Container.native.js # picked up by the React Native bundler for both Android and iOS (Metro) BigButton.ios.js BigButton.android.js Not that imports does not have .ios or .android extensions in the file names. import BigButton from './BigButton' import Container from './Container' Navigation We may use conditional rendering for the navigation, that is fine But there is a better animated way via https://reactnavigation.org/ Install with npm install @react-navigation/native For Expo also install this npx expo install react-native-screens react-native-safe-area-context There are different navigator components, for ex. native-stack https://reactnavigation.org/docs/native-stack-navigator Install it also npm install @react-navigation/native-stack We wrap content into NavigationContainer and Stack.Navigator component First Stack.Screen inside Stack.Navigator will be the initial screen Or init screen can be set by <Stack.Navigator initialRouteName="ProductDetails"> // App.js import { StatusBar } from 'expo-status-bar'; import { StyleSheet } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import CategoriesScreen from './screens/CategoriesScreen'; import MealsOverviewScreen from './screens/MealsOverviewScreen'; const Stack = createNativeStackNavigator(); export default function App() { return ( <> <StatusBar style="dark" /> <NavigationContainer> <Stack.Navigator> <Stack.Screen name="MealsCategories" component={CategoriesScreen} /> <Stack.Screen name="MealsOverview" component={MealsOverviewScreen} /> </Stack.Navigator> </NavigationContainer> </> ); } const styles = StyleSheet.create({ container: {}, }); Components at Stack.Navigator automatically receives navigation prop With navigation.navigate('name_of_the_screen'); we do navigation // CategoriesScreen.js import { FlatList } from 'react-native'; import CategoryGridTile from '../components/CategoryGridTile'; import { CATEGORIES } from '../data/dummy-data'; function CategoriesScreen({ navigation }) { function renderCategoryItem(itemData) { function pressHandler() { navigation.navigate('MealsOverview'); } return ( <CategoryGridTile title={itemData.item.title} color={itemData.item.color} onPress={pressHandler} /> ); } return ( <FlatList data={CATEGORIES} keyExtractor={(item) => item.id} renderItem={renderCategoryItem} numColumns={2} /> ); } export default CategoriesScreen; Stack.Screen option At the of the screen we have a header, which takes the name prop value by default It can be configured inside options prop All options can be found here Also default navigation screens options can be set on the Stack.Navigator component <NavigationContainer> <Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: '#351401' }, headerTintColor: 'white', contentStyle: { backgroundColor: '#3f2f25' }, }} > <Stack.Screen name="Drawer" component={DrawerNavigator} options={{ headerShown: false, }} /> <Stack.Screen name="MealsOverview" component={MealsOverviewScreen} /> <Stack.Screen name="MealDetail" component={MealDetailScreen} options={{ title: 'About the Meal', }} /> </Stack.Navigator> </NavigationContainer> Stack.Screen dynamic option we can pass a function into the options prop to get data and set options dynamically it has an access to route & navigation properties <Stack.Screen name="MealDetail" component={MealDetailScreen} options={({route, navigation}) => { const catId = route.params.categoryId return { title: catId } }} /> also we can set screen options from the component by special navigation.setOptions() method it should be done in useEffect or useLayoutEffect hook import { useLayoutEffect } from 'react'; import { View, FlatList, StyleSheet } from 'react-native'; import MealItem from '../components/MealItem'; import { MEALS, CATEGORIES } from '../data/dummy-data'; function MealsOverviewScreen({ route, navigation }) { const catId = route.params.categoryId; const displayedMeals = MEALS.filter((mealItem) => { return mealItem.categoryIds.indexOf(catId) >= 0; }); useLayoutEffect(() => { const categoryTitle = CATEGORIES.find( (category) => category.id === catId ).title; navigation.setOptions({ title: categoryTitle, }); }, [catId, navigation]); function renderMealItem(itemData) { const item = itemData.item; const mealItemProps = { id: item.id, title: item.title, imageUrl: item.imageUrl, affordability: item.affordability, complexity: item.complexity, duration: item.duration, }; return <MealItem {...mealItemProps} />; } return ( <View style={styles.container}> <FlatList data={displayedMeals} keyExtractor={(item) => item.id} renderItem={renderMealItem} /> </View> ); } export default MealsOverviewScreen; const styles = StyleSheet.create({ container: { flex: 1, padding: 16, }, }); useNavigation If navigation action is required outside of a component at Stack.Navigator We may use useNavigation() hook at any component import { View, Pressable, Text, Image, StyleSheet, Platform, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import MealDetails from './MealDetails'; function MealItem({ id, title, imageUrl, duration, complexity, affordability, }) { const navigation = useNavigation(); function selectMealItemHandler() { navigation.navigate('MealDetail', { mealId: id, }); } return ( <View style={styles.mealItem}> <Pressable android_ripple={{ color: '#ccc' }} style={({ pressed }) => (pressed ? styles.buttonPressed : null)} onPress={selectMealItemHandler} > ... </View> </Pressable> </View> ); } Pass data via navigation With navigation.navigate('name_of_the_screen', ); we can pass data in second parameter import { FlatList } from 'react-native'; import CategoryGridTile from '../components/CategoryGridTile'; import { CATEGORIES } from '../data/dummy-data'; function CategoriesScreen({ navigation }) { function renderCategoryItem(itemData) { function pressHandler() { navigation.navigate('MealsOverview', { categoryId: itemData.item.id, }); } return ( <CategoryGridTile title={itemData.item.title} color={itemData.item.color} onPress={pressHandler} /> ); } return ( <FlatList data={CATEGORIES} keyExtractor={(item) => item.id} renderItem={renderCategoryItem} numColumns={2} /> ); } export default CategoriesScreen; to extract data we may use passed route object same data can be get with useRoute() hook import { useLayoutEffect } from 'react'; import { View, FlatList, StyleSheet } from 'react-native'; import MealItem from '../components/MealItem'; import { MEALS, CATEGORIES } from '../data/dummy-data'; function MealsOverviewScreen({ route, navigation }) { const catId = route.params.categoryId; const displayedMeals = MEALS.filter((mealItem) => { return mealItem.categoryIds.indexOf(catId) >= 0; }); useLayoutEffect(() => { const categoryTitle = CATEGORIES.find( (category) => category.id === catId ).title; navigation.setOptions({ title: categoryTitle, }); }, [catId, navigation]); function renderMealItem(itemData) { const item = itemData.item; const mealItemProps = { id: item.id, title: item.title, imageUrl: item.imageUrl, affordability: item.affordability, complexity: item.complexity, duration: item.duration, }; return <MealItem {...mealItemProps} />; } return ( <View style={styles.container}> <FlatList data={displayedMeals} keyExtractor={(item) => item.id} renderItem={renderMealItem} /> </View> ); } export default MealsOverviewScreen; const styles = StyleSheet.create({ container: { flex: 1, padding: 16, }, }); or... import { useRoute } from '@react-navigation/native'; function MealsOverviewScreen({ navigation }) { const route = useRoute() ... } Add button to navigation by default in navigation header we have a title and go back button we may add additional button, for ex. to save some item useLayoutEffect(() => { navigation.setOptions({ headerRight: () => { return ( <IconButton icon="star" color="white" onPress={headerButtonPressHandler} /> ); }, }); }, [navigation, headerButtonPressHandler]); or... <Stack.Screen name="some name" component={SomeComponent} options={{ headerRight: ( <IconButton icon="star" color="white" onPress={headerButtonPressHandler} /> ), }} /> Combine different navigators Stack navigation is the best, but in some apps we may need also a drawer and bottom tabs to navigate between screens We can combine different navigators import { StatusBar } from 'expo-status-bar'; import { StyleSheet, Button } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { Ionicons } from '@expo/vector-icons'; import CategoriesScreen from './screens/CategoriesScreen'; import MealsOverviewScreen from './screens/MealsOverviewScreen'; import MealDetailScreen from './screens/MealDetailScreen'; import FavoritesScreen from './screens/FavoritesScreen'; const Stack = createNativeStackNavigator(); const Drawer = createDrawerNavigator(); function DrawerNavigator() { return ( <Drawer.Navigator screenOptions={{ headerStyle: { backgroundColor: '#351401' }, headerTintColor: 'white', sceneContainerStyle: { backgroundColor: '#3f2f25' }, drawerContentStyle: { backgroundColor: '#351401' }, drawerInactiveTintColor: 'white', drawerActiveTintColor: '#351401', drawerActiveBackgroundColor: '#e4baa1', }} > <Drawer.Screen name="Categories" component={CategoriesScreen} options={{ title: 'All Categories', drawerIcon: ({ color, size }) => ( <Ionicons name="list" color={color} size={size} /> ), }} /> <Drawer.Screen name="Favorites" component={FavoritesScreen} options={{ drawerIcon: ({ color, size }) => ( <Ionicons name="star" color={color} size={size} /> ), }} /> </Drawer.Navigator> ); } export default function App() { return ( <> <StatusBar style="light" /> <NavigationContainer> <Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: '#351401' }, headerTintColor: 'white', contentStyle: { backgroundColor: '#3f2f25' }, }} > <Stack.Screen name="Drawer" component={DrawerNavigator} options={{ headerShown: false, }} /> <Stack.Screen name="MealsOverview" component={MealsOverviewScreen} /> <Stack.Screen name="MealDetail" component={MealDetailScreen} options={{ title: 'About the Meal', }} /> </Stack.Navigator> </NavigationContainer> </> ); } const styles = StyleSheet.create({ container: {}, }); Loading sinner import { View, ActivityIndicator, StyleSheet } from 'react-native'; import { GlobalStyles } from '../../constants/styles'; function LoadingOverlay() { return ( <View style={styles.container}> <ActivityIndicator size="large" color="white" /> </View> ); } export default LoadingOverlay; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24, backgroundColor: GlobalStyles.colors.primary700, }, }); Overlay is used here during http request. import { useContext, useEffect, useState } from 'react'; import ExpensesOutput from '../components/ExpensesOutput/ExpensesOutput'; import ErrorOverlay from '../components/UI/ErrorOverlay'; import LoadingOverlay from '../components/UI/LoadingOverlay'; import { ExpensesContext } from '../store/expenses-context'; import { getDateMinusDays } from '../util/date'; import { fetchExpenses } from '../util/http'; function RecentExpenses() { const [isFetching, setIsFetching] = useState(true); const [error, setError] = useState(); const expensesCtx = useContext(ExpensesContext); useEffect(() => { async function getExpenses() { setIsFetching(true); try { const expenses = await fetchExpenses(); expensesCtx.setExpenses(expenses); } catch (error) { setError('Could not fetch expenses!'); } setIsFetching(false); } getExpenses(); }, []); if (error && !isFetching) { return <ErrorOverlay message={error} />; } if (isFetching) { return <LoadingOverlay />; } const recentExpenses = expensesCtx.expenses.filter((expense) => { const today = new Date(); const date7DaysAgo = getDateMinusDays(today, 7); return expense.date >= date7DaysAgo && expense.date <= today; }); return ( <ExpensesOutput expenses={recentExpenses} expensesPeriod="Last 7 Days" fallbackText="No expenses registered for the last 7 days." /> ); } export default RecentExpenses; Camera https://docs.expo.dev/versions/latest/sdk/camera/ Image picker allows to pick or take an image https://docs.expo.dev/versions/v48.0.0/sdk/imagepicker/ npx expo install expo-image-picker Add permission request via app.json (see link above) { "expo": { "plugins": [ [ "expo-image-picker", { "photosPermission": "The app accesses your photos to let you share them with your friends." } ] ] } } for android permission logic is done automatically, but for iOS we need to make in on our own with useCameraPermissions and PermissionStatus utilities photo details object will be returned by const image = await launchCameraAsync() function image.uri can be used with <Image style={styles.image} source={{ uri: pickedImage }} /> do not forget to provide dimensions in image styles import { Alert, Button, Image, StyleSheet, Text, View } from 'react-native'; import { launchCameraAsync, useCameraPermissions, PermissionStatus, } from 'expo-image-picker'; import { useState } from 'react'; import { Colors } from '../../constants/colors'; function ImagePicker() { const [pickedImage, setPickedImage] = useState(); const [cameraPermissionInformation, requestPermission] = useCameraPermissions(); async function verifyPermissions() { if (cameraPermissionInformation.status === PermissionStatus.UNDETERMINED) { const permissionResponse = await requestPermission(); return permissionResponse.granted; } if (cameraPermissionInformation.status === PermissionStatus.DENIED) { Alert.alert( 'Insufficient Permissions!', 'You need to grant camera permissions to use this app.' ); return false; } return true; } async function takeImageHandler() { const hasPermission = await verifyPermissions(); if (!hasPermission) { return; } const image = await launchCameraAsync({ allowsEditing: true, aspect: [16, 9], quality: 0.5, }); setPickedImage(image.uri); } let imagePreview = <Text>No image taken yet.</Text>; if (pickedImage) { imagePreview = <Image style={styles.image} source={{ uri: pickedImage }} />; } return ( <View> <View style={styles.imagePreview}>{imagePreview}</View> <Button title="Take Image" onPress={takeImageHandler} /> </View> ); } export default ImagePicker; const styles = StyleSheet.create({ imagePreview: { width: '100%', height: 200, marginVertical: 8, justifyContent: 'center', alignItems: 'center', backgroundColor: Colors.primary100, borderRadius: 4, }, image: { width: '100%', height: '100%', }, }); Geo location + Google Maps image https://docs.expo.dev/versions/v48.0.0/sdk/location/ npx expo install expo-location import { useState } from 'react'; import { Alert, View, StyleSheet, Image, Text } from 'react-native'; import { getCurrentPositionAsync, useForegroundPermissions, PermissionStatus, } from 'expo-location'; import { Colors } from '../../constants/colors'; import OutlinedButton from '../UI/OutlinedButton'; const GOOGLE_API_KEY = 'AIzaSyCTCDNDtYPCpAD0FaKgHgdzCjMN1QUHnt4'; function getMapPreview(lat, lng) { const imagePreviewUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=14&size=400x200&maptype=roadmap&markers=color:red%7Clabel:S%7C${lat},${lng}&key=${GOOGLE_API_KEY}`; return imagePreviewUrl; } function LocationPicker() { const [pickedLocation, setPickedLocation] = useState(); const [locationPermissionInformation, requestPermission] = useForegroundPermissions(); async function verifyPermissions() { if ( locationPermissionInformation.status === PermissionStatus.UNDETERMINED ) { const permissionResponse = await requestPermission(); return permissionResponse.granted; } if (locationPermissionInformation.status === PermissionStatus.DENIED) { Alert.alert( 'Insufficient Permissions!', 'You need to grant location permissions to use this app.' ); return false; } return true; } async function getLocationHandler() { const hasPermission = await verifyPermissions(); if (!hasPermission) { return; } const location = await getCurrentPositionAsync(); setPickedLocation({ lat: location.coords.latitude, lng: location.coords.longitude, }); } function pickOnMapHandler() {} let locationPreview = <Text>No location picked yet.</Text>; if (pickedLocation) { locationPreview = ( <Image style={styles.image} source={{ uri: getMapPreview(pickedLocation.lat, pickedLocation.lng), }} /> ); } return ( <View> <View style={styles.mapPreview}>{locationPreview}</View> <View style={styles.actions}> <OutlinedButton icon="location" onPress={getLocationHandler}> Locate User </OutlinedButton> <OutlinedButton icon="map" onPress={pickOnMapHandler}> Pick on Map </OutlinedButton> </View> </View> ); } export default LocationPicker; const styles = StyleSheet.create({ mapPreview: { width: '100%', height: 200, marginVertical: 8, justifyContent: 'center', alignItems: 'center', backgroundColor: Colors.primary100, borderRadius: 4, overflow: 'hidden' }, actions: { flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', }, image: { width: '100%', height: '100%', // borderRadius: 4 }, }); Interactive device map npx expo install react-native-maps https://docs.expo.dev/versions/v48.0.0/sdk/map-view/ in the example below we open an interactive map on a separate screen on press put a picker on save button return to previous screen and pass coordinates to show it on the map one interesting note: when we return to the previous screen the useEffect is not firing, because the stack screen is not mounted, but simply unhides to let useEffect kicks in we add additional useIsFocused() hook import { useEffect, useState } from 'react'; import { Alert, View, StyleSheet, Image, Text } from 'react-native'; import { getCurrentPositionAsync, useForegroundPermissions, PermissionStatus, } from 'expo-location'; import { useNavigation, useRoute, useIsFocused, } from '@react-navigation/native'; import { Colors } from '../../constants/colors'; import OutlinedButton from '../UI/OutlinedButton'; const GOOGLE_API_KEY = 'AIzaSyCTCDNDtYPCpAD0FaKgHgdzCjMN1QUHnt4'; function getMapPreview(lat, lng) { const imagePreviewUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=14&size=400x200&maptype=roadmap&markers=color:red%7Clabel:S%7C${lat},${lng}&key=${GOOGLE_API_KEY}`; return imagePreviewUrl; } function LocationPicker() { const [pickedLocation, setPickedLocation] = useState(); const isFocused = useIsFocused(); const navigation = useNavigation(); const route = useRoute(); const [locationPermissionInformation, requestPermission] = useForegroundPermissions(); useEffect(() => { if (isFocused && route.params) { const mapPickedLocation = { lat: route.params.pickedLat, lng: route.params.pickedLng, }; setPickedLocation(mapPickedLocation); } }, [route, isFocused]); async function verifyPermissions() { if ( locationPermissionInformation.status === PermissionStatus.UNDETERMINED ) { const permissionResponse = await requestPermission(); return permissionResponse.granted; } if (locationPermissionInformation.status === PermissionStatus.DENIED) { Alert.alert( 'Insufficient Permissions!', 'You need to grant location permissions to use this app.' ); return false; } return true; } async function getLocationHandler() { const hasPermission = await verifyPermissions(); if (!hasPermission) { return; } const location = await getCurrentPositionAsync(); setPickedLocation({ lat: location.coords.latitude, lng: location.coords.longitude, }); } function pickOnMapHandler() { navigation.navigate('Map'); } let locationPreview = <Text>No location picked yet.</Text>; if (pickedLocation) { locationPreview = ( <Image style={styles.image} source={{ uri: getMapPreview(pickedLocation.lat, pickedLocation.lng), }} /> ); } return ( <View> <View style={styles.mapPreview}>{locationPreview}</View> <View style={styles.actions}> <OutlinedButton icon="location" onPress={getLocationHandler}> Locate User </OutlinedButton> <OutlinedButton icon="map" onPress={pickOnMapHandler}> Pick on Map </OutlinedButton> </View> </View> ); } export default LocationPicker; const styles = StyleSheet.create({ mapPreview: { width: '100%', height: 200, marginVertical: 8, justifyContent: 'center', alignItems: 'center', backgroundColor: Colors.primary100, borderRadius: 4, overflow: 'hidden', }, actions: { flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', }, image: { width: '100%', height: '100%', // borderRadius: 4 }, }); import { useCallback, useLayoutEffect, useState } from 'react'; import { Alert, StyleSheet } from 'react-native'; import MapView, { Marker } from 'react-native-maps'; import IconButton from '../components/UI/IconButton'; function Map({ navigation }) { const [selectedLocation, setSelectedLocation] = useState(); const region = { latitude: 37.78, longitude: -122.43, latitudeDelta: 0.0922, longitudeDelta: 0.0421, }; function selectLocationHandler(event) { const lat = event.nativeEvent.coordinate.latitude; const lng = event.nativeEvent.coordinate.longitude; setSelectedLocation({ lat: lat, lng: lng }); } const savePickedLocationHandler = useCallback(() => { if (!selectedLocation) { Alert.alert( 'No location picked!', 'You have to pick a location (by tapping on the map) first!' ); return; } navigation.navigate('AddPlace', { pickedLat: selectedLocation.lat, pickedLng: selectedLocation.lng, }); }, [navigation, selectedLocation]); useLayoutEffect(() => { navigation.setOptions({ headerRight: ({ tintColor }) => ( <IconButton icon="save" size={24} color={tintColor} onPress={savePickedLocationHandler} /> ), }); }, [navigation, savePickedLocationHandler]); return ( <MapView style={styles.map} initialRegion={region} onPress={selectLocationHandler} > {selectedLocation && ( <Marker title="Picked Location" coordinate={{ latitude: selectedLocation.lat, longitude: selectedLocation.lng, }} /> )} </MapView> ); } export default Map; const styles = StyleSheet.create({ map: { flex: 1, }, }); SQLite for data storage on device https://docs.expo.dev/versions/latest/sdk/sqlite/ https://docs.expo.dev/versions/latest/sdk/sqlite/ Below how we interact with database import * as SQLite from 'expo-sqlite'; import { Place } from '../models/place'; const database = SQLite.openDatabase('places.db'); export function init() { const promise = new Promise((resolve, reject) => { database.transaction((tx) => { tx.executeSql( `CREATE TABLE IF NOT EXISTS places ( id INTEGER PRIMARY KEY NOT NULL, title TEXT NOT NULL, imageUri TEXT NOT NULL, address TEXT NOT NULL, lat REAL NOT NULL, lng REAL NOT NULL )`, [], () => { resolve(); }, (_, error) => { reject(error); } ); }); }); return promise; } export function insertPlace(place) { const promise = new Promise((resolve, reject) => { database.transaction((tx) => { tx.executeSql( `INSERT INTO places (title, imageUri, address, lat, lng) VALUES (?, ?, ?, ?, ?)`, [ place.title, place.imageUri, place.address, place.location.lat, place.location.lng, ], (_, result) => { resolve(result); }, (_, error) => { reject(error); } ); }); }); return promise; } export function fetchPlaces() { const promise = new Promise((resolve, reject) => { database.transaction((tx) => { tx.executeSql( 'SELECT * FROM places', [], (_, result) => { const places = []; for (const dp of result.rows._array) { places.push( new Place( dp.title, dp.imageUri, { address: dp.address, lat: dp.lat, lng: dp.lng, }, dp.id ) ); } resolve(places); }, (_, error) => { reject(error); } ); }); }); return promise; } export function fetchPlaceDetails(id) { const promise = new Promise((resolve, reject) => { database.transaction((tx) => { tx.executeSql( 'SELECT * FROM places WHERE id = ?', [id], (_, result) => { const dbPlace = result.rows._array[0]; const place = new Place( dbPlace.title, dbPlace.imageUri, { lat: dbPlace.lat, lng: dbPlace.lng, address: dbPlace.address }, dbPlace.id ); resolve(place); }, (_, error) => { reject(error); } ); }); }); return promise; } Database is initialized in App component after the load import { useEffect, useState } from 'react'; import { StatusBar } from 'expo-status-bar'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import AppLoading from 'expo-app-loading'; import AllPlaces from './screens/AllPlaces'; import AddPlace from './screens/AddPlace'; import IconButton from './components/UI/IconButton'; import { Colors } from './constants/colors'; import Map from './screens/Map'; import { init } from './util/database'; import PlaceDetails from './screens/PlaceDetails'; const Stack = createNativeStackNavigator(); export default function App() { const [dbInitialized, setDbInitialized] = useState(false); useEffect(() => { init() .then(() => { setDbInitialized(true); }) .catch((err) => { console.log(err); }); }, []); if (!dbInitialized) { return <AppLoading />; } return ( <> <StatusBar style="dark" /> <NavigationContainer> <Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: Colors.primary500 }, headerTintColor: Colors.gray700, contentStyle: { backgroundColor: Colors.gray700 }, }} > <Stack.Screen name="AllPlaces" component={AllPlaces} options={({ navigation }) => ({ title: 'Your Favorite Places', headerRight: ({ tintColor }) => ( <IconButton icon="add" size={24} color={tintColor} onPress={() => navigation.navigate('AddPlace')} /> ), })} /> <Stack.Screen name="AddPlace" component={AddPlace} options={{ title: 'Add a new Place', }} /> <Stack.Screen name="Map" component={Map} /> <Stack.Screen name="PlaceDetails" component={PlaceDetails} options={{ title: 'Loading Place...', }} /> </Stack.Navigator> </NavigationContainer> </> ); } That is how we use inserting into DB function import PlaceForm from '../components/Places/PlaceForm'; import { insertPlace } from '../util/database'; function AddPlace({ navigation }) { async function createPlaceHandler(place) { await insertPlace(place); navigation.navigate('AllPlaces'); } return <PlaceForm onCreatePlace={createPlaceHandler} />; } export default AddPlace; That is how we get data import { useEffect, useState } from 'react'; import { useIsFocused } from '@react-navigation/native'; import PlacesList from '../components/Places/PlacesList'; import { fetchPlaces } from '../util/database'; function AllPlaces({ route }) { const [loadedPlaces, setLoadedPlaces] = useState([]); const isFocused = useIsFocused(); useEffect(() => { async function loadPlaces() { const places = await fetchPlaces(); setLoadedPlaces(places); } if (isFocused) { loadPlaces(); // setLoadedPlaces((curPlaces) => [...curPlaces, route.params.place]); } }, [isFocused]); return <PlacesList places={loadedPlaces} />; } export default AllPlaces; That is how we fetch specific item from the db import { useEffect, useState } from 'react'; import { ScrollView, Image, View, Text, StyleSheet } from 'react-native'; import OutlinedButton from '../components/UI/OutlinedButton'; import { Colors } from '../constants/colors'; import { fetchPlaceDetails } from '../util/database'; function PlaceDetails({ route, navigation }) { const [fetchedPlace, setFetchedPlace] = useState(); function showOnMapHandler() { navigation.navigate('Map', { initialLat: fetchedPlace.location.lat, initialLng: fetchedPlace.location.lng, }); } const selectedPlaceId = route.params.placeId; useEffect(() => { async function loadPlaceData() { const place = await fetchPlaceDetails(selectedPlaceId); setFetchedPlace(place); navigation.setOptions({ title: place.title, }); } loadPlaceData(); }, [selectedPlaceId]); if (!fetchedPlace) { return ( <View style={styles.fallback}> <Text>Loading place data...</Text> </View> ); } return ( <ScrollView> <Image style={styles.image} source={{ uri: fetchedPlace.imageUri }} /> <View style={styles.locationContainer}> <View style={styles.addressContainer}> <Text style={styles.address}>{fetchedPlace.address}</Text> </View> <OutlinedButton icon="map" onPress={showOnMapHandler}> View on Map </OutlinedButton> </View> </ScrollView> ); } export default PlaceDetails; const styles = StyleSheet.create({ fallback: { flex: 1, justifyContent: 'center', alignItems: 'center', }, image: { height: '35%', minHeight: 300, width: '100%', }, locationContainer: { justifyContent: 'center', alignItems: 'center', }, addressContainer: { padding: 20, }, address: { color: Colors.primary500, textAlign: 'center', fontWeight: 'bold', fontSize: 16, }, }); Without Expo Expo provides us a development service which has a client for all devices to run the code So far we used Expo Managed workflow with minimum configuration and where we can mix JS with native code We can use also Expo Bare workflow which is more configurable Or use just React Native CLI without Expo For Bare workflow you need to setup basic React Native environment expo init and choose bare options Looks the same, but more files and folders To run the development emulator execute the npm script from the package.json, for ex. npm run ios You may need to make additional configuration to use Expo services with bare workflow, for location service some steps are needed We can go from managed to bare workflow by expo eject if you initialized a project with native cli and want to use expo packages you can do that https://docs.expo.dev/bare/installing-expo-modules/ Publish https://expo.dev/eas - service from expo for building and publishing apps in app stores app.json to be configured, details config can be found here for environment variables check here for icons and splashscreen go here read this to build an app with expo service check this video about app distribution with Expo, quite long and boring process to publish ios version without expo follow this link to publish android version without expo follow this link Local notifications Local notifications are sent from this device to this device Such notifications can be scheduled Can be utilized for ex. in alarm, reminder or to-do apps, etc... https://docs.expo.dev/versions/latest/sdk/notifications/ npx expo install expo-notifications Add some configuration from the link into the app.json Explanation of content prop https://docs.expo.dev/versions/latest/sdk/notifications/#notificationcontentinput Explanation of trigger prop https://docs.expo.dev/versions/latest/sdk/notifications/#notificationtriggerinput // app.json { "expo": { "name": "RNCourse", "slug": "RNCourse", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#ffffff" }, "updates": { "fallbackToCacheTimeout": 0 }, "assetBundlePatterns": [ "**/*" ], "ios": { "supportsTablet": true }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#FFFFFF" } }, "web": { "favicon": "./assets/favicon.png" }, "plugins": [ [ "expo-notifications", { "icon": "./local/assets/icon.png", "color": "#ffffff" } ] ] } } // App.js import { useEffect } from 'react'; import { StatusBar } from 'expo-status-bar'; import { StyleSheet, Button, View } from 'react-native'; import * as Notifications from 'expo-notifications'; Notifications.setNotificationHandler({ handleNotification: async () => { return { shouldPlaySound: false, shouldSetBadge: false, shouldShowAlert: true }; } }); export default function App() { useEffect(() => { const subscription1 = Notifications.addNotificationReceivedListener((notification) => { console.log('NOTIFICATION RECEIVED'); console.log(notification); const userName = notification.request.content.data.userName; console.log(userName); }); const subscription2 = Notifications.addNotificationResponseReceivedListener((response) => { console.log('NOTIFICATION RESPONSE RECEIVED'); console.log(response); const userName = response.notification.request.content.data.userName; console.log(userName); }); return () => { subscription1.remove(); subscription2.remove(); }; }, []); function scheduleNotificationHandler() { Notifications.scheduleNotificationAsync({ content: { title: 'My first local notification', body: 'This is the body of the notification.', data: { userName: 'Max' } }, trigger: { seconds: 5 } }); } return ( <View style={styles.container}> <Button title="Schedule Notification" onPress={scheduleNotificationHandler} /> <StatusBar style="auto" /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, }); Push notifications Push notifications are sent from the backend to devices Actually backend sends notifications to google / apple push notification services and they distribute messages further https://docs.expo.dev/push-notifications/overview/ In the example below we test sending push notification right from the device, but in reality it should be done from some lambda function This is a bit trickier, take a closer look later ... import { useEffect } from 'react'; import { StatusBar } from 'expo-status-bar'; import { StyleSheet, Button, View, Alert, Platform } from 'react-native'; import * as Notifications from 'expo-notifications'; Notifications.setNotificationHandler({ handleNotification: async () => { return { shouldPlaySound: false, shouldSetBadge: false, shouldShowAlert: true, }; }, }); export default function App() { useEffect(() => { async function configurePushNotifications() { const { status } = await Notifications.getPermissionsAsync(); let finalStatus = status; if (finalStatus !== 'granted') { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } if (finalStatus !== 'granted') { Alert.alert( 'Permission required', 'Push notifications need the appropriate permissions.' ); return; } const pushTokenData = await Notifications.getExpoPushTokenAsync(); console.log(pushTokenData); if (Platform.OS === 'android') { Notifications.setNotificationChannelAsync('default', { name: 'default', importance: Notifications.AndroidImportance.DEFAULT, }); } } configurePushNotifications(); }, []); useEffect(() => { const subscription1 = Notifications.addNotificationReceivedListener( (notification) => { console.log('NOTIFICATION RECEIVED'); console.log(notification); const userName = notification.request.content.data.userName; console.log(userName); } ); const subscription2 = Notifications.addNotificationResponseReceivedListener( (response) => { console.log('NOTIFICATION RESPONSE RECEIVED'); console.log(response); const userName = response.notification.request.content.data.userName; console.log(userName); } ); return () => { subscription1.remove(); subscription2.remove(); }; }, []); function scheduleNotificationHandler() { Notifications.scheduleNotificationAsync({ content: { title: 'My first local notification', body: 'This is the body of the notification.', data: { userName: 'Max' }, }, trigger: { seconds: 5, }, }); } function sendPushNotificationHandler() { fetch('https://exp.host/--/api/v2/push/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to: '<Your Device Push Token>]', title: 'Test - sent from a device!', body: 'This is a test!' }) }); } return ( <View style={styles.container}> <Button title="Schedule Notification" onPress={scheduleNotificationHandler} /> <Button title="Send Push Notification" onPress={sendPushNotificationHandler} /> <StatusBar style="auto" /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, });