I am developing a web application using React (React Bootstrap, Typescript, SCSS). I have made progress; I fetch and display the items from my application server, I can search and I can open the item screen with all the details.
But now for the part where I am definitely getting confused, even after searching the internet. I am creating a page for editing an item. My application consists of modules (the application is Data Crow by the way). Each module has fields. Each field has a type.
So when I build up my page, I need to loop through the fields of the selected module.
I have made a context for the module (useModule
). The page itself is sitting in an authorization context (RequireAuth
).
Now, as you can see below I then loop though the fields and for each field I call a function (sitting in a separate .tsx file) to render the component. I kind of have to it this way as I have many module and user can define their own modules and fields.
So this is the item page:
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>
);
}
So for each field the function InputField
is called. This one looks as follows (work in progress hence the empty else if blocks):
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>
)
}
And now onto the problem at hand; I have a reference field (which is a React-Select component). I have also added this below. The only thing here is that I want to make a useState
call to trigger an icon update for when the dropdown is collapsed. But this triggers the probably very well know error: 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
(and a RTFM message, which I did and after which I did I get the feeling I am doomed).
This is what I am trying to add: const [selectedValue, setSelectedValue] = useState<null>();
I am I just completely doing the wrong thing? Am in a to deep a layer to make use of hooks here? That’s basically all I am looking for in an answer, more is appreciated but that is the essence of this post. Help…. 🙂
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 }} />
);
}
Full error details:
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
Robert W. is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
The starting point of the problem is this line:
InputField(fieldValue.field, fieldValue.value, ReferencesForField(fieldValue.field))
You are calling InputField as a function, which means it executes immediately, during the rendering of ItemPage
. InputField
then calls Field
, which calls various other functions, and somewhere in one of those functions you must be calling react hooks. Since all this is taking place during the rendering of ItemPage
, those are hooks of ItemPage
.
The way that hooks are designed, you must always call the same number of hooks in the same order each time your component renders. This is called the Rules of Hooks. Your call to InputField
is in a loop, so if the length of item.fields change, you will call InputField
more times, which will in turn result in more hooks being called.
The fix for this is to create components and render them as elements. For example, we could do something like this with your InputField, making it take only one parameter, which is the props object
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>
)
}
And you’ll use it like this:
{references && item?.fields.map((fieldValue) => (
<InputField
field={fieldValue.field}
value={fieldValue.value}
references={ReferencesForField(fieldValue.field)}
/>
)}
This way, you are not calling InputField immediately, during the rendering of ItemPage. You’re just telling react “I would like to render these 6 (or however many) InputFields”. React will then set up its internal state and do separate renders for each of those input fields, and they can each call their own hooks.
Two additional comments:
- I just showed this example for InputField, but you will need to turn most of the other parts of your code into components as well. Components must take a single parameter, which is the props object, and you must render them as an element (ie, with the angle brackets
<
and>
), not call them yourself. - When you define components, they must not be inside other components. For example, your
IconOption
is defined insideDcReferenceField
. As a result, every timeDcReferenceField
renders you create a brand new function namedIconOption
. It may have the same text as the previous one, but it’s a new function, so react will think it’s a different component type. This forces react to unmount the old instance and mount a new one, which wipes out any internal state. Instead, define all your components just once at the top level of the file.
1