skip to Main Content

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


  1. 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

    const [conditionCount, setConditionCount] = useState(0);
    
    const increaseConditionCount = () => {
      setConditionCount(prevCount => prevCount + 1);
    };
    
    return (
      <>
        {[...Array(conditionCount)].map((_, index) => (
          <Condition key={index} />
        ))}
      </>
    );
    
    Login or Signup to reply.
  2. setConditions([...conditions, Condition()]);
    

    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:

    const [conditionsCount, setConditionsCount] = useState(0)
    
    const increaseConditionsCount = () => {
      setConditionsCount(prev => prev + 1)
    }
    
    {Array.from({ length: conditionsCount }).map((_, index) => (
      <Condition key={index}/>
    )}
    

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search