解决react input受控组件无法输入中文

广告位招租
扫码页面底部二维码联系

刚遇到 react input 受控组件无法输入中文的情况:

<input value={value} onChange={onChange} />

在网上搜索了很多,知道了问题原因,但是大部分文献都实际上没有解决问题。网上大部分文献,只解决了 onChange 的问题,但是没有解决 value 和 onChange 一起用的问题。value 属性导致 input 成为一个完完全全的受控组件。value 的值决定当前 input 的展示内容。因此,网上的文献只提供了一些参考,并不能解决问题本质。

因 value 属性导致无法输入中文的原因在于,react 会用 onChange 这个接口响应任意输入,而如果在此时修改 value 值,就会导致 DOM 的重绘(react 通过实时处理 input 的 DOM 节点的 value 属性值来保证重绘后使用正确的 value,你可以通过开发者工具查看这个 input 节点在输入时的变化),直接导致中文输入法框消失,输入框内变成刚才输入的英文字符(甚至带空格)。onChange 接管 input 的一系列属性,它不是单纯的 change 或 input 事件。当然,在 type=text 上,它主要功能是接管 input,但是 onInput 实际上还是有效。

我在 jqvm 中实际上也遇到这个问题。当时发现通过响应式方法替换真实 DOM 之后,原有的输入态会消失(没有 focus),所以后来解决办法是,保存原始真实 DOM,每次更新后用原来的 DOM 把新绘入的 DOM 给替换掉,再做一些输入态的处理(例如光标位置)。

显然,react 的处理方式不是这样,react 也保存了真实 DOM,但是每次更新 value 之后,会对 DOM 节点的 value 进行复杂加工。不过理论上,react 面临同样的问题,所以它应该也要解决这个输入态问题,这还会牵扯到 focus 和 blur,下面会提到。总而言之,要解决无法输入中文的问题,还是要从真实 DOM 入手解决。

import React, { useRef } from 'react'

export function Input(props) {
  const el = useRef(null)
  const { onChange, onFocus, onBlur, value, defaultValue, ...attrs } = props
  const _value = ('value' in props) ? value : ('defaultValue' in props) ? defaultValue : null
  const handleChange = (e) => {
    if (onChange) {
      onChange(e)
    }
  }
  const forceSetValue = () => {
    if ('value' in props && el.current) {
      const input = el.current
      input.value = value
      input.setAttribute('value', value)
    }
  }

  let inputing = false

  return (
    <input
      {...attrs}
      defaultValue={_value}
      ref={(input) => {
        if (!input) {
          return
        }

        el.current = input
        forceSetValue()
      }}

      // react 在focus/blur时会重新设值,如果没有下面的操作,会导致focus/blur之后,变空
      // TODO 由于是异步操作,会导致文字闪动,光标定位到最末尾
      onFocus={(e) => {
        setTimeout(forceSetValue, 10)
onFocus && onFocus(e) }} onBlur={(e) => { setTimeout(forceSetValue, 150)
onBlur && onBlur(e) }} onCompositionStart={() => { inputing = true }} onCompositionEnd={(e) => { inputing = false handleChange(e) }} onChange={(e) => { if (!inputing) { handleChange(e) } }} /> ) } export default Input

首先第一步,就是借鉴网上资料提供的方法,干掉 value,使用 defaultValue。完成这一步之后,就可以实现正常输入中文了(从这里可以看出无法输入中文的罪魁祸首还是受控机制)。接下来的问题就是

  • 既要能够通过 onChange 把修改后的值传出去,
  • 同时又要能保证 value 属性和真实 DOM 的 value 是同步的。

这里有个问题,input 组件的 defaultValue 属性会带来新的问题,如果由于切换视图当前组件被销毁了,再切换回来,此时由于 defaultValue 属性的逻辑,input 的显示内容不会变成新传入的 defaultValue 的值,而是仍然采用之前的值。假如你一进入界面的时候,defaultValue 是空的(defaultValue={value} value 为空),那么当你输入了一波,修改了 value,然后切走了,但是你想切回来重新继续输入(defaultValue={value} 此时 value 不为空),你会发现 input 内容空空如也。这是 defaultValue 的弊端,它让 input 是非受控的。这也是为什么,我们无论用 value 还是 defaultValue 都无法满足我们正常输入中文的问题。

通过 onChange 把修改后的值传出去这个好办。我在代码里面加了 compositions 控制,但是实际上不需要也无所谓,大不了就是 onChange 传出去的值是不怎么好看的字符串(带空格的拼音),加了 compositions 控制,保证每次 onChange 出去的都是完整的中文字符串,在拼音输入期间并没有触发 onChange,外部也不会接收到。所以,本质上,无法输入中文的问题,和 compositions 无关,这个结论可能和网上大部分资料的说法都相反。

保证当前这个 Input 组件的 value 和真实 DOM 的 value 的同步,则是我这个解决办法的核心,依托真实 DOM,直接将真实 DOM 节点的 value 和 value attribute 都修改为和外部传入的 value 值相等,这样这个组件虽然还是 react 组件,但是已经在 DOM 层面被我魔改了。通过这个魔改,解决了上面所说的 defaultValue 无法控制 input 的问题。现在,在形式上,Input 组件接收 value 和 onChange 两个 prop,它是一个完全支持中文输入的受控组件了。

不过 react 又干了些多余的事,在每次 focus 和 blur 时,都会去检查组件受控情况和 value 值,导致非受控组件的内部状态每次都是初始值(当第一次进来 defaultValue 为空时,那么这个 input 就一直都是空)。所以,我又加了 onFocus 和 onBlur 两个属性,在里面通过一个延时操作,在 react 完成自己的检查之后,我再去修改真实 DOM,解决这个问题。

简单总结,react 受控组件是一个大坑。

已有5条评论
  1. o797 2022-03-04 18:15

    !!!太感谢了,真的好用,困扰了我一天的问题,您的讲解真的是太清楚明朗了!

  2. Nelson 2020-12-10 18:18

    发现第一次输入之后,鼠标移开的话,文字会消失,第二次移入又出现

    • 否子戈 2020-12-13 20:55

      有代码吗?给个代码看下

      • Nelson 2020-12-15 12:39

        已经解决了,是我自己的问题

  3. labike 2020-10-24 11:08