如何基于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
相关产品推荐
相关产品推荐

