So basically I have a debounced input component that exposes 2 ref methods, one to clear the input's value and one to set it to a string.
The problem is, using the ref method from the parent component does not work.
Code:
import React, { ChangeEvent, forwardRef, useImperativeHandle, useState } from 'react';
import { TextInput } from 'components/common';
import { useDebounce } from 'utilities/CustomHooks';
import Logger from 'utilities/Logger';
export type DebouncedInputRef = {
clearValue: () => void;
setValue: (value: string) => void;
};
export const DebouncedInput = forwardRef(
(
{ onChange, ...textInputProps }: ComponentProps.DebouncedInputProps,
ref,
) => {
const [textInputValue, setTextInputValue] = useState('');
const debouncedOnChange = useDebounce((newValue: string) => {
onChange && onChange(newValue);
}, 1000);
useImperativeHandle(
ref,
(): DebouncedInputRef => {
return {
clearValue: () => {
setTextInputValue('');
},
setValue: (newValue: string) => {
Logger.debug('DebouncedInput', 'setValue fired with', newValue);
setTextInputValue(newValue);
},
};
},
);
return (
<div>
{Logger.debug('DebouncedInput', 'in render value', textInputValue)}
<TextInput
{...textInputProps}
value={textInputValue}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
setTextInputValue(e.target.value);
debouncedOnChange(e.target.value);
}}
/>
</div>
);
},
);
The code being used to call ref method is as follows:
const ListProducts = () => {
const debouncedInputRef = useRef<DebouncedInputRef>();
useEffect(() => {
debouncedInputRef?.current && debouncedInputRef.current.setValue('Test');
}, []);
return (
<DebouncedInput
ref={debouncedInputRef}
/>
);
};
The Logger.debug in setValue prints the incoming value from the parent component.
The Logger.debug in render also runs twice, signifying that re-render occurs right after setTextInputValue is called.
However, the value of the state variable during the render is the same as before.
Basically, setValue runs, but the state variable is not updated, and I have no idea why.
Any pointers will be very welcome.
UPDATE:
Okay, so I got it working. Basically, my ListProducts component had a little extra detail:
const ListProducts = () => {
const [loading, setLoading] = useState(false);
const debouncedInputRef = useRef<DebouncedInputRef>();
const mockApiCall = () => {
setLoading(true);
// call API and then
setLoading(false);
};
useEffect(() => {
debouncedInputRef?.current && debouncedInputRef.current.setValue('Test');
mockApiCall();
}, []);
if (loading) {
return <div>Spinner here</div>;
}
return (
<DebouncedInput
ref={debouncedInputRef}
/>
);
};
What I believe the problem was, the ref was capturing the initial DebouncedInput, and then API was called, which returned the spinner and removed Debounced Input from the DOM.
And later when API was done, it was rendered again, but I guess it was a different DOM element?
I'm not sure why this happened, but it was so. I'd be glad to know what exactly was causing the issue.
Here's a code sandbox example with both, working and not working examples.
If anyone could elaborate on what exactly is the issue in the not working example, I'd be very grateful :)