前言
這裡是針對日常使用react-navigation中的遇到的一些問題對其進行解決而總結出的小技巧。
TabNavigator 和 StackNavigator
簡單瞭解一下 StackNavigator
如其名就是一個棧,遵循先進後出的原則,每開啟一個screen,screen就會在頁面最頂層的位置。
簡單瞭解一下TabNavigator
在初始化TabNavigator的時候就會將TabNavigator上的所有screen都進行初始化。通過左右滑動/點選底部的TabBar對應的icon專案進行切換。
而一個TabNavigator也可以作為一個screen放入到StackNavigator中。
在瞭解了其簡單使用原理後,進入到日常可能會遇到的一些問題。
TabNavigator的子screen缺少一些額外的鉤子
由於TabNavigator會在第一次載入的時候例項化其子screen,所以其所以子screen的componentDidMount()
會隨著TabNavigator例項的時候執行。
於是就有了這樣的需要,子screen在螢幕上的時候才向伺服器請求資料,或者是更新資料等邏輯。
目前react-navigation並沒有提供相關的鉤子去幫助我們(見#51),所以我們就有需要去使用一些高階元件為我們的screen新增一些hook。
如https://github.com/pmachowski/react-navigation-is-focused-hoc
這裡提供一個在整合redux後自己實現的一個方案
import React from `react`
import { connect } from `react-redux`
import PropTypes from `prop-types`
function _getCurrentRouteName(navigationState) {
if (!navigationState) return null
const route = navigationState.routes[navigationState.index]
if (route.routes) return _getCurrentRouteName(route)
return route.routeName
}
/**
* 給當前screen傳遞isFocused以判斷是否在為當前路由
* @param {Component} WrappedComponent
* @param {string} screenName
*/
export default function withNavigationFocus(WrappedComponent, screenName) {
class InnerComponent extends React.Component {
static propTypes = {
nav: PropTypes.object,
}
static navigationOptions = (props) => {
if (typeof WrappedComponent.navigationOptions === `function`) {
return WrappedComponent.navigationOptions(props)
}
return { ...WrappedComponent.navigationOptions }
}
constructor(props) {
super(props)
this.state = {
isFocused: true,
}
}
componentDidMount() {
}
componentWillReceiveProps(nextProps) {
if (nextProps && nextProps.nav) {
this._handleNavigationChange(_getCurrentRouteName(nextProps.nav))
}
}
componentWillUnmount() {
}
_handleNavigationChange = (routeName) => {
// update state only when isFocused changes
if (this.state.isFocused !== (screenName === routeName)) {
this.setState({
isFocused: screenName === routeName,
})
}
}
render() {
return <WrappedComponent isFocused={this.state.isFocused} {...this.props} />
}
}
return connect(mapStateToProps)(InnerComponent)
}
/*將react-navigation整合到redux中*/
const mapStateToProps = (state) => ({
nav: state.nav,
})
複製程式碼
通過丟擲引玉可以為相關的其他鉤子實現提供思路
實現一個自定義的tabbar
為什麼會有這個需求,因為設計師總會各種新(sao)的想(cao)法(zuo),例如issues上看到的這個圖
這裡之前實踐的例子
import React, { Component } from `react`
import {
View,
TouchableOpacity,
Text,
StyleSheet,
Dimensions,
Image,
} from `react-native`
import PropTypes from `prop-types`
import { connect } from `react-redux`
const { width } = Dimensions.get(`window`)
function _getCurrentRouteName(navigationState) {
if (!navigationState) return null
const route = navigationState.routes[navigationState.index]
if (route.routes) return _getCurrentRouteName(route)
return route.routeName
}
const extraRoutes = [{
routeName: `InsuranceTimeline`,
defaultIcon: require(`../../assets/tabbar-icon/verify-icon/ic_circle_n.png`),
selectIcon: require(`../../assets/tabbar-icon/verify-icon/ic_circle_s.png`),
title: `screen2`,
}, {
routeName: `InsuranceLesson`,
defaultIcon: require(`../../assets/tabbar-icon/verify-icon/ic_umbrella_n.png`),
selectIcon: require(`../../assets/tabbar-icon/verify-icon/ic_umbrella_s.png`),
title: `sreen2`,
}]
class TabBar extends Component {
static defaultProps = {
activeTintColor: `#3478f6`, // Default active tint color in iOS 10
activeBackgroundColor: `transparent`,
inactiveTintColor: `#929292`, // Default inactive tint color in iOS 10
inactiveBackgroundColor: `transparent`,
showLabel: true,
showIcon: true,
}
static propTypes = {
activeTintColor: PropTypes.string,
inactiveTintColor: PropTypes.string,
navigation: PropTypes.object,
showPromoteModal: PropTypes.func,
nav: PropTypes.object.isRequired,
}
renderExtraTabBtns = (props) => {
const { navigation } = this.props
const {
activeTintColor,
inactiveTintColor,
} = this.props
const imageType = isActive ? `selectedIcon` : `defaultIcon`
const color = isActive ? activeTintColor : inactiveTintColor
const isActive = _getCurrentRouteName(navigation.state) == props.name
return <TouchableOpacity
onPress={() => {
navigation.navigate(props.routeName)
}}
style={styles.tab}
key={props.routeName}
>
<Image
source={props[imageType]}
style={styles.icon}
/>
<Text style={{ color, fontSize: 10 }}>{props.title}</Text>
</TouchableOpacity>
}
render() {
let navigation = this.props.navigation
let images = [
{
default: require(`../../assets/tabbar-icon/verify-icon/ic_home_n.png`),
selected: require(`../../assets/tabbar-icon/verify-icon/ic_home_s.png`),
},
{
default: require(`../../assets/tabbar-icon/verify-icon/ic_mine_n.png`),
selected: require(`../../assets/tabbar-icon/verify-icon/ic_mine_s.png`),
},
]
let titles = [
`screen1`,
`screen2`,
]
const { routes, index } = navigation.state
const {
activeTintColor,
inactiveTintColor,
} = this.props
const tabBtns = routes.map((route, idx) => {
const color = (index === idx) ? activeTintColor : inactiveTintColor
const isActive = index === idx
const imageType = isActive ? `selected` : `default`
return (
<TouchableOpacity
onPress={() => {
navigation.navigate(route.routeName)
}}
style={styles.tab}
key={route.routeName}
>
<Image
source={images[idx][imageType]}
style={styles.icon}
/>
<Text style={{ color, fontSize: 10 }}>{titles[idx]}</Text>
</TouchableOpacity>
)
})
const extraBtns = extraRoutes.map(route => (
this.renderExtraTabBtns(route)
))
return (
<View style={styles.tabContainer}>
{
[...tabBtns.slice(0, 1),
...extraBtns
, ...tabBtns.slice(1)]
}
</View>
)
}
}
const styles = StyleSheet.create({
tabContainer: {
borderTopWidth: 1,
borderTopColor: `#e6e6e6`,
position: `relative`,
flexDirection: `row`,
width,
backgroundColor: `#fff`,
// borderTopColor: theme.primaryColor
},
tab: {
flex: 1,
alignItems: `center`,
justifyContent: `center`,
height: 55,
},
icon: {
width: 30,
height: 30,
},
})
const mapStateToProps = (state) => ({
nav: state.nav,
})
export default connect(mapStateToProps)(TabBar)
複製程式碼
stackNavigator登入後通過狀態重新整理screen
由於登入後一般是將登入頁reset即將上面的screen直接出棧,而下面的screen是直接呈現在頁面上面,而在單一資料流,有時候沒有觸發其重新整理的資料,此時就通過加一個高階函式
import React, { Component } from `react`
import { ScrollView, RefreshControl } from `react-native`
import { connect } from `react-redux`
import PropTypes from `prop-types`
/**
*
* @param {Component} WrappedComponent 需要套的高階元件
* @param {Array} extraKeys 需要更新資料的額外key
* 連線了redux中的user
* 通過受控元件的fetchData更新資料
* (可以傳入extraKeys)
*/
const AuthComponent = (WrappedComponent, extraKeys = [], scroll = false) => {
class InnerComponent extends Component {
static navigationOptions = (props) => {
if (typeof WrappedComponent.navigationOptions === `function`) {
return WrappedComponent.navigationOptions(props)
}
return { ...WrappedComponent.navigationOptions }
}
static propTypes = {
fetchData: PropTypes.func,
user: PropTypes.object,
}
constructor(props) {
super(props)
this.state = {
refreshing: false,
}
}
/**
* 執行獲取資料(子控制元件通過這個方法重新整理資料)
*/
fetchData = (nextProps) => {
if (this.wrappedComponent.fetchData) {
//傳遞下一次props
this.wrappedComponent.fetchData(nextProps)
}
}
componentDidMount = () => {
this.fetchData()
}
componentWillReceiveProps(nextProps) {
if (nextProps.user.isLogin !== this.props.user.isLogin) {
if (nextProps) {
this.fetchData(nextProps)
} else {
this.fetchData()
}
}
for (let i = 0; i < extraKeys.length; i++) {
let key = extraKeys[i]
if (this.props.hasOwnProperty(key)) {
//只支援淺比較
if (nextProps[key] !== this.props[key]) {
this.fetchData(nextProps)
break
}
}
}
}
/**
* 比對前後屬性
*/
compare = (now, next) => {
if (Array.isArray(now) && Array.isArray(next)) {
if (now.length !== next.length) {
return false
}
return now.every((element, index) => {
return now[index] === next[index]
})
}
}
_onRefresh = () => {
}
render() {
if (scroll) {
return <ScrollView
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this._onRefresh}
/>
}
>
<WrappedComponent
ref={(wrappedComponent) => this.wrappedComponent = wrappedComponent}
{...this.props}
/>
</ScrollView>
}
return <WrappedComponent
ref={(wrappedComponent) => this.wrappedComponent = wrappedComponent}
{...this.props}
/>
}
}
return connect(mapStateToProps)(InnerComponent)
}
const mapStateToProps = (state) => ({
user: state.user,
})
export default AuthComponent
複製程式碼
重置 stack狀態
在上面提到了重置stack狀態
const resetAction = NavigationActions.reset({
index: 1,
actions: [
NavigationActions.navigate({ routeName: `screen1` }),//重置後的第一層screen
NavigationActions.navigate({ routeName: `screen2` }),//重置後的第二層screen(此時是棧頂,即為當前螢幕顯示頁面)
],
})
this.props.navigation.dispatch(resetAction)
複製程式碼
替換screen
this.props.dispatch({
key: `NearMeMap`,
type: `ReplaceCurrentScreen`,
routeName: routeName,
})
複製程式碼