You need to enable JavaScript to run this app. 使用NativeBase Picker触发同方向嵌套VirtualizedList报错求助
优惠活动
大模型
产品
解决方案
定价
更多

使用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内部实现的嵌套逻辑,和业务代码无关:

  1. NativeBase的Content组件本质是垂直方向的普通ScrollView
  2. NativeBase的旧版Picker组件打开下拉选项面板时,内部会渲染垂直方向的VirtualizedList承载选项内容
  3. 两个垂直方向的滚动容器嵌套,触发React Native内置的嵌套方向一致性校验警告
  4. 因为内部的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,一并修正:

  1. loadTransactionData方法中,accessToken初始值为null,直接读取this.state.accessToken.length会触发空值报错,需要增加空值判断:
    if (!this.state.accessToken || this.state.accessToken.length === 0) {
        return;
    }
    
  2. 「Load More」按钮的disabled属性引用错误,代码中读取的是未挂载到this上的this.boolLoadMoreBtn(实际值为undefined),应修正为读取状态值:
    disabled={!this.state.boolLoadMoreBtn}
    

内容的提问来源于stack exchange,提问作者Miracle Hades

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.11.05 14:24:03