React Native
Fundamentals
- if using the development server, shake your device to open the Developer menu (or use Cmd-D for iOS simulator and Cmd-M for Android)
- to bypass Fast Refresh and force remounting on every edit (for example, testing an intro animation) add
// @refresh reset
anywhere in the file - to debug remotely, select Debug JS Remotely from the developer menu
- React DevTools extension doesn't work, but the standalone version does
- the component selected in the React devtools is available in the Chrome console as
$r
(make sure the Chrome console dropdown saysdebuggerWorker.js
) - for the iOS version use the native Safari debugging under Develop -> Simulator -> JSContext
- choose Automatically Show Web Inspectors for JSContexts
- display console logs by running
npx react-native log-ios
(orlog-android
)- Expo projects will automatically show logs in the terminal
- use
__DEV__
to check if running in dev mode
Native Components
<View>
is non-scrolling,<ScrollView>
is scrolling<Text>
is for non-editable text (equivalent to<p>
),<TextInput>
is an editable text field- the accessible prop on a component groups all its children together as one element for accessibility purposes
Text Input
<TextInput>
has onChangeText and onSubmitEditing props- set multiline for multi-line input
- use onChangeText to update state when the text changes
- use enterKeyHint to set the label on the software Enter key to one of a few preset values
- enablesReturnKeyAutomatically will disable the Enter key if no text has been entered
- onSubmitEditing will fire when the Enter key (software or hardware) is pressed
Using a ScrollView
- set horizontal property to allow horizontal scrolling
- pagingEnabled lets you use paging
<ViewPager>
component can be used to swipe horizontally between views
- on iOS a
<ScrollView>
with a single item can allow the user to zoom in and out if the minimumZoomScale and maximumZoomScale props are set - all elements are rendered even if off screen - if there are a large number of items use
<FlatList>
instead (see #Using List Views)
Using List Views
<FlatList>
displays a scrolling list of changing, but structurally similar data- data prop is an array of data, renderItem prop is a function that takes a single data item and returns the rendered component
- number of items can change over time
- only renders elements that are currently visible
<SectionList>
lets you render a list broken into sections with section headers- takes a sections prop which is an array of objects (one per section), with a title property (the section title) and data property (array of data)
- renderItem prop as described above
Design
Layout
- style properties are written with camelCase
- all dimensions are unitless, density-independent pixels (or percent)
position
can be relative (default) or absolute- all elements use flexbox, with a few differences:
flex
only accepts a single number (flex: 2
in React Native is equivalent toflex: 2 1 0%
on the web)flexDirection
defaults to columnalignContent
defaults to flex-startflexShrink
defaults to 0
StyleSheet
StyleSheet.create(styleObject)
creates a stylesheet object that is reused between renders (inline styles are recreated on every render)- styles should be created outside of the render function
const App = () => (
<View style={styles.container}>
<Text style={styles.title}>React Native</Text>
</View>
);
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 24,
backgroundColor: "#eaeaea"
},
title: {
marginTop: 16,
paddingVertical: 8,
borderWidth: 4,
borderColor: "#20232a",
borderRadius: 6,
backgroundColor: "#61dafb",
color: "#20232a",
textAlign: "center",
fontSize: 30,
fontWeight: "bold"
}
});
StyleSheet.compose(style1, style2)
method lets you combine two StyleSheets so that the second overrides the first
const styles1 = StyleSheet.create({
button: {
backgroundColor: 'blue',
color: 'white'
}
})
const styles2 = StyleSheet.create({
button: {
color: 'yellow'
}
})
const composedStyles = StyleSheet.compose(styles1, styles2)
// creates the following:
{
button: {
backgroundColor: 'blue',
color: 'yellow',
}
}
StyleSheet.flatten(styleObjects)
takes an array of style objects and returns a flattened object that can be used in thestyle
prop
const styles1 = StyleSheet.create({
button: {
backgroundColor: 'blue',
color: 'white'
}
})
const styles2 = StyleSheet.create({
button: {
color: 'yellow'
}
})
const flattenedStyle = StyleSheet.flatten([styles1.button, styles2.button])
// creates the following:
{
backgroundColor: 'blue',
color: 'yellow',
}
StyleSheet.absoluteFill
: a premade style object that setsposition:absolute
andleft|right|top|bottom: 0
<View style={[styles.background, StyleSheet.absoluteFill]}>Hello World</View>
StyleSheet.absoluteFillObject
: same as above, but returns an object that can be destructured within aStyleSheet.create
call
const styles = StyleSheet.create({
background: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'deeppink',
}
})
StyleSheet.hairlineWidth
- the width of a thin line on the current platform
const styles = StyleSheet.create({
listItem: {
borderBottomColor: 'gray',
borderBottomWidth: StyleSheet.hairlineWidth,
}
})
Images
- To use an image (or other asset), require it with a relative path
- image path must be known statically
// GOOD
<Image source={require('./my-icon.png')} />;
// BAD
var icon = this.props.active
? 'my-icon-active'
: 'my-icon-inactive';
<Image source={require('./' + icon + '.png')} />;
// GOOD
var icon = this.props.active
? require('./my-icon-active.png')
: require('./my-icon-inactive.png');
<Image source={icon} />;
- source can also take an object with a
uri
property - this can be a URL, asset catalog name, ordata:
URL - Use
@2x
and@3x
suffixes for different screen densities (ex.my-icon@2x.png
) - images imported with require() are automatically sized, others need to be sized manually
- to scale a require() image dynamically, may need to manually style it with
{ width: undefined, height: undefined }
- to scale a require() image dynamically, may need to manually style it with
<ImageBackground>
component takes the same props as<Image>
but can have children
Colors
- use the
PlatformColor
function to get platform-specific named colors (iOS/Android) which react to light & dark mode- see here for valid color names
- first value is the default, rest are fallbacks
- doesn't work on web
- always set a default value with Platform.select to handle other platforms
const styles = StyleSheet.create({
label: {
padding: 16,
...Platform.select({
ios: {
color: PlatformColor('label'),
backgroundColor:
PlatformColor('systemTealColor'),
},
android: {
color: PlatformColor('?android:attr/textColor'),
backgroundColor:
PlatformColor('@android:color/holo_blue_bright'),
},
default: { color: 'black' }
})
},
Networking
- Fetch is available, or libraries based on XMLHttpRequest can be used
- no CORS
- WebSockets are supported
Touch (buttons)
<Button>
is a simple text-only button on iOS, and a button with colored background on Android and web- doesn't accept
style
, thecolor
prop can be used to change the text (iOS) or background (Android/web) color
- doesn't accept
- For custom buttons, wrap a single element with a
Touchable
wrapper:<TouchableHighlight>
darkens when you tap (suitable for most buttons)- change the highlight color with the
underlayColor
prop
- change the highlight color with the
<TouchableOpacity>
reduces opacity when you tap<TouchableNativeFeedback>
(Android only) does the Material Design ripple<TouchableWithoutFeedback>
provides no feedback
<Pressable>
is more flexible- supports an optional hitSlop property (Rect or number) to allow presses outside the child element
style
can take a function that receives a boolean indicating if the button is currently being pressed
<Button>
only supportsonPress
, the others support the handlers below
Navigation
react-navigation
- install
@react-navigation/native
,@react-navigation/native-stack
,react-native-screens
,react-native-safe-area-context
- wrap whole app in
<NavigationContainer>
// In App.js in a new project
import * as React from 'react';
import { View, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
function HomeScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
}
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} options={{ title: 'Overview' }} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
<Stack.Screen>
is a screen of your app, takes a name prop and a component prop for the page component- route name casing doesn't matter
- component receives a prop called navigation with navigation methods, ex.
navigation.navigate
to go to another screen - to pass additional props to a screen, wrap the Navigator with a context provider and access it inside the screen components
Safe areas
- use the react-native-safe-area-context library (already installed in the default Expo project template)
- use
<SafeAreaView>
(which is a view with padding already applied for safe areas) instead of<View>
, or use theuseSafeAreaInsets
hook to get the inset values
Platform-specific code
Platform.OS
can be used to test for platform (ios' | 'android' | 'windows' | 'macos' | 'web'
) for Expo
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
height: Platform.OS === 'ios' ? 200 : 100
});
Platform.select
method returns the correct input value based on thePlatform.OS
value - also supports the value'native'
for iOS and Android
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
...Platform.select({
ios: {
backgroundColor: 'red'
},
android: {
backgroundColor: 'green'
},
default: {
// other platforms, web for example
backgroundColor: 'blue'
}
})
}
});
/* can also be used to return platform-specific components */
const Component = Platform.select({
ios: () => require('ComponentIOS'),
android: () => require('ComponentAndroid')
})();
<Component />;
Platform.Version
returns the Android API version number (ex.25
) or iOS version string (ex.'10.3'
)- Create platform-specific imports by ending them in
.ios.js
or.android.js
- Use the
.native.js
extension for files that are the same on Android and iOS, but not used on web, and use.js
for the web version
- Use the
// imports BigButton.ios.js or BigButton.android.js depending on platform
import BigButton from './BigButton';
React Native Web
<Text>
renders to different HTML elements based onrole
andaria-*
props- Use
role="heading"
to render<h1>
-<h6>
- To change heading level use
aria-level={number}
(default is1
)
- To change heading level use
- To render links set the
href
prop, and use thehrefAttrs
prop to pass an object with link attributes (rel
,target
, etc)
- Use
<View>
does the samearticle
andparagraph
render their respective HTML elements- Use
banner
for<header>
andcontentinfo
for<footer>
<View>
uses a flex column layout by default
Warning
Default browser styles for HTML elements are not applied!
<Text role="heading">All Articles</Text>
<View role="article">
<Text role="heading" aria-level={2}>Article Title</Text>
<Text role="paragraph">Article text goes here.</Text>
<Text href="https://react.dev/">Link to React</Text>
</View>
- Renders to:
<h1>All Articles</h1>
<article>
<h2>Article Title</h2>
<p>Article text goes here.</p>
<a href="https://react.dev/">Link to React</a>
</article>
Expo
Create a new project:
npx create-expo-app@latest
- install Expo Tools for VSCode
- run
npx expo-doctor
to diagnose issues - run
npx expo lint
to set up ESLint, or run it if it's already set up - to install Prettier:
- run
npx expo install -- --save-dev prettier eslint-config-prettier eslint-plugin-prettier
- add the below to
.eslintrc.js
- run
module.exports = {
extends: ['expo', 'prettier'],
plugins: ['prettier'],
rules: {
'prettier/prettier': 'error',
},
};
Basics
- Expo has its own
<Image>
component (npx expo install expo-image
)
Routing & navigation
- uses #react-navigation, setup is done for you
- file-based routing: all files in the
app
directory become routesapp/index.tsx
is the default routeapp/settings/index.tsx
matches/settings
app/settings/general.tsx
matches/settings/general
app/settings/[page].tsx
matches any unmatched path under/settings
, and the name is available as a route parameter viauseLocalSearchParams
import { useLocalSearchParams } from 'expo-router'
export default function Page() {
const { category } = useLocalSearchParams()
}
- navigate using the
<Link>
component fromexpo-router
- to use a tab bar layout see here
Page options
- to configure page options, add a
<Stack.Screen>
component to the<Stack>
in your layout, with the name prop equal to the route name- you can set a screenOptions prop on
<Stack>
to set default options - you can also set options within a page component using the
useNavigation
hook, however, at least on web they won't take effect until the page fully loads
- you can set a screenOptions prop on
import { useNavigation } from 'expo-router'
const navigation = useNavigation()
useEffect(() => {
navigation.setOptions({ headerShown: false })
}, [navigation])
- change the header title with title: 'title'
- hide the header with headerShown: false
- add buttons to the header by passing a function that returns a component to the headerLeft or headerRight option
- show a route as a modal with presentation: modal
Data storage
- expo-secure-store : store small (<2kb) key-value pairs with encryption
npx expo install expo-secure-store
import * as SecureStore from 'expo-secure-store';
await SecureStore.setItemAsync(key, value);
const result = await SecureStore.getItemAsync(key);
- Async Storage: not encrypted, can hold larger amounts of data (~2 MB per item and 6 MB total)
npx expo install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
await AsyncStorage.setItem('todos', JSON.stringify({ name: 'Buy eggs', complete: false }))
const todos = await JSON.parse(AsyncStorage.getItem('todos') ?? '{}')
- expo-file-system: provides filesystem access
- only have read & write access to
FileSystem.documentDirectory
(for permanent files) andFileSystem.cacheDirectory
(for temporary files)
- only have read & write access to
npx expo install expo-file-system
- expo-sqlite: SQLite database
npx expo install expo-sqlite
Tamagui
Fix "problem connecting to the react-native-css-interop Metro server"
Error when running yarn ios using starters · Issue #2279 · tamagui/tamagui
Current Behavior using npm create tamagui@latest --template expo-router and follow the steps to create a project, then run yarn ios to start development via expo go or expo-dev-client, the same err...
https://github.com/tamagui/tamagui/issues/2279#issuecomment-1967426749
- in the layout, change the import of
tamagui.css
to be conditional for web only- this will cause FOUC, as will light/dark theme switching, so put up some kind of splash screen
import { Platform } from "react-native";
if (Platform.OS === "web") {
import("../tamagui-web.css");
}