background
The Input component developed by our company needs to be connected in the recent project transformation. It was originally so easy, but I found a scene where after the user clicked rename, the original static text would be changed into an input box for the user to rename, and the input box would enter the states of Focus and SELECT. Look at the props exposed component library. There is no autoFocus or autoSelect. Start thinking about doing it yourself in the parent element via the Input ref. From then on, the trip to the pit began.
Use ref to control input automatic focus
There are two ways to get the refs of child components for the Hooks component: 1. Use useRef directly to get the refs. 2. Obtain by callback ref. First try using useRef:
Function App () {// use the useRef to focus on the useEffect. 2. UseEffect is executed because the state of the parent component is redrawn, but there is no guarantee that the ref of the child component has been updated. const ref = useRef(null); const [isShowInput, setIsshowInput] = useState(false); useEffect(() => { ref.current? .focus(); ref.current? .select(); }, [ref.current]); return( <> <button onClick={() => setIsshowInput(! isShowInput)}>click</button> <Input ref={ref}/> </> ) }Copy the code
// The basic code for the Input component. const Input = forwardRef((props, ref) => { const inputEl = useRef(null); const [value, setValue] = useState(''); useImperativeHandle(ref, () => inputEl.current); return <input ref={inputEl} type="text" value={value} onChange={(e) => setValue(e.target.value)}/>; });Copy the code
When the code clicks on the Click button, an input field appears that is in the focus and SELECT states. When testing, I found that there was no focus on the first click. If you look at the notes, there’s a reason for the analysis. Since this is not possible, I will try callback ref, which is the official recommended usage document for this scenario. Here’s the code that uses callback ref:
function App () {
const [isShowInput, setIsshowInput] = useState(false);
const callbackRef = useCallback((node) => {
console.log('callbackRef', node);
node && node.focus();
node && node.select();
}, []);
function onChange(e) {
setValue(e.target.value);
}
return(
<>
<button onClick={() => setIsshowInput(!isShowInput)}>click</button>
{isShowInput && <Input ref={callbackRef} autoFocus={true}/>}
</>
)
}
Copy the code
Using callback ref allows you to be notified when the ref of the child component changes, but the problem is that if the value is controlled internally, as in the example, controlling the value by state causes the callback to be triggered repeatedly. The console.log in the code is printed over and over again. At this point, it is impossible to determine whether the logic such as focus needs to be executed for the first time. Unfortunately, the value of our component library’s Input component is controlled, so the callbackRef is constantly fired when the user enters.
conclusion
After several attempts, the only viable solution is to implement the logic of autoFocus and autoSelect inside the Input component and expose it to the parent component via props. The code is as follows:
// The basic code for the Input component. const Input = forwardRef((props, ref) => { const inputEl = useRef(null); const [value, setValue] = useState(''); useImperativeHandle(ref, () => inputEl.current); useEffect(() => { props.autoFocus && inputEl.focus(); props.autoSelect && inputEl.select(); }); return <input ref={inputEl} type="text" value={value} onChange={(e) => setValue(e.target.value)}/>; });Copy the code
If you have other better solutions, welcome to communicate ~