# React如何實現二級聯動效果
## 一、前言:什么是二級聯動
二級聯動是指兩個關聯的下拉選擇框,第一個選擇框(父級)的選項變化會動態影響第二個選擇框(子級)的可用選項。這種交互模式常見于:
- 省市級聯選擇
- 產品分類與子分類
- 學校與院系選擇
- 日期時間選擇器等
在React中實現這種效果需要綜合運用狀態管理、數據組織和事件處理等技術。本文將深入探討5種實現方案,并提供完整的代碼示例和性能優化建議。
## 二、基礎實現方案
### 1. 使用useState管理狀態
```jsx
import { useState } from 'react';
function CascadeSelect() {
// 原始數據
const data = [
{ id: 1, name: '電子產品', children: [
{ id: 11, name: '手機' },
{ id: 12, name: '電腦' }
]},
{ id: 2, name: '服裝', children: [
{ id: 21, name: '男裝' },
{ id: 22, name: '女裝' }
]}
];
// 狀態管理
const [selectedParent, setSelectedParent] = useState('');
const [childrenOptions, setChildrenOptions] = useState([]);
const [selectedChild, setSelectedChild] = useState('');
// 父級選擇變化處理
const handleParentChange = (e) => {
const parentId = e.target.value;
setSelectedParent(parentId);
// 查找對應的子選項
const parentItem = data.find(item => item.id == parentId);
setChildrenOptions(parentItem ? parentItem.children : []);
setSelectedChild(''); // 重置子選擇
};
return (
<div>
<select value={selectedParent} onChange={handleParentChange}>
<option value="">請選擇父級</option>
{data.map(item => (
<option key={item.id} value={item.id}>{item.name}</option>
))}
</select>
<select
value={selectedChild}
onChange={(e) => setSelectedChild(e.target.value)}
disabled={!selectedParent}
>
<option value="">請選擇子級</option>
{childrenOptions.map(item => (
<option key={item.id} value={item.id}>{item.name}</option>
))}
</select>
</div>
);
}
優點: - 實現簡單直觀 - 不依賴額外庫 - 適合小型應用
缺點: - 狀態分散管理 - 數據查找效率不高(O(n)) - 組件重新渲染次數較多
import { useReducer } from 'react';
const initialState = {
selectedParent: '',
selectedChild: '',
childrenOptions: []
};
function reducer(state, action) {
switch (action.type) {
case 'SELECT_PARENT':
const parentItem = action.data.find(item => item.id == action.payload);
return {
...state,
selectedParent: action.payload,
childrenOptions: parentItem ? parentItem.children : [],
selectedChild: ''
};
case 'SELECT_CHILD':
return { ...state, selectedChild: action.payload };
default:
return state;
}
}
function CascadeSelect({ data }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<select
value={state.selectedParent}
onChange={(e) => dispatch({
type: 'SELECT_PARENT',
payload: e.target.value,
data
})}
>
{/* 選項渲染 */}
</select>
<select
value={state.selectedChild}
onChange={(e) => dispatch({
type: 'SELECT_CHILD',
payload: e.target.value
})}
>
{/* 子選項渲染 */}
</select>
</div>
);
}
import { createContext, useContext, useState } from 'react';
const CascadeContext = createContext();
function CascadeProvider({ children, data }) {
const [state, setState] = useState({
selectedParent: '',
selectedChild: '',
childrenOptions: []
});
const value = {
state,
setState,
data
};
return (
<CascadeContext.Provider value={value}>
{children}
</CascadeContext.Provider>
);
}
function ParentSelect() {
const { state, setState, data } = useContext(CascadeContext);
const handleChange = (e) => {
const parentId = e.target.value;
const parentItem = data.find(item => item.id == parentId);
setState(prev => ({
...prev,
selectedParent: parentId,
childrenOptions: parentItem?.children || [],
selectedChild: ''
}));
};
return (
<select value={state.selectedParent} onChange={handleChange}>
{/* 選項渲染 */}
</select>
);
}
function ChildSelect() {
const { state, setState } = useContext(CascadeContext);
return (
<select
value={state.selectedChild}
onChange={(e) => setState(prev => ({
...prev,
selectedChild: e.target.value
}))}
>
{/* 子選項渲染 */}
</select>
);
}
import { useMemo, useState } from 'react';
function useCascadeData(rawData) {
// 預處理數據:建立ID到數據的映射
const dataMap = useMemo(() => {
const map = new Map();
rawData.forEach(parent => {
map.set(parent.id, parent);
});
return map;
}, [rawData]);
const [state, setState] = useState({
selectedParent: '',
selectedChild: ''
});
// 記憶化子選項
const childrenOptions = useMemo(() => {
if (!state.selectedParent) return [];
const parent = dataMap.get(Number(state.selectedParent));
return parent?.children || [];
}, [state.selectedParent, dataMap]);
return { state, setState, childrenOptions };
}
import { FixedSizeList as List } from 'react-window';
const OptionList = ({ options, height = 200, onSelect }) => (
<List
height={height}
itemCount={options.length}
itemSize={35}
width={300}
>
{({ index, style }) => (
<div
style={style}
onClick={() => onSelect(options[index].id)}
>
{options[index].name}
</div>
)}
</List>
);
import axios from 'axios';
import { useEffect, useState } from 'react';
function RegionSelector() {
const [regions, setRegions] = useState([]);
const [cities, setCities] = useState([]);
const [districts, setDistricts] = useState([]);
const [selected, setSelected] = useState({
province: '',
city: '',
district: ''
});
useEffect(() => {
// 加載省級數據
axios.get('/api/provinces').then(res => {
setRegions(res.data);
});
}, []);
useEffect(() => {
if (!selected.province) return;
// 加載市級數據
axios.get(`/api/cities?province=${selected.province}`)
.then(res => {
setCities(res.data);
setSelected(prev => ({ ...prev, city: '' }));
});
}, [selected.province]);
useEffect(() => {
if (!selected.city) return;
// 加載區級數據
axios.get(`/api/districts?city=${selected.city}`)
.then(res => {
setDistricts(res.data);
setSelected(prev => ({ ...prev, district: '' }));
});
}, [selected.city]);
return (
<div className="region-selector">
<select
value={selected.province}
onChange={(e) => setSelected({
province: e.target.value,
city: '',
district: ''
})}
>
<option value="">選擇省份</option>
{regions.map(province => (
<option key={province.code} value={province.code}>
{province.name}
</option>
))}
</select>
<select
value={selected.city}
onChange={(e) => setSelected(prev => ({
...prev,
city: e.target.value,
district: ''
}))}
disabled={!selected.province}
>
<option value="">選擇城市</option>
{cities.map(city => (
<option key={city.code} value={city.code}>
{city.name}
</option>
))}
</select>
<select
value={selected.district}
onChange={(e) => setSelected(prev => ({
...prev,
district: e.target.value
}))}
disabled={!selected.city}
>
<option value="">選擇區縣</option>
{districts.map(district => (
<option key={district.code} value={district.code}>
{district.name}
</option>
))}
</select>
</div>
);
}
import { Formik, Field, Form } from 'formik';
function CascadeForm() {
const initialValues = {
category: '',
subcategory: '',
product: ''
};
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => {
console.log('提交數據:', values);
}}
>
{({ values, setFieldValue }) => (
<Form>
<Field
name="category"
as="select"
onChange={(e) => {
setFieldValue('category', e.target.value);
setFieldValue('subcategory', '');
setFieldValue('product', '');
}}
>
{/* 分類選項 */}
</Field>
<Field
name="subcategory"
as="select"
disabled={!values.category}
onChange={(e) => {
setFieldValue('subcategory', e.target.value);
setFieldValue('product', '');
}}
>
{/* 子分類選項 */}
</Field>
<Field
name="product"
as="select"
disabled={!values.subcategory}
>
{/* 產品選項 */}
</Field>
<button type="submit">提交</button>
</Form>
)}
</Formik>
);
}
問題場景:子選項需要異步加載
const loadChildren = async (parentId) => {
try {
const response = await fetch(`/api/children?parent=${parentId}`);
return await response.json();
} catch (error) {
console.error('加載失敗:', error);
return [];
}
};
// 在事件處理中使用
const handleParentChange = async (e) => {
const parentId = e.target.value;
setLoading(true);
const children = await loadChildren(parentId);
setChildrenOptions(children);
setLoading(false);
};
方案 | 適用場景 | 優點 | 缺點 |
---|---|---|---|
useState | 簡單聯動、數據量小 | 實現簡單 | 狀態分散 |
useReducer | 復雜狀態邏輯 | 狀態集中管理 | 代碼量稍大 |
Context | 跨組件通信 | 解耦組件 | 需要Provider包裹 |
數據預處理 | 大數據量 | 查找速度快 | 初始化耗時 |
虛擬滾動 | 超大數據量 | 渲染高效 | 實現復雜 |
數據組織建議:
狀態管理原則:
用戶體驗優化:
可訪問性考慮:
通過本文的詳細講解,相信您已經掌握了在React中實現二級聯動的各種技術方案。根據實際項目需求選擇最適合的實現方式,并注意性能優化和用戶體驗細節,就能構建出高效好用的聯動選擇組件。 “`
注:本文實際約4500字,完整達到4850字需要進一步擴展每個章節的詳細說明和示例代碼。如需完整版本,可以擴展以下內容: 1. 增加每種方案的適用場景分析 2. 添加更多實際業務場景案例 3. 深入性能優化章節的基準測試數據 4. 增加TypeScript實現版本 5. 添加可視化圖表說明數據流動
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。