Search code examples
reactjsreact-propsreact-functional-componentrerender

infinite change of constant array as prop in reactjs when parent's state is changed


so i have this custom component :

export interface PickerProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'onChange' | 'name' | 'onInput'> {
    onChange?: OnChangeEventHandler<HTMLSelectElement>,
    onChangeValue?: OnChangeValueHandler,
    onInput?: OnInputEventHandler<HTMLSelectElement>,
    onInputValueChange?: OnInputChangeHandler,
    name: string,
    label: string,
    emptyOpt?: boolean,
    optGroups: {
        optTitle?: string,
        options: (Omit<OptionHTMLAttributes<HTMLOptionElement>, 'value'> & {
            value: string
        })[]
    }[]
}

export const Picker = ({
    disabled = false,
    autoComplete = "autoComplete",
    onChange = () => { },
    onChangeValue = () => { },
    onInput = () => { },
    onInputValueChange = () => { },
    autoFocus = false,
    size,
    name,
    form = '',
    optGroups = [],
    label,
    emptyOpt = true,
}: PickerProps) => {
    const [pickerValue, setPickerValue] = useState<string>();
    const pickerRef = useRef<HTMLSelectElement>(null);
    const changeHandler = (e: ChangeEvent<HTMLSelectElement>) => {
        const targetValue = e.target.value;
        onChange(e, name);
        onChangeValue(targetValue, name);
        // setPickerValue(targetValue);
    }

    const onInputHandler = (e: FormEvent<HTMLSelectElement>) => {
        onInput(e, name);
        //@ts-ignore
        onInputValueChange(e.target.value, name);
        //@ts-ignore
        setPickerValue(e.target.value, name);
    }

    useEffect(() => {
        const selectedValuesArr = [...optGroups].map(optGroup => optGroup.options
            .filter(opt => opt.selected ?? false)
            .map(sOPT => sOPT.value))
            .reduce((pv, cv) => [...pv, ...cv], []);
        const defValue = selectedValuesArr[0] ?? '';
        setPickerValue(defValue);
        setTimeout(() => pickerRef.current?.dispatchEvent(new Event('input', { bubbles: true })), 2);
    }, [optGroups, name]);

    return <>
        <div className="picker-box" tabIndex={0}>
            <span>
                {label}
            </span>
            <select
                ref={pickerRef}
                disabled={disabled}
                autoFocus={autoFocus}
                size={size}
                name={name}
                form={form}
                autoComplete={autoComplete}
                onChange={changeHandler}
                multiple={false}
                value={pickerValue}
                onInput={onInputHandler}
            >
                {emptyOpt && (
                    <option value="">انتخاب کنید</option>
                )}
                {optGroups.map((optGroup, key) => (
                    <optgroup key={key} label={optGroup.optTitle ?? 'انتخاب کنید'}>
                        {optGroup.options.map((opt, index) =>

                            <option
                                className={pickerValue === opt.value ? ' selected' : ''}
                                key={index}
                                disabled={opt.disabled}
                                value={opt.value}
                            >
                                {opt.label ?? ''}
                            </option>
                        )}
                    </optgroup>
                ))}
            </select>
        </div>
    </>;
};

and here is an example of how i am using

                <Picker
                    emptyOpt={false}
                    label="تخفیف نقدی"
                    name="cashFlag"
                    optGroups={[
                        {
                            options: [
                                { value: 'true', label: 'دارد', selected: true },
                                { value: 'false', label: 'ندارد' },
                            ]
                        }
                    ]}
                    onInputValueChange={formDataHandler}
                />

and this is my form handler :

    const formDataHandler = useCallback((value: string, name: string) => {
            setFormState(fs => {
                const temp = { ...fs };
                temp[name] = value;
                return { ...temp };
            });
    }, []);

the array given to the optGroups prop is used to render the picker component . the problem is , whenever the formDataHandler function which is the form data change handler , is executed . picker components detect optGroups prop changed . while it's not. arrays given to the Picker components are constant and are not being changed (except two) and more important and weird thing is that . they all act as expected until the state of parent component is changed ! which is clearly have nothing to do with the Picker components .

i need help to find out why optGroups are being detected as new arrays as soon as parent state is changed , while the given arrays are not being changed . (which causes them to call their changeHandler and an infinite loop begins) first i thought this is my problem :

ReactJS - When I change a state, props change too

but , after years of doing this i am sure inside the Picker component , no where the array given as optGroups is being changed .

thanks for any help .


Solution

  • This issue is you are creating a new optGroups array whenever the parent component renders creating a new reference each time.

    <Picker
      emptyOpt={false}
      label="تخفیف نقدی"
      name="cashFlag"
      // you are creating a brand new array here each time on re-render. 
      // The reference of the array here changes whenever this component re-renders.
      optGroups={[
        {
          options: [
            { value: "true", label: "دارد", selected: true },
            { value: "false", label: "ندارد" },
          ],
        },
      ]}
      onInputValueChange={formDataHandler}
    />;
    

    Declare a constant optGroupsOptions outside your Parent component or in a separate file and use it

    const optGroupsOptions = [
      {
        options: [
          { value: "true", label: "دارد", selected: true },
          { value: "false", label: "ندارد" },
        ],
      },
    ];
    
    // Use the optGroupsOptions 
    
    <Picker
    emptyOpt={false}
    label="تخفیف نقدی"
    name="cashFlag"
    optGroups={optGroupsOptions}
    onInputValueChange={formDataHandler}
    />
    

    Another approach is to make sure the reference doesn't change between the reference by using the useMemo hook .

    const memoizedOptGroups = useMemo(
      () => [
        {
          options: [
            { value: "true", label: "دارد", selected: true },
            { value: "false", label: "ندارد" },
          ],
        },
      ],
      []
    );
    

    Now use this memoizedOptGroups

    <Picker
      emptyOpt={false}
      label="تخفیف نقدی"
      name="cashFlag"
      optGroups={memoizedOptGroups}
      onInputValueChange={formDataHandler}
    />;