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 .
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}
/>;