使用NativeBase Picker触发同方向嵌套VirtualizedList报错求助
NativeBase Picker触发VirtualizedList嵌套报错的定位与解决
问题场景
在React Native项目中基于NativeBase实现选项选择器,点击选择器时持续抛出「VirtualizedLists should never be nested inside plain ScrollViews with the same orientation」报错。
- 检索过相关技术社区的同类讨论,未找到适配当前场景的有效解法
- 逐行排查业务代码,未显式定义任何VirtualizedList组件
- 附报错截图、完整页面代码供问题定位

完整页面实现代码
import React, { Component } from "react"; import { StyleSheet, TouchableOpacity, RefreshControl, ScrollView, SafeAreaView } from 'react-native'; import CustomHeader from './CustomHeader'; import { Content, Button, H3, Text, Grid, Row, Card, Col, CardItem, Body, Toast, Container, Root, Icon, Item, Picker, Spinner } from 'native-base'; import axios from "axios"; import { ApiURL } from "../util/constant"; import Moment from 'moment'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { backToLogin } from "../util/auth"; class TransactionScreen extends Component { constructor(props) { super(props); this.state = { accessToken: null, transactions: [], selected2: undefined, page: 0, isLoading: true, boolLoadMoreBtn: false, viewType: ['Payment', 'Refund', 'Topup'] } } async getAccessToken() { token = await AsyncStorage.getItem('@access_token'); this.setState({ accessToken: token }); } onValueChange2(value) { this.setState({ selected2: value, page: 0, isLoading: true }); this.loadTransactionData(value, true); } loadTransactionData(whereBy = null, newView = false) { if (this.state.accessToken.length === 0){ return; } const objPayload = {}; if (whereBy != null) { objPayload.whereBy = whereBy; } var page = this.state.page; if (newView === true) { page = 0; } const newPage = parseInt(page) + 1; this.setState({ page: newPage }); axios.post( `${ApiURL}/wallet/transaction?page=${newPage}`, objPayload, { headers: { 'content-type': 'application/json', 'Authorization': `Bearer ${this.state.accessToken}` } } ).then(resp => { this.setState({isLoading: false}); if (resp.status === 200) { const res = resp.data.transaction; if (newPage === 1) { console.log(res); this.setState({ transactions: res, boolLoadMoreBtn: true, isLoading: false }); } else { const r = this.state.transactions; res.map(i => { r.push(i); }) this.setState({ transactions: r, boolLoadMoreBtn: true }); } } else { Toast.show({ text: resp.data.error, buttonText: 'OK' }); return; } }).catch((error) => { this.setState({isLoading: false}); if (error.response) { if (error.response.status === 401) { backToLogin(this.props); } else if (error.response.status === 404){ if (newPage === 1){ this.setState({transactions:[]}); } } else { this.setState({ boolLoadMoreBtn: false }); } } else { Toast.show({ text: error.message, buttonText: 'OK' }); return; } }); } openDetail(transactionId) { this.props.navigation.navigate('TransactionDetail', { transactionId: transactionId, backScene: 'Transaction' }); } componentDidMount() { this._navListener = this.props.navigation.addListener("didFocus", () => { this.getAccessToken().then(() => { this.loadTransactionData(); }); }); } static navigationOptions = () => ({ headerShown: false }) pullMoreData() { const newPage = parseInt(this.state.page) + 1; this.setState({ page: newPage }); this.loadTransactionData(); } render() { return ( <Root> <Container> <CustomHeader isHome={false} navigation={this.props.navigation} backScene="Home" title="Transaction" /> <Content refreshControl={ <RefreshControl //refresh control used for the Pull to Refresh refreshing={this.state.isLoading} onRefresh={() => this.loadTransactionData(this.state.selected2 === undefined ? null : this.state.selected2, true)} /> }> <Item picker> <Picker mode="dropdown" iosIcon={<Icon name="arrow-down" />} style={{ width: undefined }} placeholder="All" Header="Transaction Type" iosHeader="Transaction Type" placeholderStyle={{ color: "#bfc6ea" }} placeholderIconColor="#007aff" selectedValue={this.state.selected2} onValueChange={this.onValueChange2.bind(this)} > <Picker.Item label="Payment" value="Payment" /> <Picker.Item label="Top up" value="Topup" /> <Picker.Item label="Refunded" value="Refunded" /> </Picker> </Item> { this.state.isLoading === true ? <Spinner /> : Object.keys(this.state.transactions).length > 0 ? this.state.transactions.map((transaction, i) => { const dt = Moment(transaction.createdAt).format('L LTS'); return ( <TouchableOpacity key={transaction.id} onPress={() => this.openDetail(transaction.id)}> <Card key={i}> <CardItem key={transaction.id + "1"} style={{ borderBottomWidth: 1, borderColor: "#f8f9fa", backgroundColor: "#f8f9fa" }} header> <Grid> <Row> <Col size={55}><Text style={{ alignSelf: "flex-start" }}>{dt}</Text></Col> <Col size={45}><Text style={{ alignSelf: "flex-end" }}>{transaction.mode}</Text></Col> </Row> </Grid> </CardItem> <CardItem key={transaction.id + "2"}> <Grid> <Row style={{ marginBottom: 5 }}> <Col><H3>{transaction.merchant}</H3></Col> </Row> <Row style={{ marginBottom: 5 }}> <Col><Text style={{ alignSelf: "flex-start" }}>RM {parseFloat(transaction.amount).toFixed(2)}</Text></Col> </Row> <Row> <Col><Text style={{ fontSize: 12 }}>S/N:{transaction.id}</Text></Col> </Row> </Grid> </CardItem> </Card> </TouchableOpacity> ) }) : <Card key={9999}> <CardItem> <Body><Text>No Transaction is available now</Text></Body> </CardItem> </Card> } { this.state.boolLoadMoreBtn === true && ( <Button onPress={() => this.loadTransactionData(this.state.selected2 === undefined ? null : this.state.selected2)} style={{ backgroundColor: "#f8f9fa" }} disabled={(this.boolLoadMoreBtn === true ? true : false)} light block><Text>Load More</Text></Button> ) } </Content> </Container> </Root> ) } }; const styles = StyleSheet.create({ container: { flex: 1 } }); export default TransactionScreen;
根因定位
该报错完全来自NativeBase内部实现的嵌套逻辑,和业务代码无关:
- NativeBase的
Content组件本质是垂直方向的普通ScrollView - NativeBase的旧版
Picker组件打开下拉选项面板时,内部会渲染垂直方向的VirtualizedList承载选项内容 - 两个垂直方向的滚动容器嵌套,触发React Native内置的嵌套方向一致性校验警告
- 因为内部的VirtualizedList完全封装在Picker组件内部,业务代码层看不到显式声明,所以排查时很难直接定位来源
注:该警告不会造成实际功能异常,Picker的下拉面板是浮层渲染,不会和外层Content的滚动逻辑产生冲突,仅会在控制台输出警告信息
可选解决方案
方案1:定向屏蔽警告(最快落地)
在项目入口文件(如index.js)中对该特定警告做定向过滤,不影响其他错误、警告的正常上报:
import { LogBox } from 'react-native'; // 仅屏蔽该条已知的组件内部警告,不影响其他日志输出 LogBox.ignoreLogs(['VirtualizedLists should never be nested inside plain ScrollViews with the same orientation']);
方案2:调整组件层级(从根源消除警告)
将Picker组件从垂直滚动的Content区域移出,放到不参与垂直滚动的固定头部层级:
- 把Picker标签从
Content内部,移到CustomHeader和Content标签之间的固定区域 - 该区域没有同方向的ScrollView嵌套,Picker打开下拉面板时就不会触发校验警告
方案3:替换选择组件(彻底规避依赖)
如果使用NativeBase v3以上版本,可直接替换为新版Select组件,该组件内部已经适配了滚动容器检测逻辑,不会出现该类警告;也可使用react-native-modal-selector等第三方同等功能组件实现选择能力。
现有代码附带的逻辑问题修正
即使解决了警告,现有代码还存在几个会导致功能异常的bug,一并修正:
loadTransactionData方法中,accessToken初始值为null,直接读取this.state.accessToken.length会触发空值报错,需要增加空值判断:if (!this.state.accessToken || this.state.accessToken.length === 0) { return; }- 「Load More」按钮的
disabled属性引用错误,代码中读取的是未挂载到this上的this.boolLoadMoreBtn(实际值为undefined),应修正为读取状态值:disabled={!this.state.boolLoadMoreBtn}
内容的提问来源于stack exchange,提问作者Miracle Hades
相关产品推荐
相关产品推荐

