React-Redux多表复用编辑组件:条件Hook调用报错解决方案咨询
解决React Hook条件调用错误:复用Edit组件操作多表CRUD
问题描述
我有一个用于操作数据库中三个表(景点、餐厅、演出)的Edit组件,根据传入的type参数调用对应表的CRUD Hook,但当前写法触发了React Hook is called conditionally错误。既不想拆分组件破坏复用性,尝试自定义Hook也没解决,求可行方案。
原错误代码:
import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { TextField, FormControlLabel, Checkbox, FormGroup, Button } from "@mui/material"; import { useAddAttractionMutation, useFetchAttractionQuery, useUpdateAttractionMutation, useRemoveAttractionMutation, useAddRestaurantMutation, useFetchRestaurantQuery, useUpdateRestaurantMutation, useRemoveRestaurantMutation, useAddShowMutation, useFetchShowQuery, useUpdateShowMutation, useRemoveShowMutation, } from "../store/apis/dappApi"; import Dropdown from "./Dropdown"; import { PARK_AREAS } from "../constants"; function Edit({ type }) { const { id } = useParams(); const navigate = useNavigate(); const parks = Object.keys(PARK_AREAS); const [name, setName] = useState(""); const [park, setPark] = useState(parks[0]); const [area, setArea] = useState(PARK_AREAS[park][0]); const [completed, setCompleted] = useState(false); let mutationVariables; if (type === "attractions") { mutationVariables = { addMutation: useAddAttractionMutation()[0], updateMutation: useUpdateAttractionMutation()[0], removeMutation: useRemoveAttractionMutation()[0], fetchData: useFetchAttractionQuery(id), }; } else if (type === "restaurants") { mutationVariables = { addMutation: useAddRestaurantMutation()[0], updateMutation: useUpdateRestaurantMutation()[0], removeMutation: useRemoveRestaurantMutation()[0], fetchData: useFetchRestaurantQuery(id), }; } else if (type === "shows") { mutationVariables = { addMutation: useAddShowMutation()[0], updateMutation: useUpdateShowMutation()[0], removeMutation: useRemoveShowMutation()[0], fetchData: useFetchShowQuery(id), }; } const { addMutation, updateMutation, removeMutation, fetchData: { data, isLoading }, } = mutationVariables; const handleDelete = () => { removeMutation(id); navigate(`/${type}`); } const handleUpdate = () => { const updateData = { id, name, park, area, completed, }; updateMutation(updateData); navigate(`/${type}`); } const handleAdd = () => { const attractionData = { name, park, area, completed }; addMutation(attractionData); navigate(`/${type}`); } useEffect(() => { if (!isLoading && id != null) { setPark(data.park); setCompleted(data.completed); setName(data.name); setArea(data.area); } }, [isLoading]); return isLoading ? ( <div className="flex items-center justify-center">Loading...</div> ) : ( <div> <FormGroup className="flex flex-col m-6 gap-4"> <TextField label="Name" value={name} onChange={(event) => { setName(event.target.value); }} /> <Dropdown options={parks} value={park} label="Park" setValue={setPark} /> <Dropdown options={PARK_AREAS[park]} value={area} label="Area" setValue={setArea} /> <FormControlLabel control={ <Checkbox checked={completed} onChange={(event) => { setCompleted(event.target.checked); }} /> } label="Completed" /> <div className="outline rounded-lg h-80 flex justify-center align-center" > map will go here </div> {id != null ? ( <div className="flex justify-center gap-4"> <Button variant="contained" onClick={handleUpdate}> Save </Button> <Button variant="contained" color="error" onClick={handleDelete}> Delete </Button> </div> ) : ( <div className="flex justify-center"> <Button variant="contained" onClick={handleAdd}>Create</Button> </div> )} </FormGroup> </div> ); } export default Edit; Edit.propTypes = { type: PropTypes.string.isRequired, };
解决方案:顶层声明所有Hook,按类型映射选择
React的Hook规则要求所有Hook必须在组件的顶层作用域调用,不能放在条件判断、循环或嵌套函数里。所以我们需要先把所有表的CRUD Hook都在组件顶层声明,再通过一个映射对象根据type来选择对应的Hook实例。
修改后的代码
import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { TextField, FormControlLabel, Checkbox, FormGroup, Button } from "@mui/material"; import { useAddAttractionMutation, useFetchAttractionQuery, useUpdateAttractionMutation, useRemoveAttractionMutation, useAddRestaurantMutation, useFetchRestaurantQuery, useUpdateRestaurantMutation, useRemoveRestaurantMutation, useAddShowMutation, useFetchShowQuery, useUpdateShowMutation, useRemoveShowMutation, } from "../store/apis/dappApi"; import Dropdown from "./Dropdown"; import { PARK_AREAS } from "../constants"; function Edit({ type }) { const { id } = useParams(); const navigate = useNavigate(); const parks = Object.keys(PARK_AREAS); const [name, setName] = useState(""); const [park, setPark] = useState(parks[0]); const [area, setArea] = useState(PARK_AREAS[park][0]); const [completed, setCompleted] = useState(false); // 1. 顶层声明所有Hook,符合React规则 // 景点相关Hook const [addAttraction] = useAddAttractionMutation(); const [updateAttraction] = useUpdateAttractionMutation(); const [removeAttraction] = useRemoveAttractionMutation(); const attractionFetch = useFetchAttractionQuery(id); // 餐厅相关Hook const [addRestaurant] = useAddRestaurantMutation(); const [updateRestaurant] = useUpdateRestaurantMutation(); const [removeRestaurant] = useRemoveRestaurantMutation(); const restaurantFetch = useFetchRestaurantQuery(id); // 演出相关Hook const [addShow] = useAddShowMutation(); const [updateShow] = useUpdateShowMutation(); const [removeShow] = useRemoveShowMutation(); const showFetch = useFetchShowQuery(id); // 2. 创建类型到Hook的映射 const hookMap = { attractions: { addMutation: addAttraction, updateMutation: updateAttraction, removeMutation: removeAttraction, fetchData: attractionFetch, }, restaurants: { addMutation: addRestaurant, updateMutation: updateRestaurant, removeMutation: removeRestaurant, fetchData: restaurantFetch, }, shows: { addMutation: addShow, updateMutation: updateShow, removeMutation: removeShow, fetchData: showFetch, }, }; // 3. 根据type获取对应的Hook集合 const { addMutation, updateMutation, removeMutation, fetchData: { data, isLoading }, } = hookMap[type]; const handleDelete = () => { removeMutation(id); navigate(`/${type}`); } const handleUpdate = () => { const updateData = { id, name, park, area, completed, }; updateMutation(updateData); navigate(`/${type}`); } const handleAdd = () => { const itemData = { name, park, area, completed }; addMutation(itemData); navigate(`/${type}`); } useEffect(() => { if (!isLoading && id != null && data) { // 增加data存在的判断,避免空值报错 setPark(data.park); setCompleted(data.completed); setName(data.name); setArea(data.area); } }, [isLoading, id, data]); // 依赖项补充id和data,符合useEffect规则 return isLoading ? ( <div className="flex items-center justify-center">Loading...</div> ) : ( <div> <FormGroup className="flex flex-col m-6 gap-4"> <TextField label="Name" value={name} onChange={(event) => { setName(event.target.value); }} /> <Dropdown options={parks} value={park} label="Park" setValue={setPark} /> <Dropdown options={PARK_AREAS[park]} value={area} label="Area" setValue={setArea} /> <FormControlLabel control={ <Checkbox checked={completed} onChange={(event) => { setCompleted(event.target.checked); }} /> } label="Completed" /> <div className="outline rounded-lg h-80 flex justify-center align-center" > map will go here </div> {id != null ? ( <div className="flex justify-center gap-4"> <Button variant="contained" onClick={handleUpdate}> Save </Button> <Button variant="contained" color="error" onClick={handleDelete}> Delete </Button> </div> ) : ( <div className="flex justify-center"> <Button variant="contained" onClick={handleAdd}>Create</Button> </div> )} </FormGroup> </div> ); } export default Edit; Edit.propTypes = { type: PropTypes.string.isRequired, };
关键修改点
- 所有Hook移到组件顶层:不管当前type是什么,都先声明所有表的CRUD Hook,满足React的Hook调用规则。
- 用映射对象关联类型与Hook:创建
hookMap对象,将每个type对应到一组Hook,通过hookMap[type]获取当前需要的Hook实例。 - 修复useEffect依赖项:原代码的useEffect缺少
id和data依赖,补充后避免潜在的闭包问题。 - 增加空值判断:在useEffect里判断
data是否存在,防止加载完成但数据未返回时的报错。
可选优化:封装自定义Hook
如果觉得组件里声明所有Hook太冗余,可以封装一个自定义Hook来统一处理类型与Hook的映射,同时保证Hook在顶层调用:
// useEntityCrud.js import { useAddAttractionMutation, useFetchAttractionQuery, useUpdateAttractionMutation, useRemoveAttractionMutation, useAddRestaurantMutation, useFetchRestaurantQuery, useUpdateRestaurantMutation, useRemoveRestaurantMutation, useAddShowMutation, useFetchShowQuery, useUpdateShowMutation, useRemoveShowMutation, } from "../store/apis/dappApi"; export function useEntityCrud(type, id) { // 顶层声明所有Hook const [addAttraction] = useAddAttractionMutation(); const [updateAttraction] = useUpdateAttractionMutation(); const [removeAttraction] = useRemoveAttractionMutation(); const attractionFetch = useFetchAttractionQuery(id); const [addRestaurant] = useAddRestaurantMutation(); const [updateRestaurant] = useUpdateRestaurantMutation(); const [removeRestaurant] = useRemoveRestaurantMutation(); const restaurantFetch = useFetchRestaurantQuery(id); const [addShow] = useAddShowMutation(); const [updateShow] = useUpdateShowMutation(); const [removeShow] = useRemoveShowMutation(); const showFetch = useFetchShowQuery(id); const hookMap = { attractions: { addMutation: addAttraction, updateMutation: updateAttraction, removeMutation: removeAttraction, fetchData: attractionFetch, }, restaurants: { addMutation: addRestaurant, updateMutation: updateRestaurant, removeMutation: removeRestaurant, fetchData: restaurantFetch, }, shows: { addMutation: addShow, updateMutation: updateShow, removeMutation: removeShow, fetchData: showFetch, }, }; return hookMap[type]; }
然后在Edit组件中使用:
// Edit.js import { useEntityCrud } from './useEntityCrud'; function Edit({ type }) { const { id } = useParams(); // ...其他状态声明 // 调用自定义Hook获取对应CRUD Hook const { addMutation, updateMutation, removeMutation, fetchData: { data, isLoading }, } = useEntityCrud(type, id); // ...剩余逻辑不变 }
这种方式既符合Hook规则,又能保持组件的简洁性,同时复用逻辑。
内容的提问来源于stack exchange,提问作者honks69
相关产品推荐
相关产品推荐

