/
๐Ÿ“”

React Native

react-native
Table of contents

Getting Started

// install
sudo npm install expo-cli --global
// start new project, can choose typescript template
expo init project-name
cd project-name
// install typescript types later
npm install --save-dev typescript @types/jest @types/react @types/react-native @types/react-test-renderer
npm start

Styling is done via JS. CSS-like property names are offered by RN.

Install Android Studio to emulate development on the pc.

  1. Download from: developer.android.com/st...dio
  2. Follow installation instructions
  3. Once installed, configure SDK Manager
  4. Install appropriate SDK platforms
  5. Install SDK Tools
    1. Android SDK Built-in Tools
    2. Android Emulator
    3. Android SDK Platform Tools
    4. Google Play Services
  6. 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
  7. To start expo in the emulator, run expo project and press 'a'
  8. 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:

jsx
<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.

jsx
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'.

jsx
// inline styling
<View>
<View style={styles.screen}>
<TextInput
placeholder="Enter Goal"
style={{
borderBottomColor: "black",
borderBottomWidth: 1,
}}
/>
</View>
</View>
// stylesheet object
const styles = StyleSheet.create({
screen: {
padding: 50,
},
});

Adding fonts

Add fonts into a dedicated folder (./assets/fonts).

jsx
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 <AppLoading
startAsync={fetchFonts}
onFinish={() => setDataLoaded(true)}
onError={(err) => console.log(err)}
></AppLoading>;
}
}

Adding icons

jsx
import { Ionicons } from "@expo/vector-icons";
<Ionicons name="md-remove" size={24} color="white"/>

Setting Global Styles

  1. Can create a custom component wrapper with the desired styled attached. (i.e. <BodyText>)
  2. Have a globally managed StyleSheet that you import into a component and apply where needed.
jsx
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

jsx
import { StyleSheet, View, Image } from "react-native";
const GameOverScreen: React.FC<Props> = () => {
return (
<View style={styles.imageContainer}>
<Image
source={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.

jsx
const styles = StyleSheet.create({
button: {
width: Dimensions.get('window').width / 4,
// note, can also apply conditional styles in render
marginTop: 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:

jsx
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:

jsx
<ScrollView>
<KeyboardAvoidingView
behavior="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.

jsx
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>
// OR
let 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.

Install with this command:

cli
npm install react-navigation
expo 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:

jsx
// in top component
import { 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.

jsx
// example: https://medium.com/@vdelacou/add-react-navigation-to-react-native-typescript-app-d1cf855b3fe7
// ROOT NAVIGATOR
import { 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 screens
const 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 navigator
export default createAppContainer(MealNavigator);
// SCREEN COMPONENT
import 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>
<Button
title="Go To Details!"
onPress={() =>
// alternative syntax
// props.navigation.navigate('SomeIdentifier');
// can also use push, pop, replace, popToTop, goBack
props.navigation.navigate({ routeName: "MealDetail" })
}
></Button>
<Button
title="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:

jsx
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()).

  1. Install react-navigation-header-buttons package
  2. Use the button:
jsx
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}>
<Item
title="Favorite"
iconName="ios-star"
onPress={() => {}}
></Item>
</HeaderButtons>
),
};
};

Tabs Navigation

jsx
const MealsFavTabNavigator = createBottomTabNavigator({
Meals: {
screen: MealNavigator,
navigationOptions: {
tabBarIcon: (tabInfo) => {
return (
<Ionicons
name="ios-restaurant"
size={25}
color={tabInfo.tintColor}
></Ionicons>
);
},
},
},
Favorites: {
screen: FavoritesScreen,
navigationOptions: {
tabBarLabel: "Favorites!",
tabBarIcon: (tabInfo) => {
return (
<Ionicons
name="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

jsx
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

jsx
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}>
<Item
title="Menu"
iconName="ios-menu"
onPress={() => navData.navigation.toggleDrawer()}
></Item>
</HeaderButtons>
),
headerRight: () => (
<HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
<Item
title="Save"
iconName="ios-save"
onPress={navData.navigation.getParam("save")}
></Item>
</HeaderButtons>
),
};
};

Store Management

Setting up Redux

cli
npm install redux react-redux

Create store folder => reducers + actions folders

Create a reducer:

jsx
import { MEALS } from "../../../data/data"
const initialState = {
meals: MEALS,
filteredMeals: MEALS,
favoriteMeals: []
}
const mealsReducer = (state = initialState, action) {
return state
}

Create store

jsx
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>
);
Want to make your own site like this? Try gatsby-theme-code-notes by Zander Martineau.
A starter for gatsby-theme-code-notes