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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 01:44:56