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:
- 先将DOM元素赋值给本地
textareaRef.current - 再调用
register(element)让React Hook Form绑定它的ref
- 先将DOM元素赋值给本地
修改后的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
相关产品推荐
相关产品推荐

