
本文详细介绍了如何在react应用中实现级联选择器,即根据第一个下拉选择框(select)的选项变化,动态更新第二个select的选项。文章将通过`usestate`管理组件状态,并利用`useeffect`监听依赖项变化以触发异步数据请求,从而实现选项的动态加载和更新,提升用户交互体验。
理解级联选择器需求
在Web表单开发中,级联选择器是一种常见的交互模式,它允许用户在一个选择器中做出选择后,动态地影响另一个或多个选择器的可用选项。例如,选择一个“菜单类型”(如“主菜单”或“页脚菜单”)后,“父菜单”的选项列表应仅显示该类型下可用的父级菜单。
在您提供的react代码中,存在两个关键的select元素:
当前的问题是,table_id选择器的选项(通过menus.map(…)生成)是在组件首次加载时通过menuservice.getAll()一次性获取的。这意味着无论type选择什么,table_id的选项都不会随之变化。要实现级联效果,我们需要在type选择器值改变时,重新获取或过滤table_id的可用选项。
核心概念:useState与useEffect
在React函数组件中,实现动态数据加载和状态管理的两个核心Hook是useState和useEffect。
- useState: 用于在函数组件中声明和管理状态变量。我们将用它来存储当前选中的type值,以及根据type动态加载的table_id选项列表。
- useEffect: 用于在函数组件中执行副作用操作,例如数据获取、订阅或手动dom操作。在这里,我们将利用useEffect来监听type状态的变化,并在其变化时触发异步数据获取逻辑。
实现步骤
为了实现根据type选择器动态更新table_id选择器的选项,我们将进行以下改造:
1. 状态初始化
除了现有的type状态,我们需要一个新的状态来存储table_id选择器将展示的选项列表。
import { useEffect, useState } from "react"; import menuservice from "../../../services/MenuService"; function MenuCreate() { // ... 其他状态 const [type, setType] = useState(""); const [table_id, setTable_id] = useState(0); // 新增:用于存储根据type加载的父菜单选项 const [parentMenuOptions, setParentMenuOptions] = useState([]); const [isLoadingParentMenus, setIsLoadingParentMenus] = useState(false); // 可选:加载状态 // ... }
2. type选择器onChange处理
type选择器的onChange事件处理器需要更新type状态。当type状态更新时,这将触发useEffect中的逻辑。
// ... <select name="type" className="input" value={type} onChange={(e) => { setType(e.target.value); setTable_id(0); // 可选:当类型改变时,重置table_id为默认值 }} > <option disabled value="">--Chọn loại menu--</option> {/* 确保有空的value以便初始选择 */} <option value="mainmenu">Menu chính</option> <option value="footermenu">Menu footer</option> </select> // ...
注意:为了确保select的value在初始时能够匹配option disabled,通常将option disabled的value设为””(空字符串),并确保type的初始状态也为””。
3. 数据获取逻辑
我们将创建一个异步函数来根据传入的菜单类型获取相应的父菜单数据。假设menuservice有一个方法getMenusbyType(type)。
// ... function MenuCreate() { // ... 其他状态 const [type, setType] = useState(""); const [table_id, setTable_id] = useState(0); const [parentMenuOptions, setParentMenuOptions] = useState([]); const [isLoadingParentMenus, setIsLoadingParentMenus] = useState(false); // 异步函数:根据类型获取父菜单选项 const fetchParentMenusByType = async (selectedType) => { if (!selectedType) { // 如果没有选择类型,则清空选项 setParentMenuOptions([]); return; } setIsLoadingParentMenus(true); try { // 假设menuservice有一个根据类型获取菜单的方法 // 如果没有,你可能需要修改后端API或在前端进行过滤 const result = await menuservice.getMenusByType(selectedType); setParentMenuOptions(result.data); } catch (error) { console.error("Error fetching parent menus:", error); setParentMenuOptions([]); // 发生错误时清空选项 } finally { setIsLoadingParentMenus(false); } }; // ... }
重要提示:如果menuservice没有getMenusByType这样的方法,并且menuservice.getAll()返回所有菜单,你可以在fetchParentMenusByType函数内部对menus数组进行过滤。但这通常不如后端直接提供过滤接口效率高。
4. useEffect监听type变化
现在,我们使用useEffect来监听type状态。每当type的值改变时,fetchParentMenusByType函数就会被调用。
// ... function MenuCreate() { // ... 状态和 fetchParentMenusByType 函数 useEffect(() => { fetchParentMenusByType(type); }, [type]); // 依赖数组中包含type,当type变化时重新执行 // ... }
5. 渲染table_id选择器
最后,修改table_id选择器,使其选项列表来源于parentMenuOptions状态。
// ... <fieldset className="input-container"> <label htmlFor="table_id">Chọn menu cha</label> <select name="table_id" className="input" value={table_id} onChange={(e) => setTable_id(e.target.value)} > <option disabled value="0">--Chọn menu cha--</option> <option value="0">Không có cha</option> {isLoadingParentMenus ? ( <option disabled>正在加载...</option> ) : ( parentMenuOptions.map((menu) => ( <option key={menu.id} value={menu.id}> {menu.name} </option> )) )} </select> </fieldset> // ...
完整示例代码(关键部分)
import { faBackward, faFloppyDisk } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, useNavigate } from "react-router-dom"; import { useEffect, useState, useCallback } from "react"; // 引入useCallback import menuservice from "../../../services/MenuService"; function MenuCreate() { const navigate = useNavigate(); const [name, setName] = useState(""); const [link, setLink] = useState(""); const [table_id, setTable_id] = useState(0); const [type, setType] = useState(""); // 初始值为空字符串,匹配禁用选项 const [status, setStatus] = useState(1); // 新增状态:用于存储根据type加载的父菜单选项 const [parentMenuOptions, setParentMenuOptions] = useState([]); const [isLoadingParentMenus, setIsLoadingParentMenus] = useState(false); // 加载状态 // 异步函数:根据类型获取父菜单选项,使用 useCallback 避免不必要的重新创建 const fetchParentMenusByType = useCallback(async (selectedType) => { if (!selectedType) { setParentMenuOptions([]); return; } setIsLoadingParentMenus(true); try { // 假设 menuservice 有一个根据类型获取菜单的方法 // 例如:await menuservice.getMenusByType(selectedType); // 为演示目的,这里模拟数据或假设服务层有此功能 const result = await menuservice.getMenusByType(selectedType); // 请根据实际API调整 setParentMenuOptions(result.data); } catch (error) { console.error("Error fetching parent menus:", error.response?.data || error.message); setParentMenuOptions([]); // 发生错误时清空选项 } finally { setIsLoadingParentMenus(false); } }, []); // 依赖数组为空,函数只创建一次 // useEffect 监听 type 变化,并触发数据获取 useEffect(() => { fetchParentMenusByType(type); }, [type, fetchParentMenusByType]); // 依赖数组包含 type 和 fetchParentMenusByType async function postStore(event) { event.preventDefault(); const image = document.querySelector("#image"); var menu = new FormData(); menu.append("name", name); menu.append("link", link); menu.append("table_id", table_id); menu.append("type", type); menu.append("status", status); // 检查 image.files[0] 是否存在,避免上传 undefined if (image && image.files && image.files[0]) { menu.append("image", image.files[0]); } try { await menuservice.create(menu).then(function (res) { alert(res.data.message); navigate("../../admin/menu", { replace: true }); }); } catch (error) { console.error(error.response.data); } } return ( <section className="mainList"> <div className="wrapper"> <div className="card1"> <form method="post" onSubmit={postStore}> <div className="card-header"> <strong className="title1">THÊM MENU</strong> <div className="button"> <Link to="/admin/menu" className="backward"> <FontAwesomeIcon icon={faBackward} /> go back </Link> <button type="submit" className="save"> <FontAwesomeIcon icon={faFloppyDisk} /> Save </button> </div> </div> <div className="form-container grid -bottom-3 "> <div className="grid__item large--three-quarters"> <fieldset className="input-container"> <label htmlFor="name">Tên menu</label> <input name="name" type="text" className="input" id="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Nhập tên menu..." /> </fieldset> <fieldset className="input-container"> <label htmlFor="link">Đường dẫn menu</label> <input name="link" type="text" className="input" id="link" // 修正id value={link} onChange={(e) => setLink(e.target.value)} placeholder="Nhập đường dẫn..." /> </fieldset> </div> <div className="grid__item large--one-quarter"> <fieldset className="input-container"> <label htmlFor="type">Chọn loại menu</label> <select name="type" className="input" value={type} onChange={(e) => { setType(e.target.value); setTable_id(0); // 当类型改变时,重置table_id为默认值 }} > <option disabled value="">--Chọn loại menu--</option> <option value="mainmenu">Menu chính</option> <option value="footermenu">Menu footer</option> </select> </fieldset> <fieldset className="input-container"> <label htmlFor="table_id">Chọn menu cha</label> <select name="table_id" className="input" value={table_id} onChange={(e) => setTable_id(parseInt(e.target.value))} // 确保table_id是数字类型 > <option disabled value="0">--Chọn menu cha--</option> <option value="0">Không có cha</option> {isLoadingParentMenus ? ( <option disabled>Đang tải...</option> ) : ( parentMenuOptions.map((menu) => ( <option key={menu.id} value={menu.id}> {menu.name} </option> )) )} </select> </fieldset> <fieldset className="input-container"> <label htmlFor="status">Tình trạng xuất bản</label> <select name="status" className="input" value={status} onChange={(e) => setStatus(parseInt(e.target.value))} // 确保status是数字类型 > <option disabled value="">--Chọn tình trạng xuất bản--</option> <option value="1">Xuất bản</option> <option value="2">Không xuất bản</option> </select> </fieldset> {/* 图像上传字段,如果需要保留 */} <fieldset className="input-container"> <label htmlFor="image">Hình ảnh</label> <input type="file" id="image" name="image" className="input" /> </fieldset> </div> </div> </form> </div> </div> </section> ); } export default MenuCreate;
注意事项
- API设计: 确保您的后端API能够根据菜单类型(type)过滤并返回相应的菜单列表。如果后端没有这样的接口,您可能需要在前端获取所有菜单数据后进行过滤,但这在大数据量时效率较低。
- 初始加载: useEffect在组件首次挂载时也会执行一次。如果type的初始值为””或NULL,fetchParentMenusByType应能正确处理,例如清空选项。
- 错误处理: 在fetchParentMenusByType函数中加入了try-catch块来捕获API请求可能发生的错误,并在控制台输出错误信息,同时清空选项以避免显示不正确的数据。
- 加载状态: 使用isLoadingParentMenus状态可以在数据加载期间向用户显示“正在加载…”的提示,提升用户体验,防止用户在数据未准备好时进行操作。
- 类型转换: select元素返回的值始终是字符串。如果您的table_id或status需要是数字类型,请在onChange处理函数中进行parseInt()转换。
- useCallback: 在fetchParentMenusByType函数上使用useCallback可以优化性能,防止该函数在每次组件渲染时都被重新创建,从而避免useEffect在依赖项中包含它时引起不必要的重新执行。
- 默认选项的value: 为了确保select的value能够正确匹配option disabled,请确保option disabled的value设置为””(空字符串)或0,并与useState的初始值保持一致。
- 表单提交: 在postStore中,当type和table_id的值来自状态时,直接使用它们即可,无需再从DOM中获取。
总结
通过useState和useEffect的组合,我们成功实现了React中级联选择器的功能。当第一个选择器(type)的值发生变化时,useEffect会触发一个异步数据获取过程,根据新的type值从后端获取相应的选项,并更新第二个选择器(table_id)的选项列表。这种模式是处理React中动态数据加载和组件间交互的常用且高效的方法。


