React Hooks实现input输入框自动focus和select时的一些问题总结

4,072 阅读2分钟

背景

  最近项目改造中需要接入我司自研的Input组件。本来是件so easy的事情,接到一半发现有个场景是在用户点击重命名之后,原来的静态文本会变成输入框供用户重命名,同时输入框要进入focus和select的状态。看了下组件库暴露的props,发现木有autoFocus和autoSelect之类的东东。就开始琢磨着自己在父元素通过Input的ref来实现吧。从此采坑之旅正式开始。

使用ref控制input自动focus

  对于Hooks组件获取子组件的ref有两种方式,1. 直接使用useRef获取。2. 使用callback ref的方式获取。首选试试使用useRef,上代码:

function App () {
  // 使用useRef然后在useEffect里面去focus的方式不靠谱,因为: 1. ref变更的时候不会通知父组件。2. useEffect里面的执行是因为父组件自身状态发生重绘导致,但此时不能保证子组件的ref已经更新。
  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}/>
      </>
  )
}
// Input组件的基本代码。
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)}/>;
});

这个代码点击click按钮的时候,会出现输入框,同时输入框会处于focus和select的状态。测试的时候发现,第一次点击的时候没有focus上。可以看注释,里面写上了分析的原因。
既然不靠谱,那我就用callback ref试试,这也是官方推荐的这种场景的用法 文档。下面看看使用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}/>}
    </>
  )
}

使用callback ref的方式的话可以实现当子组件的ref变化时得到通知,但是有个问题是,如果像示例这样Input组件内部实现的时候value是受控的话,即通过state来控制value,会导致输入时不断触发callback,也就是代码中的console.log会被不断打印。此时无法判断是否首次而是否需要执行focus等逻辑。
而不巧的是,我们的组件库的Input组件的value是受控的,所以callbackRef在用户输入的时候会被不断触发。

结论

  经过几番尝试,题主觉得目前唯一靠谱的解决方案是在Input组件内部实现autoFocus和autoSelect的逻辑然后通过props暴露给父组件。代码如下:

// Input组件的基本代码。
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)}/>;
});

大神们如果有其他更好的方案,也欢迎提出交流~