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

Commit

Permalink
feat(boilerplate): Migrated to functional components, hooks, and cont…
Browse files Browse the repository at this point in the history
…ext (#239 by @bryanstearns)

More details:

* Make new app's package name kebab-case, `const` a `let`, and fix comment grammar
* Move exit route list to primaryNavigator
* Convert class components to functions
* Bump mobx stuff; use context for store provider and convert consumers to functional components
* Bump to react-native 0.60.5
  • Loading branch information
bryanstearns authored and jamonholmgren committed Sep 16, 2019
1 parent 0ae48ff commit 49d0b8a
Show file tree
Hide file tree
Showing 22 changed files with 380 additions and 441 deletions.
9 changes: 5 additions & 4 deletions boilerplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ async function install(context) {
template,
prompt,
patching,
strings,
} = context
const { colors } = print
const { red, yellow, bold, gray, cyan } = colors
Expand Down Expand Up @@ -75,8 +76,7 @@ async function install(context) {
}

// attempt to install React Native or die trying
let rnInstall
rnInstall = await reactNative.install({
const rnInstall = await reactNative.install({
name,
version: getReactNativeVersion(context),
useNpm: !ignite.useYarn,
Expand All @@ -89,6 +89,7 @@ async function install(context) {
".babelrc",
".buckconfig",
".eslintrc.js",
".prettierrc.js",
".flowconfig",
"App.js",
"__tests__",
Expand Down Expand Up @@ -181,11 +182,11 @@ async function install(context) {
* Merge the package.json from our template into the one provided from react-native init.
*/
async function mergePackageJsons() {
// transform our package.json in case we need to replace variables
// transform our package.json so we can replace variables
const rawJson = await template.generate({
directory: `${ignite.ignitePluginPath()}/boilerplate`,
template: "package.json.ejs",
props: templateProps,
props: { ...templateProps, kebabName: strings.kebabCase(templateProps.name) },
})
const newPackageJson = JSON.parse(rawJson)

Expand Down
91 changes: 34 additions & 57 deletions boilerplate/app/app.tsx.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@
// In this file, we'll be kicking off our app or storybook.

import "./i18n"
import * as React from "react"
import React, { useState, useEffect } from "react"
import { AppRegistry, YellowBox } from "react-native"
import { StatefulNavigator } from "./navigation"
import { StorybookUIRoot } from "../storybook"
import { RootStore, setupRootStore } from "./models/root-store"
import { Provider } from "mobx-react"
import { BackButtonHandler } from "./navigation/back-button-handler"
import { RootStore, RootStoreProvider, setupRootStore } from "./models/root-store"
import { BackButtonHandler, exitRoutes } from "./navigation"
import { contains } from "ramda"
import { DEFAULT_NAVIGATION_CONFIG } from "./navigation/navigation-config"

/**
* Ignore some yellowbox warnings. Some of these are for deprecated functions
* that we haven't gotten around to replacing yet.
*/
YellowBox.ignoreWarnings([
YellowBox.ignoreWarnings([
"componentWillMount is deprecated",
"componentWillReceiveProps is deprecated",
])
Expand All @@ -28,69 +26,48 @@ import { DEFAULT_NAVIGATION_CONFIG } from "./navigation/navigation-config"
* points RN's AsyncStorage at the community one, fixing the warning. Here's the
* Storybook issue about this: https://github.com/storybookjs/storybook/issues/6078
*/
const ReactNative = require('react-native')
Object.defineProperty(ReactNative, 'AsyncStorage', {
const ReactNative = require("react-native")
Object.defineProperty(ReactNative, "AsyncStorage", {
get(): any {
return require('@react-native-community/async-storage').default;
},
})

interface AppState {
rootStore?: RootStore
}
/**
* 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)

/**
* This is the root component of our app.
*/
export class App extends React.Component<{}, AppState> {
/**
* When the component is mounted. This happens asynchronously and simply
* re-renders when we're good to go.
*/
async componentDidMount() {
this.setState({
rootStore: await setupRootStore(),
})
}
export const App: React.FunctionComponent<{}> = () => {
const [ rootStore, setRootStore ] = useState<RootStore | undefined>(undefined)
useEffect(() => { setupRootStore().then(setRootStore) }, [])

/**
* 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.
*/
canExit(routeName: string) {
return contains(routeName, DEFAULT_NAVIGATION_CONFIG.exitRoutes)
// 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
}

render() {
const rootStore = this.state && this.state.rootStore

// 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
}

// otherwise, we're ready to render the app

// wire stores defined in root-store.ts file
const { navigationStore, ...otherStores } = rootStore

return (
<Provider rootStore={rootStore} navigationStore={navigationStore} {...otherStores}>
<BackButtonHandler canExit={this.canExit}>
<StatefulNavigator />
</BackButtonHandler>
</Provider>
)
}
// otherwise, we're ready to render the app
return (
<RootStoreProvider value={rootStore}>
<BackButtonHandler canExit={canExit}>
<StatefulNavigator />
</BackButtonHandler>
</RootStoreProvider>
)
}

/**
Expand Down
6 changes: 3 additions & 3 deletions boilerplate/app/components/header/header.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ storiesOf("Header", module)
<Story>
<UseCase noPad text="default" usage="The default usage">
<View style={VIEWSTYLE}>
<Header headerTx="secondExampleScreen.howTo" />
<Header headerTx="demoScreen.howTo" />
</View>
</UseCase>
<UseCase noPad text="leftIcon" usage="A left nav icon">
<View style={VIEWSTYLE}>
<Header
headerTx="secondExampleScreen.howTo"
headerTx="demoScreen.howTo"
leftIcon="back"
onLeftPress={() => Alert.alert("left nav")}
/>
Expand All @@ -33,7 +33,7 @@ storiesOf("Header", module)
<UseCase noPad text="rightIcon" usage="A right nav icon">
<View style={VIEWSTYLE}>
<Header
headerTx="secondExampleScreen.howTo"
headerTx="demoScreen.howTo"
rightIcon="bullet"
onRightPress={() => Alert.alert("right nav")}
/>
Expand Down
65 changes: 32 additions & 33 deletions boilerplate/app/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,39 +24,38 @@ const RIGHT: ViewStyle = { width: 32 }
/**
* Header that appears on many screens. Will hold navigation buttons and screen title.
*/
export class Header extends React.Component<HeaderProps, {}> {
render() {
const {
onLeftPress,
onRightPress,
rightIcon,
leftIcon,
headerText,
headerTx,
titleStyle,
} = this.props
const header = headerText || (headerTx && translate(headerTx)) || ""
export const Header: React.FunctionComponent<HeaderProps> = props => {
const {
onLeftPress,
onRightPress,
rightIcon,
leftIcon,
headerText,
headerTx,
style,
titleStyle,
} = props
const header = headerText || (headerTx && translate(headerTx)) || ""

return (
<View style={{ ...ROOT, ...this.props.style }}>
{leftIcon ? (
<Button preset="link" onPress={onLeftPress}>
<Icon icon={leftIcon} />
</Button>
) : (
<View style={LEFT} />
)}
<View style={TITLE_MIDDLE}>
<Text style={{ ...TITLE, ...titleStyle }} text={header} />
</View>
{rightIcon ? (
<Button preset="link" onPress={onRightPress}>
<Icon icon={rightIcon} />
</Button>
) : (
<View style={RIGHT} />
)}
return (
<View style={{ ...ROOT, ...style }}>
{leftIcon ? (
<Button preset="link" onPress={onLeftPress}>
<Icon icon={leftIcon} />
</Button>
) : (
<View style={LEFT} />
)}
<View style={TITLE_MIDDLE}>
<Text style={{ ...TITLE, ...titleStyle }} text={header} />
</View>
)
}
{rightIcon ? (
<Button preset="link" onPress={onRightPress}>
<Icon icon={rightIcon} />
</Button>
) : (
<View style={RIGHT} />
)}
</View>
)
}
123 changes: 58 additions & 65 deletions boilerplate/app/components/switch/switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,73 +50,66 @@ const enhance = (style, newStyles): any => {
return mergeAll(flatten([style, newStyles]))
}

interface SwitchState {
timer: Animated.Value
}
const makeAnimatedValue = switchOn => new Animated.Value(switchOn ? 1 : 0)

export const Switch: React.FunctionComponent<SwitchProps> = props => {
const [timer] = React.useState<Animated.Value>(makeAnimatedValue(props.value))
const startAnimation = React.useMemo(
() => (newValue: boolean) => {
const toValue = newValue ? 1 : 0
const easing = Easing.out(Easing.circle)
Animated.timing(timer, {
toValue,
duration: DURATION,
easing,
useNativeDriver: true,
}).start()
},
[timer],
)

const [previousValue, setPreviousValue] = React.useState<boolean>(props.value)
React.useEffect(() => {
if (props.value !== previousValue) {
startAnimation(props.value)
setPreviousValue(props.value)
}
}, [props.value])

export class Switch extends React.PureComponent<SwitchProps, SwitchState> {
state = {
timer: new Animated.Value(this.props.value ? 1 : 0),
}
const handlePress = React.useMemo(() => () => props.onToggle && props.onToggle(!props.value), [
props.onToggle,
props.value,
])

startAnimation(newValue: boolean) {
const toValue = newValue ? 1 : 0
const easing = Easing.out(Easing.circle)
Animated.timing(this.state.timer, {
toValue,
duration: DURATION,
easing,
useNativeDriver: true,
}).start()
if (!timer) {
return null
}

componentWillReceiveProps(newProps: SwitchProps) {
if (newProps.value !== this.props.value) {
this.startAnimation(newProps.value)
}
}

/**
* Fires when we tap the touchable.
*/
handlePress = () => this.props.onToggle && this.props.onToggle(!this.props.value)

/**
* Render the component.
*/
render() {
const translateX = this.state.timer.interpolate({
inputRange: [0, 1],
outputRange: [OFF_POSITION, ON_POSITION],
})

const style = enhance({}, this.props.style)

let trackStyle = TRACK
trackStyle = enhance(trackStyle, {
backgroundColor: this.props.value ? ON_COLOR : OFF_COLOR,
borderColor: this.props.value ? BORDER_ON_COLOR : BORDER_OFF_COLOR,
})
trackStyle = enhance(
trackStyle,
this.props.value ? this.props.trackOnStyle : this.props.trackOffStyle,
)

let thumbStyle = THUMB
thumbStyle = enhance(thumbStyle, {
transform: [{ translateX }],
})
thumbStyle = enhance(
thumbStyle,
this.props.value ? this.props.thumbOnStyle : this.props.thumbOffStyle,
)

return (
<TouchableWithoutFeedback onPress={this.handlePress} style={style}>
<Animated.View style={trackStyle}>
<Animated.View style={thumbStyle} />
</Animated.View>
</TouchableWithoutFeedback>
)
}
const translateX = timer.interpolate({
inputRange: [0, 1],
outputRange: [OFF_POSITION, ON_POSITION],
})

const style = enhance({}, props.style)

let trackStyle = TRACK
trackStyle = enhance(trackStyle, {
backgroundColor: props.value ? ON_COLOR : OFF_COLOR,
borderColor: props.value ? BORDER_ON_COLOR : BORDER_OFF_COLOR,
})
trackStyle = enhance(trackStyle, props.value ? props.trackOnStyle : props.trackOffStyle)

let thumbStyle = THUMB
thumbStyle = enhance(thumbStyle, {
transform: [{ translateX }],
})
thumbStyle = enhance(thumbStyle, props.value ? props.thumbOnStyle : props.thumbOffStyle)

return (
<TouchableWithoutFeedback onPress={handlePress} style={style}>
<Animated.View style={trackStyle}>
<Animated.View style={thumbStyle} />
</Animated.View>
</TouchableWithoutFeedback>
)
}
Loading

0 comments on commit 49d0b8a

Please sign in to comment.