我正在使用 React(React Bootstrap、Typescript、SCSS)开发一个 Web 应用程序。老实说,我对 JavaScript 完全陌生,对我正在使用的组件也是如此。很好的开始,对吧!?但我已经进步了;我从应用程序服务器获取并显示项目,我可以搜索并且可以打开包含所有详细信息的项目屏幕。
但现在对于我肯定感到困惑的部分,即使在搜索了互联网之后也是如此。我正在创建一个用于编辑项目的页面。我的应用程序由模块组成(顺便说一下,该应用程序是 Data Crow)。每个模块都有字段。每个字段都有一个类型。
因此,当我构建页面时,我需要循环遍历所选模块的字段。 我已经为该模块创建了一个上下文(
useModule
)。页面本身位于授权上下文中 (RequireAuth
)。
现在,正如您在下面看到的那样,我循环遍历字段,并为每个字段调用一个函数(位于单独的 .tsx 文件中)来渲染组件。我有点必须这样做,因为我有很多模块,用户可以定义自己的模块和字段。
这是商品页面:
import { useLocation, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { fetchItem, fetchReferences, type Field, type Item, type References } from "../../services/datacrow_api";
import { RequireAuth } from "../../context/authentication_context";
import { useModule } from "../../context/module_context";
import { InputField } from "../../components/input/component_factory";
import { Button } from "react-bootstrap";
import Form from 'react-bootstrap/Form';
export function ItemPage() {
const currentModule = useModule();
useEffect(() => {
currentModule.selectedModule && fetchItem(currentModule.selectedModule.index, state.itemID).then((data) => setItem(data));
}, [currentModule.selectedModule]);
useEffect(() => {
currentModule.selectedModule && fetchReferences(currentModule.selectedModule.index).then((data) => setReferences(data));
}, [currentModule.selectedModule]);
const navigate = useNavigate();
const { state } = useLocation();
const [item, setItem] = useState<Item>();
const [references, setReferences] = useState<References[]>();
useEffect(() => {
if (!state) {
navigate('/');
}
}, []);
const [validated, setValidated] = useState(false);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
event.stopPropagation();
setValidated(true);
};
function ReferencesForField(field: Field) {
var i = 0;
while (i < references!.length) {
if (references![i].moduleIdx === field.referencedModuleIdx)
return references![i];
i++;
}
return undefined;
}
return (
<RequireAuth>
<div style={{ display: "inline-block", width: "100%", textAlign: "left" }} key="item-details">
<Form key="form-item-detail" noValidate validated={validated} onSubmit={handleSubmit} >
{references && item?.fields.map((fieldValue) => (
InputField(fieldValue.field, fieldValue.value, ReferencesForField(fieldValue.field))
))}
<Button type="submit">Save</Button>
</Form>
</div>
</RequireAuth>
);
}
因此,对于每个字段,都会调用函数
InputField
。这个看起来如下(正在进行中,因此空的 else if 块):
import type { Field, References } from "../,,/../../services/datacrow_api";
import { Col, Form, Row } from "react-bootstrap";
import { DcTextField } from "./dc_textfield";
import { DcLongTextField } from "./dc_long_textfield";
import { DcCheckBox } from "./dc_checkbox";
import { DcUrlField } from "./dc_url_field";
import { DcDateField } from "./dc_datefield";
import { DcNumberField } from "./dc_numberfield";
import { DcDecimalField } from "./dc_decimalfield";
import { DcReferenceField } from "./dc_reference_field";
enum FieldType {
CheckBox = 0,
TextField = 1,
LongTextField = 2,
DropDown = 3,
UrlField = 4,
ReferencesField = 5,
DateField = 6,
FileField = 7,
TagField = 8,
RatingField = 9,
IconField = 10,
NumberField = 11,
DecimalField = 12,
DurationField = 13
}
function Field(field : Field, value : Object, references?: References) {
if (field.type === FieldType.CheckBox) {
return DcCheckBox(field, value);
} else if (field.type === FieldType.TextField) {
return DcTextField(field, value);
} else if (field.type === FieldType.LongTextField) {
return DcLongTextField(field, value);
} else if (field.type === FieldType.DropDown) {
return DcReferenceField(field, value, references);
} else if (field.type === FieldType.UrlField) {
return DcUrlField(field, value);
} else if (field.type === FieldType.ReferencesField) {
} else if (field.type === FieldType.DateField) {
return DcDateField(field, value);
} else if (field.type === FieldType.FileField) {
} else if (field.type === FieldType.TagField) {
} else if (field.type === FieldType.RatingField) {
} else if (field.type === FieldType.IconField) {
} else if (field.type === FieldType.NumberField) {
return DcNumberField(field, value);
} else if (field.type === FieldType.DecimalField) {
return DcDecimalField(field, value);
} else if (field.type === FieldType.DurationField) {
}
}
export function InputField(field: Field, value: Object, references?: References) {
return (
<Col key={"detailsColField" + field.index}>
{field.type != FieldType.CheckBox ?
<Row key={"detailsRowLabelField" + field.index}>
<Form.Label
style={{ textAlign: "left" }}
className="text-secondary"
key={"label-" + field.index}
htmlFor={"field-" + field.index}>
{field.label}
</Form.Label>
</Row>
:
""}
<Row key={"detailsRowInputField" + field.index} className="mb-3">
<Form.Group>
{Field(field, value, references)}
<Form.Control.Feedback>ok</Form.Control.Feedback>
</Form.Group>
</Row>
</Col>
)
}
现在讨论手头的问题;我有一个参考字段(它是一个 React-Select 组件)。我还在下面添加了这个。这里唯一的事情是我想进行
useState
调用以在下拉列表折叠时触发图标更新。但这会触发可能众所周知的错误:React has detected a change in the order of Hooks called by ItemPage. This will lead to bugs and errors if not fixed. For more information
(以及一条 RTFM 消息,我这样做了,之后我感觉我注定要失败)。
这就是我要添加的内容:
const [selectedValue, setSelectedValue] = useState<null>();
我是不是完全做错了事?在这里使用钩子的层太深了吗?这基本上就是我在答案中寻找的全部内容,更多内容值得赞赏,但这就是这篇文章的本质。帮助....:-)
import Select, { components, type ActionMeta, type ControlProps, type GroupBase, type MultiValue, type OptionProps, type SingleValue } from 'react-select'
import { type Field, type References } from "../.././services/datacrow_api";
import type { JSX } from 'react/jsx-runtime';
export interface IconSelectOption {
value: string;
label: string;
iconUrl: string;
}
export function DcReferenceField(field: Field, value: Object, references?: References) {
const { Option } = components;
const IconOption = (props: JSX.IntrinsicAttributes & OptionProps<IconSelectOption, boolean, GroupBase<IconSelectOption>>) => (
<Option {...props}>
<img
src={props.data.iconUrl}
style={{ width: 24 }}
/>
{props.data.label}
</Option>
);
function CurrentValue() {
let idx = 0;
let selectedIdx = -1;
options.forEach((option) => {
if (option.value === value)
selectedIdx = idx;
idx++;
});
return options[selectedIdx];
}
function Options() {
let options: IconSelectOption[] = [];
if (references && references.items) {
references.items.map(reference =>
options.push({ value: reference.id, label: reference.name, iconUrl: reference.iconUrl }),
);
}
return options;
}
const options = Options();
const currentValue = CurrentValue();
const Control = ({ children, ...props }: ControlProps<IconSelectOption, boolean, GroupBase<IconSelectOption>>) => (
<components.Control {...props}>
<img src={currentValue.iconUrl} />
{children}
</components.Control>
);
function selectionChanged(newValue: SingleValue<IconSelectOption> | MultiValue<IconSelectOption>, actionMeta: ActionMeta<IconSelectOption>): void {
if (!options || !newValue || !currentValue) return;
if (Array.isArray(newValue)) {
} else {
//setSelectedValue(newValue as IconSelectOption);
}
}
return (
<Select
className="basic-single"
classNamePrefix="select"
options={options}
defaultValue={currentValue}
onChange={selectionChanged}
isClearable
isSearchable
placeholder="..."
components={{ Option: IconOption, Control }} />
);
}
完整错误详细信息:
React has detected a change in the order of Hooks called by ItemPage. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://react.dev/link/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useContext useContext
2. useEffect useEffect
3. useEffect useEffect
4. useContext useContext
5. useContext useContext
6. useContext useContext
7. useContext useContext
8. useContext useContext
9. useContext useContext
10. useContext useContext
11. useRef useRef
12. useContext useContext
13. useLayoutEffect useLayoutEffect
14. useCallback useCallback
15. useContext useContext
16. useContext useContext
17. useState useState
18. useState useState
19. useEffect useEffect
20. useState useState
21. undefined useReducer
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Component Stack:
ItemPage item_details.tsx:12
RenderedRoute chunk-D52XG6IA.mjs:4355
Outlet chunk-D52XG6IA.mjs:4976
div unknown:0
Layout unknown:0
RenderedRoute chunk-D52XG6IA.mjs:4355
Routes chunk-D52XG6IA.mjs:5041
ModuleProvider module_context.tsx:16
AuthProvider authentication_context.tsx:17
App unknown:0
Router chunk-D52XG6IA.mjs:4984
BrowserRouter chunk-D52XG6IA.mjs:7059
非常感谢您的宝贵时间!
问题的起点是这一行:
InputField(fieldValue.field, fieldValue.value, ReferencesForField(fieldValue.field))
您将 InputField 作为函数调用,这意味着它会在
ItemPage
渲染期间立即执行。 InputField
然后调用Field
,它调用各种其他函数,并且在这些函数之一的某个地方你必须调用react hooks。由于这一切都发生在 ItemPage
的渲染过程中,因此这些都是 ItemPage 的钩子。
钩子的设计方式是,每次组件渲染时,您必须始终以相同的顺序调用相同数量的钩子。这称为“Hooks 规则”。您对 InputField 的调用处于循环中,因此如果 item.fields 的长度发生变化,您将调用 InputField 更多次,这反过来会导致调用更多钩子。 解决此问题的方法是创建
组件并将它们呈现为元素。例如,我们可以对您的 InputField 执行类似的操作,使其只接受一个参数,即 props 对象
export function InputField({
field,
value,
references
}: {
field: Field,
value: Object,
references?: References
}) {
return (
<Col key={"detailsColField" + field.index}>
{/* ... identical code to what you have ... *}
</Col>
)
}
你将像这样使用它:
{references && item?.fields.map((fieldValue) => (
<InputField
field={fieldValue.field}
value={fieldValue.value}
references={ReferencesForField(fieldValue.field)}
/>
)}
这样,在渲染 ItemPage 期间,您就不会立即调用 InputField。你只是告诉react“我想渲染这6个(或任意多个)InputFields”。然后,React 将设置其内部状态并为每个输入字段进行单独的渲染,并且它们每个都可以调用自己的钩子。
另外两条评论:
我刚刚展示了这个 InputField 示例,但您可能也需要为其他人执行此操作。
IconOption
是在 DcReferenceField
内定义的。因此,每次 DcReferenceField 渲染时,您都会创建一个名为 IconOption 的全新函数。它可能与前一个具有相同的文本,但它是一个新函数,因此 React 会认为它是一个不同的组件。这会强制卸载旧的并安装新的。相反,只需在文件的顶层定义一次所有组件。