typescriptnext.jsselectmaterial-ui

Out-Of-Range value for MUI select with dependent select values


I'm currently running into this issue where I have one select field that is dependent on the other one.

MUI: You have provided an out-of-range value `1` for the select component.
Consider providing a value that matches one of the available options or ''.

Here are my two fields

<Select
    labelId="originEntityLabel"
    id="originEntityField"
    displayEmpty
    defaultValue={entityForm.originEntity?.name ?? ""}
    label="Entity"
    onChange={(event: SelectChangeEvent) =>  onFieldChangeEntity(event.target.value, "entity")}                            
>
    <MenuItem key="none" value="">
        --None--
    </MenuItem>
    {entityList.map((entity) => {
        return (
            <MenuItem key={entity.name} value={entity.name}>
                {entity.name}
            </MenuItem>
        )
    })}
</Select>

And here's the Select field that I'm having an issue with.

<Select
    labelId="startTierFieldLabel"
    id="startTierField"
    defaultValue={String(entityForm.startTier) ?? ""}
    label="Start Tier"
    onChange={(event: SelectChangeEvent) =>  onFieldChange(event.target.value, "tier")}                            
>
    {tierRange && tierRange.map((tier) => {
        return (
            <MenuItem key={tier} value={tier}>
                Tier {tier}
            </MenuItem>
        )
    })}
</Select>

The tierRange is controlled via the state

 const [tierRange, setTierRange] = useState<number[]>(range(0, 11));

Here are my two on change methods

const onFieldChange = (value: any, field: string) => {
    const currEntity = entityForm as Entity;
    currEntity[field as keyof Entity] = value as never;
    console.log('field change', currEntity);
    setEntityForm(currEntity);
}

const onFieldChangeEntity = (value: any, field: string) => {
    console.log('field change entity', value);
    const entityLookup = entityList.find(entity => entity.name === value);
    const currEntity = entityForm as Entity;
    const minTier = entityLookup ? entityLookup.startTier + 1 : 0;
    currEntity.originEntity = entityLookup;
    currEntity.startTier = Math.max(currEntity.startTier, minTier);
    
    setEntityForm(cloneDeep(currEntity));
    setTierRange(range(minTier, 11));
}

I'm not sure why this error comes up. When I try to log the information, the currEntity has the correct startTier, but I'm not sure why it's not reflecting correctly in the form.

enter image description here

I've also tried creating an entirely separate useEffect on the entityForm state, that then updates the tierRange

useEffect(() => {
    console.log('in use effect entity form');
    const minTier = entityForm.originEntity ? entityForm.originEntity.startTier + 1 : 0;
    setTierRange(range(minTier, 11));
}, [entityForm]);

but that also didn't help the situation. I'm relatively new to typescript and mui, so I'm not sure what might be causing this. (Also, I am open to a better way of doing what I'm attempting - basically having 1 field have dependent values on another field, it would be great to know as well.)


Solution

  • TL; DR: use the value prop of your 2nd <Select> to turn it as a controlled Component, and reset it when its options are changing (at least when, as a result, its current value is no longer part of its new options).

    1. The 2nd <Select> (label="Start Tier") has some value (initially from its defaultValue, or its 1st option value from tierRange)
    2. Your user picks some option in the 1st <Select> (label="Entity"), firing its onChange callback, here onFieldChangeEntity, which changes the tierRange options of the 2nd Select, as well as the entityForm.startTier
    3. However, the latter is only used to set the defaultValue prop of the 2nd Select, hence it affects its value only initially
    4. In case the current value of the 2nd Select is no longer be part of its options, you get your out-of-range error

    In step 2, you need to reset the value (not just the defaultValue) of the 2nd Select at the same time as you change its tierRange options, hence it must be a controlled Component. At least if its current value is no longer part of its new options (but you may choose to reset it in all cases):

    // value of 2nd Select (start tier)
    const [selectedTier, setSelectedTier] = useState(String(entityForm.startTier) ?? tierRange[0]);
    
    // onChange callback of the 2nd Select
    const onFieldChange = (value: any, field: string) => {
      // etc.
      setSelectedTier(value); // We now have to explicitly control the Select value
    }
    
    // onChange callback of 1st Select (entity)
    const onFieldChangeEntity = (value: any, field: string) => {
      // etc.
      const newTierRange = range(minTier, 11);
      setTierRange(newTierRange);
    
      // Reset the value of the 2nd Select (start tier),
      // at least if its current value is no longer in tierRange
      if (!newTierRange.includes(selectedTier)) {
        setSelectedTier(minTier); // or currEntity.startTier
      }
    }
    
    <Select
      value={selectedTier} // Use the Select value prop to turn it into a controlled Component
      label="Start Tier"
      onChange={(event: SelectChangeEvent) => onFieldChange(event.target.value, "tier")}
    >
      {/* tierRange options */}
    </Select>