Reputation: 93
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.
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 :)
Upvotes: 1
Views: 7723
Reputation: 53894
First lets start with small tweaks:
useImperativeHandle
should be used with dep array, in this case its empty:
useImperativeHandle(...,[]);
useEffect
should always have a SINGLE responsibility, according to your logic, you just want to set ref value on mount, add another useEffect
(doesn't effect anything in this specific example)
// API CALl
useEffect(callback1, []);
// Ref set on mount
useEffect(callback2, []);
Then, from the theoretic side, on every state change, React compares React Sub Node trees to decide if an UI update needed (see Reconciliation).
Those are two different React Nodes:
// #1
<div>
{renderContent()}
</div>
// #2
<div>
<DebouncedInput ref={myCompRef} />
{loading && <span>Loading right now</span>}
</div>
Since in #1, the function called renderContent()
on every render, therefore you actually re-mount the node on every render.
Why your code didn't work? Because you called some logic on parent MOUNT:
useEffect(() => {
myCompRef.current?.setValue("Value set from ref method");
}, [])
If the ref mounted, it worked, if its not, there wasn't function call.
But in context of #1 tree, you instantly unmounted it on next render, therefore you reset the inner value
state.
In context of #2, its the same React node, therefore React just updated it.
Upvotes: 1