• 聊聊React中的受控组件与非受控组件的用法
  • 发布于 1周前
  • 57 热度
    0 评论
  • 飛雲
  • 0 粉丝 56 篇博客
  •   
引言
在如今的前端开发里,React 可是响当当的角色,是咱搭建用户界面的得力帮手。一碰到表单处理和用户输入交互这些事儿,受控组件和非受控组件就派上大用场了,它们就像是两种不一样的工具,各有各的厉害之处。要是咱能把它们摸透了,知道啥时候用啥,那开发出来的 React 应用肯定既好用又靠谱,用户体验也差不了。
一、受控组件
(一)定义咋来的
受控组件,简单讲,就是让表单元素的值跟 React 组件的state绑得死死的,完全同步。用户在表单上不管是打字还是选东西,都像触动了个开关,马上让组件状态更新,接着组件就重新渲染一遍,保证表单显示的值和组件里存的值一模一样,一点差错都没有。这其实就是 React 响应式编程的一种典型做法,用状态这个 “老大” 管着表单元素,让一切都井井有条。
(二)啥时候用它
1.需要实时查错、给反馈的时候 :像用户注册、登录,还有提交各种信息的表单,对数据准不准要求可高了。就说电商平台的注册表单吧,用户填电子邮箱,刚输完,组件就能立马用正则表达式瞅瞅格式对不对。要是不对,马上在输入框旁边亮个红灯,给个错误提示,还能拦住表单不让提交。这靠的就是受控组件和状态的紧密配合,在onChange事件回调里,实时更新状态,再根据新状态决定显示啥反馈,这么一来,用户就能及时改错误,表单填得又快又准。

2.要让界面联动起来的时候 :想象一下在线文档编辑工具,用户调字体大小,文本输入框里的字得跟着变吧,不光如此,标题栏字体、预览区字体,还有排版样式,好多相关的 UI 元素都得一起变。受控组件在这儿就起大作用了,把字体大小这些关键信息放在组件状态里管着。onChange事件一触发,就统一更新状态、重新渲染 UI,各个相关部分就像配合默契的齿轮,一起转起来,给用户的操作提供顺滑流畅的反馈。
(三)代码咋写的,举俩例子
先看个多功能文本输入框的例子:
import React, { useState } from'react';
// 堆代码 duidaima.com
function ControlledInput() {
    const [inputValue, setInputValue] = useState('');
    const [isFocused, setIsFocused] = useState(false);
    const [charCount, setCharCount] = useState(0);

    const handleChange = (e) => {
        const newValue = e.target.value;
        setInputValue(newValue);
        setCharCount(newValue.length);
    };

    const handleFocus = () => {
        setIsFocused(true);
    };

    const handleBlur = () => {
        setIsFocused(false);
    };

    return (
        <div>
            <input
                type="text"
                value={inputValue}
                onChange={handleChange}
                onFocus={handleFocus}
                onBlur={handleBlur}
            />
            <p>Character count: {charCount}</p >
            {isFocused? (
                <span style={{ color: 'blue', fontWeight: 'bold' }}>Input is focused</span>
            ) : null}
        </div>
    );
}
export default ControlledInput;
在这个例子里,文本输入框的value属性就像和组件的inputValue状态锁在一起了,不离不弃。用户一输入,onChange事件立马响应,handleChange函数上场,不光更新inputValue,让输入框显示最新内容,还顺便算出字符个数,更新charCount状态,在界面上实时显示字符统计。同时,onFocus和onBlur事件分别管着isFocused状态,输入框聚焦就显示个蓝字提示,失焦就不显示,通过巧妙地操控状态,把输入框的各种行为和界面反馈安排得明明白白。

再看个更复杂的,模拟在线笔记编辑,有表单验证和自动保存功能:
import React, { useState, useEffect } from'react';
function NoteEditor() {
    const [noteContent, setNoteContent] = useState('');
    const [isValidNote, setIsValidNote] = useState(true);
    const [isSaving, setIsSaving] = useState(false);
    const [lastSavedTime, setLastSavedTime] = useState(null);

    const handleNoteChange = (e) => {
        const newContent = e.target.value;
        setNoteContent(newContent);
        const noteRegex = /^.{1,100}$/;
        setIsValidNote(noteRegex.test(newContent));
    };

    useEffect(() => {
        if (isValidNote && noteContent) {
            const interval = setInterval(() => {
                setIsSaving(true);
                // 模拟保存操作,实际可能是API调用
                setTimeout(() => {
                    setIsSaving(false);
                    setLastSavedTime(Date.now());
                }, 2000);
            }, 5000);
            return() => clearInterval(interval);
        }
    }, [isValidNote, noteContent]);

    return (
        <div>
            <textarea
                value={noteContent}
                onChange={handleNoteChange}
            />
            {isValidNote? null : (
                <span style={{ color:'red' }}>Note must be between 1 and 100 characters</span>
            )}
            {isSaving? (
                <span style={{ color: 'orange', fontWeight: 'bold' }}>Saving...</span>
            ) : null}
            <p>Last saved: {lastSavedTime? new Date(lastSavedTime).toLocaleString() : 'Never'}</p >
        </div>
    );
}
export default NoteEditor;
这里,textarea是核心表单元素,它的值和noteContent状态绑得紧紧的。onChange回调不光更新内容、实时查错,还根据结果决定错误提示的显示。同时,用useEffect钩子监听内容合法性和有没有值,条件满足就启动定时自动保存,模拟真实应用里的数据保存操作,保存过程中切换状态显示保存状态,最后记录并显示上次保存时间,全方位展示了受控组件在复杂业务场景下的本事。

二、非受控组件
(一)定义是啥意思
非受控组件呢,就像是个比较随性的家伙,表单元素的值由 DOM 自己管着,React 组件平时不插手,就等关键时候,用ref这个 “工具” 去拿 DOM 元素的值。它不折腾那些复杂的状态同步,尊重 DOM 本来的样子,和表单元素打交道简单直接,就像是给咱开了条捷径。
(二)适合啥情况
1.集成第三方表单的时候 :现在前端的工具、库特别多,有些第三方表单库功能超强。要是引入react-bootstrap表单组件库里的高级日期选择器,硬要把它改成受控组件,那麻烦可就大了,不光可能碰到兼容性问题,代码还会变得超级复杂。这时候非受控组件就好用了,用ref轻松拿到日期选择器选的值,顺顺利利就把它集成到 React 项目里,就像搭了座桥,让不同的东西能一起干活。

2.处理文件上传的时候 :文件上传这事儿有点特殊,因为浏览器的安全策略,还有文件对象本身的性质,受控组件不好使。就说社交媒体应用里的图片上传功能,用户选了本地照片,文件输入框里的文件对象不能像普通文本一样让 React 状态随便改,更不能直接设置它的值。非受控组件就靠ref精准找到文件输入框,提交的时候一把抓住文件对象,后面就能把文件上传到服务器,这就把文件上传的难题给解决了。
(三)代码咋弄,看俩例子
先看个简单的非受控组件文本输入框:
import React, { useRef } from'react';
function UncontrolledInput() {
    const inputRef = useRef(null);
    const [submittedValue, setSubmittedValue] = useState('');
    const handleSubmit = (e) => {
        e.preventDefault();
        const inputValue = inputRef.current.value;
        setSubmittedValue(inputValue);
        console.log('You entered:', inputValue);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                ref={inputRef}
            />
            <button type="submit">Submit</button>
            <p>Submitted: {submittedValue}</p >
        </form>
    );
}
export default UncontrolledInput
在这个例子里,useRef搞出来的inputRef就像一根线,把 React 组件和 DOM 里的文本输入框连上了。平时用户输入,输入框的值在 DOM 里自己变,React 不管。等提交按钮一点,handleSubmit函数靠inputRef.current.value把 DOM 里的值拿过来,放到submittedValue状态里存着,还显示出来,就这么实现了从 DOM 到 React 的过渡。

再看个进阶的多文件上传例子:
import React, { useRef, useState } from'react';
function MultiFileUpload() {
    const fileInputRef = useRef(null);
    const [fileList, setFileList] = useState([]);
    const handleUpload = (e) => {
        e.preventDefault();
        const files = fileInputRef.current.files;
        if (files) {
            const fileArray = Array.from(files);
            setFileList(fileArray);
            console.log('Uploading files:', fileArray.map((file) => file.name));
            // 后续可添加文件上传至服务器的逻辑
        }
    };

    return (
        <form onSubmit={handleUpload}>
            <input
                type="file"
                multiple
                ref={fileInputRef}
            />
            <button type="submit">Upload</button>
            <ul>
                {fileList.map((file, index) => (
                    <li key={index}>{file.name}</li>
                ))}
            </ul>
        </form>
    );
}
exportdefault MultiFileUpload;
这个多文件上传的例子里,fileInputRef紧紧跟着文件输入框。用户批量选文件再提交的时候,handleUpload。函数靠fileInputRef.current.files拿到文件,转成数组存到fileList状态里,实时显示要上传的文件列表,既尊重了文件输入框 DOM 的原生行为,又把它融进了 React 的数据管理,给文件上传流程搭了个好框架。

三、特殊富文本组件那些事儿
(一)组件特性唠唠
咱就说 CKEditor、TinyMCE 这些富文本编辑器哈,那里面老复杂了,就跟个百宝箱似的,啥编辑功能都有,样式管理也是五花八门的。跟普通的表单输入组件一比,那状态管理的道儿可完全不一样。这些富文本组件都有自个儿的 “一亩三分地”,守着一套独立的内容数据结构,有时候用 HTML 片段存文本内容、格式啥的,有时候又鼓捣出些自定义的格式。而且啊,手里还攥着专门用来摆弄这些内容的 API,就跟攥着个独家秘籍似的,外人要是不仔细研究,根本整不明白咋回事儿。
(二)伪装成受控组件的窍门
1.数据绑定这点事儿:
要是想让它跟受控组件有点像,可得动点脑筋。在 React 里,咱先用 useState 钩子整出一个状态变量,这变量就好比是富文本组件内容状态的 “替身”,专门模拟它。然后呢,就得把富文本组件自带的获取内容的 API 利用起来,就拿 CKEditor 来说,它有个 editor.getData() 的招儿,能把编辑器里的内容数据拽出来。瞅准机会,像是组件刚挂到页面上,或者用户在里头一顿操作完事儿了,麻溜地把抓取到的富文本内容赋值给之前弄的模拟状态变量。这么一来,旁人瞅着,这富文本组件的值就好像被这个状态变量拿捏得死死的,跟受控组件里用状态管值那套路差不多。
2.事件处理的小妙招:
富文本组件可不是个 “消停” 的玩意儿,时不时就来点 “小动作”,像内容改了、选区变了,这些情况一冒头,对应的事件就触发了。咱就得在这些事件的回调函数里做文章,一旦富文本内容有个风吹草动,回调函数立刻启动。在里头调用相应的 API 把最新的内容搞到手,再火急火燎地更新到之前模拟的那个状态变量里。这么一来,整个表单处理的逻辑就能以这个状态变量为核心,顺顺当当进行后续操作,不管是验证也好,提交也罢,就跟摆弄普通受控组件,靠事件改变状态来统一管表单逻辑没啥两样。
(三)统一处理表单的好处杠杠的
1.数据验证那叫一个省心:
把这特殊富文本组件伪装成受控组件之后,那可就跟打开了方便之门似的。验证富文本内容的时候,再也不用绞尽脑汁给它单搞一套规则了,直接把它跟普通表单组件,像普通文本输入框、下拉框这些,同等对待,用一样的验证逻辑就行。比如说,给字数设个限,看看有没有特定的关键词,一股脑儿扔到统一的表单验证函数里,一次性搞定,多省心啊,开发效率 “嗖” 地就上去了。
2.提交数据方便得没治了:
轮到提交表单数据的时候,那更是便利得很。所有的数据,富文本内容肯定也在里头,都能从咱们精心管着的那个统一状态里轻轻松松拿到手,再按照统一的格式,比如说弄成 JSON 格式,把所有表单数据打包整合好,发给后端。后端接收处理起来也轻松,咱们这边代码结构也变得清清爽爽,后期维护的时候,眼睛一扫就能明白,再也不用在乱糟糟的代码里 “抓瞎” 找问题了。

(四)代码示例瞅瞅
import React, { useState, useEffect } from'react';
import CKEditor from'@ckeditor/ckeditor5-react';
import ClassicEditor from'@ckeditor/ckeditor5-build-classic';

const MyForm = () => {
    // 模拟富文本内容的状态,相当于受控组件的状态值
    const [richTextContent, setRichTextContent] = useState('');

    useEffect(() => {
        // 可以在这里进行一些初始化操作,比如给富文本内容设置初始值等
        setRichTextContent('<p>Initial content</p >');
        }, []);

    // 处理富文本内容改变的函数,模拟受控组件的事件处理更新状态
    const handleRichTextChange = (event, editor) => {
        const data = editor.getData();
        setRichTextContent(data);
        };

    const handleSubmit = (e) => {
        e.preventDefault();
        // 在这里可以进行表单验证等操作,比如简单验证富文本内容长度
        if (richTextContent.length < 10) {
            alert('富文本内容字数过少,请补充内容');
            return;
        }
        // 假设这里是将表单数据提交给后端的逻辑,此处简单打印
        console.log('提交的富文本内容:', richTextContent);
        };

    return (
        <form onSubmit={handleSubmit}>
            {/* 使用 CKEditor 富文本编辑器,并将其伪装成受控组件的形式 */}
            <CKEditor
                editor={ClassicEditor}
                data={richTextContent}
                onChange={handleRichTextChange}
            />
            <button type="submit">提交表单</button>
        </form>
    );
};
exportdefault MyForm;
在这段代码里:
首先,用 useState 钩子搞出来的 richTextContent 状态变量,那可是重中之重,就如同受控组件里掌控全局的那个状态值,专门用来模拟富文本组件对应的内容状态。
其次,useEffect 钩子在组件刚启动那阵儿就派上用场了,就像是个 “打头阵的”,咱们可以趁着这个时候给富文本内容设个初始值,让后面的事儿有个好开头。
再者,handleRichTextChange 函数时刻盯着富文本内容有没有变化。只要一有变动,比如用户噼里啪啦敲了些字进去,或者改了改格式,它就立马行动。在函数里头,借助 editor.getData() 拿到最新的富文本内容,接着毫不犹豫地把它更新到 richTextContent 状态变量中,这做法,跟受控组件靠事件改变状态的玩法如出一辙。

最后到了表单提交的 handleSubmit 函数这儿,富文本内容和其他表单组件数据一样,都得按规矩来,先接受验证,合格了才能提交。要是富文本内容长度小于 10,就弹出个框提醒用户多写点,要是没问题,就可以把包括富文本内容在内的所有数据按照统一的流程发出去。这么一搞,整个表单处理流程就被巧妙地统一起来了,是不是还挺得劲儿的?

不过不同的富文本编辑器都有自己的 “脾气”,API 和使用方式各有不同。真到用的时候,要根据选的编辑器,把代码逻辑这儿调调,那儿整整,不过总体思路万变不离其宗,就是把它的内容与模拟状态绑一块儿,再利用它的行为机制更新状态,顺顺利利融入统一的表单处理流程当中,这事儿就算成了。

四、受控组件与非受控组件的对比
(一)数据咋流动的
受控组件:就像建了个双向高速路,数据从用户操作开始,流到组件状态里存着,然后 React 根据状态更新表单元素,又反馈给用户,形成一个闭环,实时响应。每次交互都是状态和视图一起变,保证用户看到的和操作的完全同步。
非受控组件:数据走的是单向路,大多时候在 DOM 里自己流转,表单元素自己管自己的值。React 组件就偶尔用ref去 DOM 那儿取个值,像是偶尔采个蜜的蜜蜂,和 DOM 是种松散关系。
(二)代码复杂不复杂
受控组件:因为要精细维护状态,调度各种复杂逻辑,代码结构就像个精密仪器,一环扣一环。从一开始设状态,到onChange、onBlur等好多事件回调里更新状态,再到根据不同状态显示不同 UI 组件,每个环节都得精心弄。特别是处理多个表单元素联动、深度验证的时候,状态树变得老复杂了,虽然掌控力强,但调试、维护起来不容易,得技术好才能驾驭。
非受控组件:代码风格简单直接,不用搭复杂的状态体系,直接用ref和 DOM 交流,少了中间那些逻辑层。不过在大项目里,要是老用ref在 DOM 和 React 之间穿梭,代码就像散了架,逻辑不连贯,维护起来也麻烦,得看情况用,别给自己挖坑。
(三)啥时候用哪个好
受控组件:要是追求极致的交互体验,对数据管控要求特别精细,那就选它。像金融产品风险评估、医疗信息录入这些重要又复杂的表单,数据准不准至关重要,实时反馈、联动调整都不能少。受控组件靠状态驱动,把用户输入、验证规则、UI 显示绑得紧紧的,虽然复杂,但可靠,能给关键业务护航。
非受控组件:要是碰到特殊情况,像集成第三方表单有兼容性问题,或者处理文件上传这种受限的事儿,它就好使。它不纠结 React 状态,拥抱 DOM 原生力量,用最小代价把功能实现,给项目推进和满足多样需求提供灵活办法。

五、总结
在 React 开发里,受控组件和非受控组件就像一对好搭档,各有千秋,互相补充。咱开发者得从项目全局考虑,看业务需求是啥样,团队技术水平咋样,再决定用哪个。把它们的原理、用法、适用场景都搞清楚了,开发表单和用户输入交互功能的时候,就能得心应手。复杂业务咱能处理得稳稳当当,特殊场景也能轻松应对。

用户评论