【问题标题】:Modifying user input value in React controlled component在 React 受控组件中修改用户输入值
【发布时间】:2021-09-19 01:19:04
【问题描述】:

我已经被这个错误困扰了很长时间,所以我希望能得到一些帮助。这是一个最小可重现的示例:

import "./App.css";
import React, { useState } from "react";
import $ from "jquery";

function App() {
    const [value, setValue] = useState("");

    const focusHandler = () => {
        $("input").on("keypress", (e) => {
            let copy = value;
            if (e.key === ".") {
                e.preventDefault();
                copy += "    ";
                setValue(copy);
            }
        });
    };

    const blurHandler = (event) => {
        $("input").off("keypress");
        setValue(event.target.value);
    };

    const changeHandler = (event) => {
        setValue(event.target.value);
    };

    return (
        <div>
            <input
                value={value}
                onFocus={focusHandler}
                onBlur={blurHandler}
                onChange={changeHandler}
            />
        </div>
    );
}

export default App;

在输入焦点上,我添加了一个事件侦听器来查找. 按键,并在按下时将一个制表符(4 个空格)附加到输入。但是当我多次按下. 时,输入会卡在第一个选项卡上,并且不会进一步移动(例如,输入永久显示 4 个空格)。使用console.log 显示value 状态似乎没有在focusHandler 中更新并恢复为原始值("")。

重要的一点是,使用this.state 切换到基于类的组件可以使其工作。关于为什么会发生这种情况的任何见解?

【问题讨论】:

  • 你用 React 开发工具检查过状态吗?
  • 为什么要结合 jQuery 和 React?这是一个奇怪的设置——React 已经将侦听器附加到 DOM 元素上,所以在上面扔 jQuery 处理程序似乎只会增加混乱。
  • @Code-Apprentice 是的,我有。我无法从开发工具中获得比 console.log 语句更多的信息
  • @ggorlen 诚然,这不是处理我想做的事情的最干净的方式。你认为混合 jQuery 和 React 是导致错误的原因吗?
  • 即使它有效,我也觉得它是一种反模式。我的意思是,你有两个听众:onChange =&gt; setValue(something); 和 jQuery 有 .on("keypress" =&gt; setValue(somethingElse)。我不清楚 React 应该如何理解这一点,因为按下一个键会触发 change 和 keypress 事件。顺便说一句,keypress is deprecated.

标签: reactjs state


【解决方案1】:

不要将直接 DOM 操作(无论是原生 JavaScript 还是 jQuery)与 React 混合使用。此处无需使用 jQuery 添加事件处理程序,因为您的方法已经是事件处理程序。直接使用即可:

    const focusHandler = (e) => {
        // handle the event here!
    }

【讨论】:

  • 谢谢。这实际上很有意义。事件监听器的event 参数已经为我提供了我需要的一切。
【解决方案2】:

我的解决方案:

const changeHandler = (event) => {
        const key = event.nativeEvent.data;
        if (key === ".") {
            event.preventDefault();
            const initialValue = event.target.value.split(".")[0];
            console.log(initialValue);
            setValue(initialValue + "    ");
        } else {
            setValue(event.target.value);
        }
};

【讨论】:

    【解决方案3】:

    正如 cmets 中提到的,jQuery 是 wrong tool for the job。引入 jQuery 与直接调用 DOM 方法相同:它绕过了 React 并在 React 已经为您提供的监听器之上添加了额外的监听器。如果您从多个与 React 无关的处理程序设置状态,则可能会出现不当行为。使用 React 后,使用它提供的工具(引用、效果、处理程序)来解决问题。

    最坏的情况是当一种方法似乎工作,然后在生产中失败,在其他人的机器/浏览器上,从类重构为钩子或反之亦然,在不同版本的 React 中,或 1000 个用户中的 1 个。保持良好的 API React 可以更好地保证您的应用程序将正确运行。

    Controlled component

    要操作输入值,您可以使用both onKeyDown and onChange listenersonKeyDown 首先触发。在onKeyDown 内部调用event.preventDefault() 将阻止更改事件,并确保每次击键时只调用一次setState 以获取受控输入值。

    input cursor moves to the end after the component updates 的问题(参见相关的GitHub issue)。解决此问题的一种方法是在对字符串进行侵入性更改时手动调整光标位置,方法是添加状态以跟踪光标并使用 ref 和 useEffect 设置 selectionStartselectionEnd输入元素的属性。

    由于渲染后的异步性,这会导致简短的blinking effect,因此这不是一个很好的解决方案。如果您总是附加到值的末尾,您假设用户不会像其他答案那样移动光标,或者您希望光标在末尾完成,那么这不是问题,但是这个假设一般情况下不成立。

    一种解决方案是使用useLayoutEffect,它在重绘之前同步运行,消除了眨眼。

    useEffect:

    const {useEffect, useRef, useState} = React;
    
    const App = () => {
      const [value, setValue] = useState("");
      const [cursor, setCursor] = useState(-1);
      const inputRef = useRef();
      const pad = ".    ";
      
      const onKeyDown = event => {
        if (event.code === "Period") {
          event.preventDefault();
          const {selectionStart: start} = event.target;
          const {selectionEnd: end} = event.target;
          const v = value.slice(0, start) + pad + value.slice(end);
          setValue(v);
          setCursor(start + pad.length);
        }
      };
      
      const onChange = event => {
        setValue(event.target.value);
        setCursor(-1);
      };
      
      useEffect(() => {
        if (cursor >= 0) {
          inputRef.current.selectionStart = cursor;
          inputRef.current.selectionEnd = cursor;
        }
      }, [cursor]);
    
      return (
        <div>
          <p>press `.` to add 4 spaces:</p>
          <input
            ref={inputRef}
            value={value}
            onChange={onChange}
            onKeyDown={onKeyDown}
          />
        </div>
      );
    };
    
    ReactDOM.render(<App />, document.body);
    input {
      width: 100%;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

    useLayoutEffect:

    const {useLayoutEffect, useRef, useState} = React;
    
    const App = () => {
      const [value, setValue] = useState("");
      const [cursor, setCursor] = useState(-1);
      const inputRef = useRef();
      const pad = ".    ";
      
      const onKeyDown = event => {
        if (event.code === "Period") {
          event.preventDefault();
          const {selectionStart: start} = event.target;
          const {selectionEnd: end} = event.target;
          const v = value.slice(0, start) + pad + value.slice(end);
          setValue(v);
          setCursor(start + pad.length);
        }
      };
      
      const onChange = event => {
        setValue(event.target.value);
        setCursor(-1);
      };
      
      useLayoutEffect(() => {
        if (cursor >= 0) {
          inputRef.current.selectionStart = cursor;
          inputRef.current.selectionEnd = cursor;
        }
      }, [cursor]);
    
      return (
        <div>
          <p>press `.` to add 4 spaces:</p>
          <input
            ref={inputRef}
            value={value}
            onChange={onChange}
            onKeyDown={onKeyDown}
          />
        </div>
      );
    };
    
    ReactDOM.render(<App />, document.body);
    input {
      width: 100%;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

    Uncontrolled component

    这是另一个使用不受控制的组件的尝试。这不存在闪烁问题,因为 DOM 元素的 .value 属性与 .selectionStart 属性同时设置,并在同一次重绘中呈现。

    const App = () => {
      const pad = ".    ";
      
      const onKeyDown = event => {
        if (event.code === "Period") {
          event.preventDefault();
          const {target} = event;
          const {
            value, selectionStart: start, selectionEnd: end,
          } = target;
          target.value = value.slice(0, start) + 
                         pad + value.slice(end);
          target.selectionStart = start + pad.length;
          target.selectionEnd = start + pad.length;
        }
      };
    
      return (
        <div>
          <p>press `.` to add 4 spaces:</p>
          <input
            onKeyDown={onKeyDown}
          />
        </div>
      );
    };
    
    ReactDOM.render(<App />, document.body);
    input {
      width: 100%;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

    【讨论】:

      猜你喜欢
      • 2019-02-22
      • 2021-07-29
      • 2020-08-04
      • 2018-10-02
      • 2021-09-02
      • 2018-11-15
      • 1970-01-01
      • 2020-11-04
      • 2022-11-28
      相关资源
      最近更新 更多