You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于useEffect实现React Select选项的滚动懒加载以优化性能?

解决Select组件大量选项卡顿:滚动懒加载实现

问题背景

当前代码中,选择/64这类子网掩码时,possibleSubnets会生成海量选项,一次性渲染所有选项会导致页面卡顿甚至无响应。需要实现滚动懒加载:初始加载10个选项,下拉菜单滚动到底部时自动加载更多。

实现步骤

1. 新增状态变量

添加用于维护全量数据、已加载数量和加载状态的变量:

const [allChildSubnets, setAllChildSubnets] = useState([]); // 存储所有计算出的子子网
const [loadedCount, setLoadedCount] = useState(10); // 初始加载10个
const [isLoadingMore, setIsLoadingMore] = useState(false); // 加载更多的状态

2. 修改useEffect计算全量数据

将原逻辑中直接赋值给渲染列表的操作,改为存储全量数据并重置加载计数:

useEffect(() => {
  if (!netmask || !data?.subnet) return;
  
  let calculatedChildSubnets;
  if (isV4) {
    calculatedChildSubnets = possibleSubnets(data.subnet, netmask);
  } else {
    calculatedChildSubnets = v6PossibleSubnets(data.subnet, netmask);
  }
  
  setAllChildSubnets(calculatedChildSubnets);
  setLoadedCount(10); // 每次切换掩码都重置为初始加载10个
  setShowSelect(true);
}, [netmask, supernet, isV4]);

3. 实现滚动加载逻辑

给下拉菜单添加滚动监听,判断滚动位置是否接近底部,触发加载更多:

const handleScroll = (e) => {
  const { scrollTop, clientHeight, scrollHeight } = e.target;
  // 滚动到距离底部20px以内时触发加载
  if (scrollTop + clientHeight >= scrollHeight - 20 && !isLoadingMore && loadedCount < allChildSubnets.length) {
    setIsLoadingMore(true);
    // 模拟异步加载(同步计算可直接更新计数)
    setTimeout(() => {
      setLoadedCount(prev => Math.min(prev + 10, allChildSubnets.length));
      setIsLoadingMore(false);
    }, 200);
  }
};

4. 修改Select组件渲染

  • 只渲染已加载的选项
  • 挂载滚动监听事件
  • 添加加载状态提示
<Select
  dropdownRender={menu => (
    <div onScroll={handleScroll} style={{ maxHeight: '300px', overflow: 'auto' }}>
      {menu}
      {isLoadingMore && <div style={{ textAlign: 'center', padding: '8px' }}>加载中...</div>}
      {!isLoadingMore && loadedCount >= allChildSubnets.length && <div style={{ textAlign: 'center', padding: '8px', color: '#999' }}>已加载全部</div>}
    </div>
  )}
>
  {allChildSubnets.slice(0, loadedCount).map((subnetItem) => (
    <Option key={subnetItem.id} value={subnetItem.id}>
      {subnetItem.networkAddress}
    </Option>
  ))}
</Select>

完整修改后的代码

import React, { useEffect, useState, useRef } from 'react';
import { Row, Col, Button, Form, Input, Space, Alert, Drawer, Divider } from 'antd';
import { useQuery, useMutation } from "@apollo/client";

import { TextInput, Select } from '../../components/pants-d';
import { NetmaskForm } from '../../components/SubnetBuilder';
import { possibleSubnets } from '../../components/SubnetBuilder/V4';
import cidrRegex from 'cidr-regex';
import { possibleSubnets as v6PossibleSubnets } from '../../components/SubnetBuilder/V6';
import debounce from 'lodash/debounce';

const { Option } = Select;

const subnet = require("../../entities/subnet");
const zone = require('../../entities/zone')

const SubnettingForm = ({ supernetId, networkAddress, onCreate, onClose }) => {

  const [errors, setErrors] = useState([])
  const [open, setOpen] = useState(true);
  const [showSelect, setShowSelect] = useState(false);

  const [createRecord, { data: createData, loading: createLoading, error: createErrors }] = useMutation(subnet.create, {
    onCompleted: ({ createSubnet: subnet }) => {
      onClose()
    },
    onError: (err) => {
      const message = err?.message
      setErrors([{ message: (message.charAt(0).toUpperCase() + message.slice(1)).replaceAll('\"', '') }])
    }
  });

  const { loading, error, data, refetch } = useQuery(subnet.GET_QUERY, { variables: { id: parseInt(supernetId) } });

  const [netmask, setNetmask] = useState();
  const [allChildSubnets, setAllChildSubnets] = useState([]); // 存储全量子子网
  const [loadedCount, setLoadedCount] = useState(10); // 已加载数量
  const [isLoadingMore, setIsLoadingMore] = useState(false); // 加载状态

  const supernet = data?.subnet?.networkAddress
  const [form] = Form.useForm();
  const isV4 = cidrRegex.v4({ exact: true }).test(data?.subnet?.networkAddress)

  useEffect(() => {
    if (!netmask || !data?.subnet) return;
    
    let calculatedChildSubnets;
    if (isV4) {
      calculatedChildSubnets = possibleSubnets(data.subnet, netmask);
    } else {
      calculatedChildSubnets = v6PossibleSubnets(data.subnet, netmask);
    }
    
    setAllChildSubnets(calculatedChildSubnets);
    setLoadedCount(10); // 切换掩码时重置加载数量
    setShowSelect(true);
  }, [netmask, supernet, isV4]);

  // 滚动加载逻辑
  const handleScroll = (e) => {
    const { scrollTop, clientHeight, scrollHeight } = e.target;
    // 滚动到接近底部时触发加载
    if (scrollTop + clientHeight >= scrollHeight - 20 && !isLoadingMore && loadedCount < allChildSubnets.length) {
      setIsLoadingMore(true);
      // 模拟异步加载(如果计算是同步的,可直接更新计数)
      setTimeout(() => {
        setLoadedCount(prev => Math.min(prev + 10, allChildSubnets.length));
        setIsLoadingMore(false);
      }, 200);
    }
  };

  return (
    <Drawer
      title={`Create a subnet in ${data?.subnet?.networkAddress}`}
      onClose={onClose}
      width={520}
      visible={open}>
      <Form
        layout="vertical"
        form={form}
        name="subnet"
        onFinish={({ networkAddress, netmask, zone, ...values }) => {
          createRecord({ variables: { ...values, networkAddress: `${networkAddress}`, supernetId: supernetId } })
        }} >
        {errors && errors.map(error => (
          <><Alert type="error" message={error.message} /><br /></>
        ))}
        <Form.Item label="Netmask">
          <NetmaskForm limit={10} netmask={parseInt(data?.subnet?.networkAddress.split('/')[1])} onSelect={(v) => { setNetmask(v) }} defaultValue=' ' />
        </Form.Item>
        {showSelect && (
          <Form.Item label="Subnet">
            <Select
              dropdownRender={menu => (
                <div onScroll={handleScroll} style={{ maxHeight: '300px', overflow: 'auto' }}>
                  {menu}
                  {isLoadingMore && <div style={{ textAlign: 'center', padding: '8px' }}>加载中...</div>}
                  {!isLoadingMore && loadedCount >= allChildSubnets.length && <div style={{ textAlign: 'center', padding: '8px', color: '#999' }}>已加载全部</div>}
                </div>
              )}
            >
              {allChildSubnets.slice(0, loadedCount).map((subnetItem) => (
                <Option key={subnetItem.id} value={subnetItem.id}>
                  {subnetItem.networkAddress}
                </Option>
              ))}
            </Select>
          </Form.Item>
        )}
        <TextInput name="name" />

        <Row style={{ marginTop: '40px' }}>
          <Col span={24}>
            <Button type="secondary" onClick={onClose} style={{ float: 'right', marginLeft: '20px' }}>
              Cancel
            </Button>
            <Button type="primary" htmlType="submit" style={{ float: 'right', marginLeft: '20px' }} loading={createLoading}>
              Submit
            </Button>
          </Col>
        </Row>
      </Form>
    </Drawer>
  )
}

export default SubnettingForm;

额外说明

  • 如果possibleSubnets计算耗时过长,建议将计算逻辑移入Web Worker,避免阻塞主线程;
  • 加载步长(每次加载10个)和滚动触发阈值(距离底部20px)可根据实际体验调整。

内容的提问来源于stack exchange,提问作者Deepanshu Bhalotia

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 16:15:53