Skip to content
This repository has been archived by the owner on Jan 13, 2023. It is now read-only.

Commit

Permalink
feat(navigation): Simplifies navigation, adds helpful comments (#338 by
Browse files Browse the repository at this point in the history
@jamonholmgren)

[skip ci]
  • Loading branch information
jamonholmgren committed May 21, 2020
1 parent 928381c commit 4abb814
Show file tree
Hide file tree
Showing 24 changed files with 275 additions and 236 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
defaults: &defaults
docker:
# Choose the version of Node you want here
- image: circleci/node:11.10.1
- image: circleci/node:13.8.0
working_directory: ~/repo

mac: &mac
Expand Down
116 changes: 32 additions & 84 deletions boilerplate/app/app.tsx.ejs
Original file line number Diff line number Diff line change
@@ -1,83 +1,55 @@
// Welcome to the main entry point of the app.
//
// In this file, we'll be kicking off our app or storybook.

/**
* Welcome to the main entry point of the app. In this file, we'll
* be kicking off our app or storybook.
*
* Most of this file is boilerplate and you shouldn't need to modify
* it very often. But take some time to look through and understand
* what is going on here.
*
* The app navigation resides in ./app/navigation, so head over there
* if you're interested in adding screens and navigators.
*/
import "./i18n"
import React, { useState, useEffect, useRef } from "react"
import { YellowBox } from "react-native"
import "./utils/ignore-warnings"
import React, { useState, useEffect, useRef, FunctionComponent as Component } from "react"
import { NavigationContainerRef } from "@react-navigation/native"
import { SafeAreaProvider, initialWindowSafeAreaInsets } from "react-native-safe-area-context"
<% if (props.useExpo) { -%>
import { initFonts } from "./theme/fonts"
<% } -%>
import { contains } from "ramda"
import { enableScreens } from "react-native-screens"
import { SafeAreaProvider, initialWindowSafeAreaInsets } from "react-native-safe-area-context"

import { RootNavigator, exitRoutes, setRootNavigation } from "./navigation"
import { useBackButtonHandler } from "./navigation/use-back-button-handler"
import { RootStore, RootStoreProvider, setupRootStore } from "./models/root-store"
import * as storage from "./utils/storage"
import getActiveRouteName from "./navigation/get-active-routename"
import {
useBackButtonHandler,
RootNavigator,
canExit,
setRootNavigation,
useNavigationPersistence,
} from "./navigation"
import { RootStore, RootStoreProvider, setupRootStore } from "./models/root-store"

// This puts screens in a native ViewController or Activity. If you want fully native
// stack navigation, use `createNativeStackNavigator` in place of `createStackNavigator`:
// https://github.com/kmagiera/react-native-screens#using-native-stack-navigator
import { enableScreens } from 'react-native-screens';
enableScreens()

/**
* Ignore some yellowbox warnings. Some of these are for deprecated functions
* that we haven't gotten around to replacing yet.
*/
YellowBox.ignoreWarnings([
"componentWillMount is deprecated",
"componentWillReceiveProps is deprecated",
"Require cycle:",
])

/**
* Are we allowed to exit the app? This is called when the back button
* is pressed on android.
*
* @param routeName The currently active route name.
*/
const canExit = (routeName: string) => contains(routeName, exitRoutes)

export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE"

/**
* This is the root component of our app.
*/
const App: React.FunctionComponent<{}> = () => {
const App: Component<{}> = () => {
const navigationRef = useRef<NavigationContainerRef>()
const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined)
const [initialNavigationState, setInitialNavigationState] = useState()
const [isRestoringNavigationState, setIsRestoringNavigationState] = useState(true)

setRootNavigation(navigationRef)
useBackButtonHandler(navigationRef, canExit)
const { initialNavigationState, onNavigationStateChange } = useNavigationPersistence(
storage,
NAVIGATION_PERSISTENCE_KEY,
)

/**
* Keep track of state changes
* Track Screens
* Persist State
*/
const routeNameRef = useRef()
const onNavigationStateChange = state => {
const previousRouteName = routeNameRef.current
const currentRouteName = getActiveRouteName(state)

if (previousRouteName !== currentRouteName) {
// track screens.
__DEV__ && console.tron.log(currentRouteName)
}

// Save the current route name for later comparision
routeNameRef.current = currentRouteName

// Persist state to storage
storage.save(NAVIGATION_PERSISTENCE_KEY, state)
}

// Kick off initial async loading actions, like loading fonts and RootStore
useEffect(() => {
(async () => {
<% if (props.useExpo) { -%>
Expand All @@ -87,35 +59,11 @@ const App: React.FunctionComponent<{}> = () => {
})()
}, [])

useEffect(() => {
const restoreState = async () => {
try {
const state = await storage.load(NAVIGATION_PERSISTENCE_KEY)

if (state) {
setInitialNavigationState(state)
}
} finally {
setIsRestoringNavigationState(false)
}
}

if (isRestoringNavigationState) {
restoreState()
}
}, [isRestoringNavigationState])

// Before we show the app, we have to wait for our state to be ready.
// In the meantime, don't render anything. This will be the background
// color set in native by rootView's background color.
//
// This step should be completely covered over by the splash screen though.
//
// You're welcome to swap in your own component to render if your boot up
// sequence is too slow though.
if (!rootStore) {
return null
}
// color set in native by rootView's background color. You can replace
// with your own loading component if you wish.
if (!rootStore) return null

// otherwise, we're ready to render the app
return (
Expand Down
4 changes: 2 additions & 2 deletions boilerplate/app/components/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from "react"
import React, { FunctionComponent as Component } from "react"
import { View, ViewStyle, TextStyle } from "react-native"
import { HeaderProps } from "./header.props"
import { Button } from "../button/button"
Expand All @@ -24,7 +24,7 @@ const RIGHT: ViewStyle = { width: 32 }
/**
* Header that appears on many screens. Will hold navigation buttons and screen title.
*/
export const Header: React.FunctionComponent<HeaderProps> = props => {
export const Header: Component<HeaderProps> = props => {
const {
onLeftPress,
onRightPress,
Expand Down
4 changes: 2 additions & 2 deletions boilerplate/app/components/switch/switch.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from "react"
import React, { FunctionComponent as Component } from "react"
import { ViewStyle, Animated, Easing, TouchableWithoutFeedback } from "react-native"
import { color } from "../../theme"
import { SwitchProps } from "./switch.props"
Expand Down Expand Up @@ -52,7 +52,7 @@ const enhance = (style, newStyles): any => {

const makeAnimatedValue = switchOn => new Animated.Value(switchOn ? 1 : 0)

export const Switch: React.FunctionComponent<SwitchProps> = props => {
export const Switch: Component<SwitchProps> = props => {
const [timer] = React.useState<Animated.Value>(makeAnimatedValue(props.value))
const startAnimation = React.useMemo(
() => (newValue: boolean) => {
Expand Down
4 changes: 2 additions & 2 deletions boilerplate/app/components/text-field/text-field.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from "react"
import React, { FunctionComponent as Component } from "react"
import { View, TextInput, TextStyle, ViewStyle } from "react-native"
import { color, spacing, typography } from "../../theme"
import { translate } from "../../i18n"
Expand Down Expand Up @@ -32,7 +32,7 @@ const enhance = (style, styleOverride) => {
/**
* A component which has a label and an input together.
*/
export const TextField: React.FunctionComponent<TextFieldProps> = props => {
export const TextField: Component<TextFieldProps> = props => {
const {
placeholderTx,
placeholder,
Expand Down
13 changes: 0 additions & 13 deletions boilerplate/app/navigation/get-active-routename.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion boilerplate/app/navigation/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from "./primary-navigator"
export * from "./root-navigator"
export * from "./navigation-service"
export * from "./navigation-utilities"
23 changes: 0 additions & 23 deletions boilerplate/app/navigation/navigation-service.tsx

This file was deleted.

127 changes: 127 additions & 0 deletions boilerplate/app/navigation/navigation-utilities.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, { useState, useEffect, useRef } from "react"
import { BackHandler } from "react-native"
import { PartialState, NavigationState, NavigationContainerRef } from "@react-navigation/native"

export const RootNavigation = {
navigate(name: string) {
name // eslint-disable-line no-unused-expressions
},
goBack() {}, // eslint-disable-line @typescript-eslint/no-empty-function
resetRoot(state?: PartialState<NavigationState> | NavigationState) {}, // eslint-disable-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
getRootState(): NavigationState {
return {} as any
},
}

export const setRootNavigation = (ref: React.RefObject<NavigationContainerRef>) => {
for (const method in RootNavigation) {
RootNavigation[method] = (...args: any) => {
if (ref.current) {
return ref.current[method](...args)
}
}
}
}

/**
* Gets the current screen from any navigation state.
*/
export function getActiveRouteName(state: NavigationState | PartialState<NavigationState>) {
const route = state.routes[state.index]

// Found the active route -- return the name
if (!route.state) return route.name

// Recursive call to deal with nested routers
return getActiveRouteName(route.state)
}

/**
* Hook that handles Android back button presses and forwards those on to
* the navigation or allows exiting the app.
*/
export function useBackButtonHandler(
ref: React.RefObject<NavigationContainerRef>,
canExit: (routeName: string) => boolean,
) {
const canExitRef = useRef(canExit)

useEffect(() => {
canExitRef.current = canExit
}, [canExit])

useEffect(() => {
// We'll fire this when the back button is pressed on Android.
const onBackPress = () => {
const navigation = ref.current

if (navigation == null) {
return false
}

// grab the current route
const routeName = getActiveRouteName(navigation.getRootState())

// are we allowed to exit?
if (canExitRef.current(routeName)) {
// let the system know we've not handled this event
return false
}

// we can't exit, so let's turn this into a back action
if (navigation.canGoBack()) {
navigation.goBack()

return true
}

return false
}

// Subscribe when we come to life
BackHandler.addEventListener("hardwareBackPress", onBackPress)

// Unsubscribe when we're done
return () => BackHandler.removeEventListener("hardwareBackPress", onBackPress)
}, [ref])
}

/**
* Custom hook for persisting navigation state.
*/
export function useNavigationPersistence(storage: any, persistenceKey: string) {
const [initialNavigationState, setInitialNavigationState] = useState()
const [isRestoringNavigationState, setIsRestoringNavigationState] = useState(true)

const routeNameRef = useRef()
const onNavigationStateChange = state => {
const previousRouteName = routeNameRef.current
const currentRouteName = getActiveRouteName(state)

if (previousRouteName !== currentRouteName) {
// track screens.
__DEV__ && console.tron.log(currentRouteName)
}

// Save the current route name for later comparision
routeNameRef.current = currentRouteName

// Persist state to storage
storage.save(persistenceKey, state)
}

const restoreState = async () => {
try {
const state = await storage.load(persistenceKey)
if (state) setInitialNavigationState(state)
} finally {
setIsRestoringNavigationState(false)
}
}

useEffect(() => {
if (isRestoringNavigationState) restoreState()
}, [isRestoringNavigationState])

return { onNavigationStateChange, restoreState, initialNavigationState }
}

0 comments on commit 4abb814

Please sign in to comment.