You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中合并React Hook Form的register与组件Ref异常问题解决

问题描述

我正在使用React Hook Form库。
我有一个来自useForm()的ref——register:

const {register, errors} = useForm<any>({
   reValidateMode: "onChange",
   mode: "onChange",
   nativeValidation: false
});

<CustomTextarea
     register={register({
          required: "This is the required field"
     })}
     errors={errors}
     name="custom_name"
     label="Test label"
     placeholder="Test placeholder" 
     value={customEmails} 
     setDataChanged={handleCustomEmailsChange}
     isBBCode={false}
     isFocus={true}
     rows={4}
/>

另外组件内部还有一个ref:

import React, {useState, useRef, useEffect} from "react";
import {getInstanceError} from "../../utils/getInstanceError";
import ICustomTextarea from "../../typings/ICustomTextarea";
import CustomTooltip from "../Forms/common/CustomTooltip";

const CustomTextarea = ({register, errors, name, label, isDisabled, defaultValue = "", placeholder = "", isBBCode, isFocus, rows, isCopy, setDataCopiedToClipboard, setDataChanged, ...rest}: ICustomTextarea) => {
    const [touch, setTouch] = useState(false);
    const [textareaValue, setTextareaValue] = useState(defaultValue);
    const textareaRef = useRef<HTMLTextAreaElement | null>(null);
    const {
        getMessageError,
        getClassError
    } = getInstanceError(errors);

    const combinedRef = (element: HTMLTextAreaElement | null) => {
        textareaRef.current = register(element);
    };

    useEffect(() => {
        if (isFocus && textareaRef.current) {
            textareaRef.current.focus();

            if (textareaRef.current.value.length === 0) {
                textareaRef.current.select();
            }
        }

        if (isCopy && textareaRef.current) {
            if (textareaRef.current.value.length > 0) {
                textareaRef.current.select();
                navigator.clipboard.writeText(textareaRef.current.value);
                setDataCopiedToClipboard?.();
            }
        }
    }, [isFocus, isCopy]);

    if (isDisabled) {
        return null;
    }

    const errorMsg = getMessageError(name);
    console.log("errorMsg: ", errorMsg);

    const handelBlur = () => {
        setTouch(true);
        console.log("handelBlur!!!");
    };

    const handleChange = (event) => {
        const data = event.target.value;
        setTextareaValue(data);
        setDataChanged?.(data);
    };

    const handleBBCodeButtonClick = (bbcodeTag) => {
        textareaRef.current?.focus();
        const start = textareaRef.current?.selectionStart;
        const end = textareaRef.current?.selectionEnd;
        const selectedText = textareaValue.substring(start, end);
        const newData = textareaValue.substring(0, start) + `[${bbcodeTag}]${selectedText}[/${bbcodeTag}]` + textareaValue.substring(end);
        setTextareaValue(newData);
        setDataChanged?.(newData);
    };

    return (
      <div className={getClassError(name, touch)}>
        {label && <label>{label}</label>}
        <textarea 
            name={name} 
            placeholder={placeholder}
            value={textareaValue}
            ref={combinedRef}
            onBlur={handelBlur}
            onChange={handleChange}
            rows={rows}
            {...rest}
        />
        {errorMsg && <p>{errorMsg}</p>}
        {isBBCode && 
            <div className="bbcode-container">
                <CustomTooltip msg="Bold"><button className="bbcode-btn" id="bold-btn" type="button" name="codeB" onClick={() => handleBBCodeButtonClick("b")}>B</button></CustomTooltip>
                <CustomTooltip msg="Italic"><button className="bbcode-btn" id="italic-btn" type="button" name="codeI" onClick={() => handleBBCodeButtonClick("i")}>i</button></CustomTooltip>
                <CustomTooltip msg="Underline"><button className="bbcode-btn" id="underline-btn" type="button" name="codeU" onClick={() => handleBBCodeButtonClick("u")}>u</button></CustomTooltip>
                <CustomTooltip msg="LineThrough"><button className="bbcode-btn" id="linethrough-btn" type="button" name="codeS" onClick={() => handleBBCodeButtonClick("s")}>S</button></CustomTooltip>
                <CustomTooltip msg="Left"><button className="bbcode-btn" id="left-btn" type="button" name="codeL">L</button></CustomTooltip>
                <CustomTooltip msg="Center"><button className="bbcode-btn" id="center-btn" type="button" name="codeC">C</button></CustomTooltip>
                <CustomTooltip msg="Right"><button className="bbcode-btn" id="right-btn" type="button" name="codeR">R</button></CustomTooltip>
                <CustomTooltip msg="Inline"><button className="bbcode-btn" id="inline-btn" type="button" name="codeInline">Inline</button></CustomTooltip>
                <CustomTooltip msg="Image"><button className="bbcode-btn" id="img-btn" type="button" name="codeImg">Image</button></CustomTooltip>
                <CustomTooltip msg="Quote"><button className="bbcode-btn" id="quote-btn" type="button" name="codeQuote">Quote</button></CustomTooltip>
                <CustomTooltip msg="Link"><button className="bbcode-btn" id="url-btn" type="button" name="codeUrl">Link</button></CustomTooltip>
                <CustomTooltip msg="Code"><button className="bbcode-btn" id="code-btn" type="button" name="codeCode">Code</button></CustomTooltip>
                <CustomTooltip msg="Spoiler"><button className="bbcode-btn" id="spoiler-btn" type="button" name="codeSpoiler">Spoiler</button></CustomTooltip>
                <CustomTooltip msg="List"><button className="bbcode-btn" id="list-btn" type="button" name="codeList">List</button></CustomTooltip>
            </div>
        }
      </div>      
    );
};

export default CustomTextarea;

我尝试合并这两个ref,但失败了。在textarea中输入内容时会跳过第一个字符,仅从第二个字符开始输入;删除内容时则需要操作两次才能生效。

解决方案

问题出在你的combinedRef函数里,register(element)返回的是React Hook Form内部处理后的ref,直接把它赋值给textareaRef.current会覆盖本地ref引用,导致两个ref无法同时正常工作。

  • 正确的合并方式是分别处理两个ref:
    1. 先将DOM元素赋值给本地textareaRef.current
    2. 再调用register(element)让React Hook Form绑定它的ref

修改后的combinedRef函数:

const combinedRef = (element: HTMLTextAreaElement | null) => {
    textareaRef.current = element; // 优先保存本地ref
    register(element); // 再让React Hook Form处理ref
};
  • 额外优化:组件同时维护本地状态textareaValue和父组件传入的value,会导致状态不一致,也是输入异常的潜在诱因,建议统一状态管理:
    • 若为受控组件:直接使用父组件传递的value,移除本地textareaValue状态,更新时调用setDataChanged通知父组件
    • 若为非受控组件:仅接收defaultValue初始化,内部维护状态,不接收父组件的value

受控模式下的组件调整示例:

// 移除本地textareaValue状态,直接使用父组件传入的value
const CustomTextarea = ({register, errors, name, label, isDisabled, value, placeholder = "", isBBCode, isFocus, rows, isCopy, setDataCopiedToClipboard, setDataChanged, ...rest}: ICustomTextarea) => {
    const [touch, setTouch] = useState(false);
    const textareaRef = useRef<HTMLTextAreaElement | null>(null);
    const {
        getMessageError,
        getClassError
    } = getInstanceError(errors);

    const combinedRef = (element: HTMLTextAreaElement | null) => {
        textareaRef.current = element;
        register(element);
    };

    useEffect(() => {
        if (isFocus && textareaRef.current) {
            textareaRef.current.focus();

            if (textareaRef.current.value.length === 0) {
                textareaRef.current.select();
            }
        }

        if (isCopy && textareaRef.current) {
            if (textareaRef.current.value.length > 0) {
                textareaRef.current.select();
                navigator.clipboard.writeText(textareaRef.current.value);
                setDataCopiedToClipboard?.();
            }
        }
    }, [isFocus, isCopy]);

    if (isDisabled) {
        return null;
    }

    const errorMsg = getMessageError(name);

    const handelBlur = () => {
        setTouch(true);
    };

    const handleChange = (event) => {
        const data = event.target.value;
        setDataChanged?.(data); // 直接通知父组件更新状态
    };

    const handleBBCodeButtonClick = (bbcodeTag) => {
        textareaRef.current?.focus();
        const start = textareaRef.current?.selectionStart;
        const end = textareaRef.current?.selectionEnd;
        const selectedText = value.substring(start, end);
        const newData = value.substring(0, start) + `[${bbcodeTag}]${selectedText}[/${bbcodeTag}]` + value.substring(end);
        setDataChanged?.(newData);
    };

    return (
      <div className={getClassError(name, touch)}>
        {label && <label>{label}</label>}
        <textarea 
            name={name} 
            placeholder={placeholder}
            value={value} // 使用父组件传入的value
            ref={combinedRef}
            onBlur={handelBlur}
            onChange={handleChange}
            rows={rows}
            {...rest}
        />
        {errorMsg && <p>{errorMsg}</p>}
        {isBBCode && 
            <div className="bbcode-container">
                <CustomTooltip msg="Bold"><button className="bbcode-btn" id="bold-btn" type="button" name="codeB" onClick={() => handleBBCodeButtonClick("b")}>B</button></CustomTooltip>
                <CustomTooltip msg="Italic"><button className="bbcode-btn" id="italic-btn" type="button" name="codeI" onClick={() => handleBBCodeButtonClick("i")}>i</button></CustomTooltip>
                <CustomTooltip msg="Underline"><button className="bbcode-btn" id="underline-btn" type="button" name="codeU" onClick={() => handleBBCodeButtonClick("u")}>u</button></CustomTooltip>
                <CustomTooltip msg="LineThrough"><button className="bbcode-btn" id="linethrough-btn" type="button" name="codeS" onClick={() => handleBBCodeButtonClick("s")}>S</button></CustomTooltip>
                <CustomTooltip msg="Left"><button className="bbcode-btn" id="left-btn" type="button" name="codeL">L</button></CustomTooltip>
                <CustomTooltip msg="Center"><button className="bbcode-btn" id="center-btn" type="button" name="codeC">C</button></CustomTooltip>
                <CustomTooltip msg="Right"><button className="bbcode-btn" id="right-btn" type="button" name="codeR">R</button></CustomTooltip>
                <CustomTooltip msg="Inline"><button className="bbcode-btn" id="inline-btn" type="button" name="codeInline">Inline</button></CustomTooltip>
                <CustomTooltip msg="Image"><button className="bbcode-btn" id="img-btn" type="button" name="codeImg">Image</button></CustomTooltip>
                <CustomTooltip msg="Quote"><button className="bbcode-btn" id="quote-btn" type="button" name="codeQuote">Quote</button></CustomTooltip>
                <CustomTooltip msg="Link"><button className="bbcode-btn" id="url-btn" type="button" name="codeUrl">Link</button></CustomTooltip>
                <CustomTooltip msg="Code"><button className="bbcode-btn" id="code-btn" type="button" name="codeCode">Code</button></CustomTooltip>
                <CustomTooltip msg="Spoiler"><button className="bbcode-btn" id="spoiler-btn" type="button" name="codeSpoiler">Spoiler</button></CustomTooltip>
                <CustomTooltip msg="List"><button className="bbcode-btn" id="list-btn" type="button" name="codeList">List</button></CustomTooltip>
            </div>
        }
      </div>      
    );
};

export default CustomTextarea;

这样修改后,既解决了ref合并的问题,又统一了状态管理,输入异常的情况就会消失。

内容的提问来源于stack exchange,提问作者Hunter91151

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 15:04:52