I have a component, shown below, which works great on its own. The idea, is that I’m building up a list of comparison operators where the item selected in the first dropdown is a key in a key value pair collection where the value is a string array which populates options relevant to that key in the third box. The second box acts as the comparator selector.
This works great as a component, or a nested component. But as soon as I render that component in a map
it breaks the hooks system.
It would be hard to move the state out of the component, since each iteration needs its own store (meaning, moving it to the parent we need to start storing values on index, but only the parent knows the index and now we’re passing data back and forth which makes things worse).
What’s the right paradigm here?
const Condition = () => {
const [selectables, setSelectables] = useState([]);
//This will be passed in as a prop
const vals = {
"Fruit" : ["Apple", "Orange"],
"Vegetable" : ["Cucumber", "Celery", "Onion"],
};
const filterSelectablesOnClick = (selected) => {
var newState = selected.map((item) => {
return { label: item, value: item };
});
setSelectables(newState);
};
const valKeys = Object.entries(vals).map(([key, value]) => {
return { label: key, value: key };
})
return (
<Row>
<div className="col-md-4">
<ReactSelect
options={valKeys}
onChange={(selected) => {
filterSelectablesOnClick(value);
}}
/>
</div>
<div className="col-md-4">
<Form.Select aria-label="Default select example" className="form-select flex-grow-1">
<option>Choose one...</option>
<option value="is">is</option>
<option value="is-not">is-not</option>
</Form.Select>
</div>
<div className="col-md-4">
<ReactSelect
options={selectables}
isMulti
placeholder="Select comestible..."
/>
</div>
</Row>
);
}
This works in two out of three of the following scenarios:
//In a single instance, fine
<Condition />
//In any number of multiple instances, lovely
<Condition />
<Condition />
//But not as a dynamically created and iterated component:
const [conditions, setConditions] = useState([]);
const increaseConditionCount = () => {
setConditions([...conditions, Condition()]);
};
{conditions.map((component, index) => (
<React.Fragment key={index}>
{ component }
</React.Fragment>
))}
2
Answers
Why you would store Components in a state? you can just store the data in state and passed as a props in needed.
so you can do something like this
Your main problem is that you are calling your component as a function (
Condition()
), not rendering it as an element (<Condition />
). By doing so, you are not actually rendering a separate component, but instead have grafted additional code onto this component. All the hooks called in this process get counted for your current component. As a result the number of hooks being called changes, breaking the rules of hooks.Additionally, while this is not causing your problem, i strongly recommend you do not store elements in state. Doing so makes it very easy to have bugs in which you are rendering stale data. Instead, your state should be the minimum amount of data to describe what you want to render, and then you create the elements during rendering.
From what you described, it looks like a number is the only state you would need:
In many cases you’ll need a more complicated state which describes properties about each condition, so if your situation requires it, feel free to change the state to an array with some data in it, which then gets passed to the
<Condition />
element.