MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 中的受控与非受控组件解析

2023-11-197.6k 阅读

1. 基本概念

在 React 开发中,理解受控组件与非受控组件是构建高效、灵活用户界面的关键。

1.1 受控组件

受控组件是指其状态由 React 组件的 state 控制的表单元素。在传统的 HTML 表单中,表单元素(如 <input><textarea><select> 等)会维护自身的状态,并在用户输入时更新。然而,在 React 中,我们通过将表单元素的值绑定到组件的 state 上,使 React 成为“单一数据源”。这样,每当表单元素的值发生变化时,React 组件的 state 也会相应更新,并且反过来,state 的变化会反映在表单元素的显示上。

例如,创建一个简单的文本输入框受控组件:

import React, { useState } from 'react';

function ControlledInput() {
  const [inputValue, setInputValue] = useState('');

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

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={handleChange}
      />
      <p>输入的值为: {inputValue}</p>
    </div>
  );
}

export default ControlledInput;

在上述代码中,inputValue 存储在 useState 钩子创建的 state 中,input 元素的 value 属性绑定到 inputValueonChange 事件触发 handleChange 函数,该函数更新 inputValue。这样,输入框的值始终与 inputValue 保持同步,实现了受控的效果。

1.2 非受控组件

非受控组件则与受控组件相反,它的状态不受 React 组件的 state 直接控制。非受控组件依赖于 DOM 本身来存储其内部状态。在 React 中,我们通常使用 ref 来访问非受控组件的当前值。

例如,创建一个非受控的文本输入框:

import React, { useRef } from 'react';

function UncontrolledInput() {
  const inputRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('输入的值为: ', inputRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        ref={inputRef}
      />
      <button type="submit">提交</button>
    </form>
  );
}

export default UncontrolledInput;

这里通过 useRef 创建了 inputRef,并将其应用到 input 元素的 ref 属性上。在表单提交时,通过 inputRef.current.value 获取输入框的值,而非依赖于 React 的 state 来维护输入框的值。

2. 受控组件深入剖析

2.1 状态同步与更新机制

在受控组件中,状态同步是核心要点。以 <input> 元素为例,当用户在输入框中输入内容时,onChange 事件会被触发。在上述受控输入框的例子中,handleChange 函数接收 event 对象,通过 e.target.value 获取最新输入的值,并调用 setInputValue 更新 inputValue state。这一过程确保了输入框的显示值与 React 组件的 state 保持一致。

从 React 的更新机制角度来看,当 setInputValue 被调用时,React 会重新渲染组件。由于 input 元素的 value 属性依赖于 inputValue state,所以新的 state 值会反映在输入框上。这种双向的数据绑定机制使得开发人员可以精确地控制表单元素的状态,便于进行数据验证、实时反馈等操作。

2.2 数据验证与实时反馈

受控组件在数据验证和实时反馈方面具有显著优势。例如,我们可以在 handleChange 函数中添加数据验证逻辑:

import React, { useState } from 'react';

function ControlledInputWithValidation() {
  const [inputValue, setInputValue] = useState('');
  const [isValid, setIsValid] = useState(true);

  const handleChange = (e) => {
    const value = e.target.value;
    setInputValue(value);
    // 简单的验证逻辑,假设只允许数字输入
    if (/^\d+$/.test(value)) {
      setIsValid(true);
    } else {
      setIsValid(false);
    }
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={handleChange}
        style={{ border: isValid? '1px solid green' : '1px solid red' }}
      />
      {!isValid && <p style={{ color:'red' }}>请输入数字</p>}
    </div>
  );
}

export default ControlledInputWithValidation;

在上述代码中,当输入的值符合数字格式时,输入框边框显示为绿色,否则为红色,并提示用户输入数字。这种实时反馈机制得益于受控组件状态与输入值的紧密同步,开发人员可以方便地根据输入值的变化执行各种逻辑。

2.3 表单联动与复杂交互

受控组件在处理表单联动和复杂交互方面表现出色。例如,有一个包含多个输入框的表单,其中一个输入框的值会影响其他输入框的可用状态。

import React, { useState } from 'react';

function ComplexForm() {
  const [mainInputValue, setMainInputValue] = useState('');
  const [secondaryInputDisabled, setSecondaryInputDisabled] = useState(true);

  const handleMainInputChange = (e) => {
    const value = e.target.value;
    setMainInputValue(value);
    if (value.length > 0) {
      setSecondaryInputDisabled(false);
    } else {
      setSecondaryInputDisabled(true);
    }
  };

  return (
    <div>
      <input
        type="text"
        value={mainInputValue}
        onChange={handleMainInputChange}
        placeholder="主输入框"
      />
      <input
        type="text"
        disabled={secondaryInputDisabled}
        placeholder="依赖的输入框"
      />
    </div>
  );
}

export default ComplexForm;

在这个例子中,主输入框的值发生变化时,会根据其长度决定依赖输入框的禁用状态。这种表单联动效果通过受控组件的状态管理和更新机制得以轻松实现。

3. 非受控组件深入剖析

3.1 DOM 状态管理原理

非受控组件依赖 DOM 来管理自身状态。在 React 中使用 ref 来获取 DOM 元素的引用,进而访问和操作其值。当 ref 被挂载到表单元素上时,React 会在组件渲染完成后将 DOM 元素赋值给 ref.current。例如在前面的非受控输入框例子中,inputRef.current 就指向了 <input> 元素的 DOM 实例,通过它可以直接获取输入框的值。

从 DOM 角度看,非受控组件的行为与传统 HTML 表单元素类似。用户输入时,DOM 元素自身的状态发生变化,而 React 只是在需要时通过 ref 获取这个状态,并不主动维护它。这种方式在某些场景下可以简化代码逻辑,尤其是对于一些不需要实时响应输入变化的场景。

3.2 初始值设置与默认行为

非受控组件在设置初始值方面与受控组件有所不同。对于受控组件,我们通过设置 state 的初始值来决定表单元素的初始显示值。而对于非受控组件,我们通常使用 defaultValue(对于 <input><textarea>)或 defaultChecked(对于 <input type="checkbox"><input type="radio">)属性来设置初始值。

例如,一个带有初始值的非受控文本输入框:

import React, { useRef } from 'react';

function UncontrolledInputWithDefaultValue() {
  const inputRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('输入的值为: ', inputRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        ref={inputRef}
        defaultValue="初始值"
      />
      <button type="submit">提交</button>
    </form>
  );
}

export default UncontrolledInputWithDefaultValue;

这里 defaultValue 设置了输入框的初始显示文本。当表单提交时,无论用户是否修改了初始值,都可以通过 ref 获取当前输入框的值。

3.3 与第三方库的集成

在与第三方库集成时,非受控组件有时能提供更便利的方式。例如,某些富文本编辑器库可能要求直接操作 DOM 元素来实现特定功能。使用非受控组件结合 ref,可以方便地将这些库集成到 React 应用中。

假设我们要集成一个简单的第三方文本编辑器库 SimpleEditor

import React, { useRef, useEffect } from'react';
import SimpleEditor from 'third - party - simple - editor';

function EditorIntegration() {
  const editorRef = useRef();

  useEffect(() => {
    if (editorRef.current) {
      new SimpleEditor(editorRef.current);
    }
  }, []);

  return (
    <div ref={editorRef} />
  );
}

export default EditorIntegration;

在上述代码中,通过 ref 将 DOM 元素传递给第三方库 SimpleEditor 进行初始化。这种方式利用了非受控组件直接操作 DOM 的特性,避免了复杂的状态同步逻辑,使集成过程更加简洁。

4. 受控与非受控组件的选择策略

4.1 根据业务需求选择

如果业务需求对数据的实时验证、联动交互要求较高,例如注册表单需要实时检查用户名是否可用、密码强度是否符合要求等场景,受控组件是更好的选择。因为受控组件能够精确地控制和响应输入值的变化,方便实现复杂的业务逻辑。

相反,对于一些简单的表单提交场景,如只需要在用户提交表单时获取数据,不需要实时响应输入变化,非受控组件可以简化代码结构。例如,一个简单的反馈表单,用户填写完内容后点击提交按钮,此时非受控组件能够满足需求且代码更为简洁。

4.2 性能考量

在性能方面,受控组件由于每次状态更新都会触发组件重新渲染,对于大型表单或频繁更新的表单元素,可能会带来一定的性能开销。在这种情况下,如果可以避免不必要的重新渲染,非受控组件可能是更好的选择。

然而,React 的优化机制(如 shouldComponentUpdateReact.memo 等)可以在一定程度上缓解受控组件的性能问题。如果合理使用这些优化手段,受控组件在性能上也能表现良好。例如,对于一个包含多个输入框的表单,如果只有部分输入框的变化需要触发特定逻辑,可以通过 shouldComponentUpdate 来控制组件的重新渲染,只在必要时更新。

4.3 代码维护与可扩展性

从代码维护和可扩展性角度来看,受控组件的状态集中管理在 React 组件的 state 中,使得代码逻辑更加清晰,易于理解和维护。当需求发生变化,需要添加新的交互逻辑或数据验证规则时,在受控组件的基础上进行扩展相对容易。

非受控组件虽然在简单场景下代码简洁,但随着业务复杂度的增加,直接操作 DOM 可能会导致代码变得难以理解和维护。例如,如果需要在非受控组件的基础上添加实时验证逻辑,可能需要额外编写复杂的事件监听和状态管理代码,而这些在受控组件中可以通过简单的 state 更新实现。

5. 受控与非受控组件的混合使用

在实际项目中,并非所有场景都能简单地选择受控组件或非受控组件,有时需要将两者混合使用以达到最佳效果。

5.1 部分受控与部分非受控

例如,在一个复杂的表单中,某些输入框需要实时验证和联动,而另一些输入框只在提交时获取数据。我们可以将需要实时交互的输入框设置为受控组件,而将其他输入框设置为非受控组件。

import React, { useState, useRef } from'react';

function MixedForm() {
  const [name, setName] = useState('');
  const emailRef = useRef();

  const handleNameChange = (e) => {
    setName(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('姓名: ', name);
    console.log('邮箱: ', emailRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={handleNameChange}
        placeholder="姓名"
      />
      <input
        type="email"
        ref={emailRef}
        placeholder="邮箱"
      />
      <button type="submit">提交</button>
    </form>
  );
}

export default MixedForm;

在这个例子中,姓名输入框是受控组件,实时更新 state,而邮箱输入框是非受控组件,只在提交时获取值。

5.2 动态切换受控与非受控状态

在某些情况下,组件可能需要根据用户操作或业务逻辑动态切换受控与非受控状态。例如,一个文本输入框,在用户开始编辑时切换为受控状态以进行实时验证,在编辑结束后切换回非受控状态以减少不必要的渲染。

import React, { useState, useRef, useEffect } from'react';

function DynamicInput() {
  const [isControlled, setIsControlled] = useState(false);
  const [inputValue, setInputValue] = useState('');
  const inputRef = useRef();

  const handleEditStart = () => {
    setIsControlled(true);
    if (inputRef.current) {
      setInputValue(inputRef.current.value);
    }
  };

  const handleEditEnd = () => {
    setIsControlled(false);
  };

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

  useEffect(() => {
    if (!isControlled && inputRef.current) {
      inputRef.current.value = inputValue;
    }
  }, [isControlled, inputValue]);

  return (
    <div>
      {isControlled? (
        <input
          type="text"
          value={inputValue}
          onChange={handleChange}
        />
      ) : (
        <input
          type="text"
          ref={inputRef}
          defaultValue={inputValue}
          onFocus={handleEditStart}
          onBlur={handleEditEnd}
        />
      )}
    </div>
  );
}

export default DynamicInput;

在上述代码中,通过 isControlled 状态来控制输入框是受控还是非受控。当输入框获得焦点时,切换为受控状态并同步当前值;失去焦点时,切换回非受控状态并更新 DOM 元素的值。

6. 常见问题与解决方法

6.1 受控组件的性能问题

如前文所述,受控组件频繁的状态更新可能导致性能问题。解决方法之一是使用 shouldComponentUpdate 生命周期方法(对于类组件)或 React.memo(对于函数组件)来控制组件的重新渲染。例如,对于一个包含多个输入框的表单组件:

import React from'react';

class FormComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 假设只有 name 字段变化时才重新渲染
    return nextState.name!== this.state.name;
  }

  render() {
    return (
      <form>
        <input
          type="text"
          value={this.state.name}
          onChange={(e) => this.setState({ name: e.target.value })}
        />
        <input
          type="text"
          value={this.state.age}
          onChange={(e) => this.setState({ age: e.target.value })}
        />
      </form>
    );
  }
}

export default React.memo(FormComponent);

在上述代码中,通过 shouldComponentUpdate 方法只在 name 字段变化时重新渲染组件,减少了不必要的渲染开销。

6.2 非受控组件的值获取时机

在非受控组件中,获取值的时机可能会成为问题。例如,如果在组件挂载完成前就尝试通过 ref 获取值,可能会得到 null。解决方法是使用 useEffect 钩子(对于函数组件)或 componentDidMount 生命周期方法(对于类组件)来确保在组件挂载完成后再获取值。

import React, { useRef, useEffect } from'react';

function UncontrolledGetValue() {
  const inputRef = useRef();

  useEffect(() => {
    if (inputRef.current) {
      console.log('组件挂载后的值: ', inputRef.current.value);
    }
  }, []);

  return (
    <input
      type="text"
      ref={inputRef}
      defaultValue="初始值"
    />
  );
}

export default UncontrolledGetValue;

在这个例子中,useEffect 确保了在组件挂载完成后才获取输入框的值,避免了 ref.currentnull 的问题。

6.3 混合使用时的状态同步问题

在受控与非受控组件混合使用时,可能会出现状态同步问题。例如,在动态切换受控与非受控状态时,如果没有正确处理,可能会导致数据不一致。解决方法是仔细梳理状态变化逻辑,确保在切换状态时进行正确的数据同步。如前文动态切换受控与非受控状态的例子中,通过 useEffect 钩子在状态切换时更新 DOM 元素的值,保证了数据的一致性。

7. 受控与非受控组件在不同场景下的实践案例

7.1 电子商务中的商品搜索表单

在电子商务应用中,商品搜索表单通常需要实时显示搜索结果,并且可能包含多种筛选条件。此时,受控组件是理想的选择。

import React, { useState } from'react';

function ProductSearchForm() {
  const [searchTerm, setSearchTerm] = useState('');
  const [category, setCategory] = useState('all');

  const handleSearchTermChange = (e) => {
    setSearchTerm(e.target.value);
  };

  const handleCategoryChange = (e) => {
    setCategory(e.target.value);
  };

  // 模拟搜索逻辑
  const performSearch = () => {
    console.log('搜索词: ', searchTerm);
    console.log('类别: ', category);
  };

  return (
    <form>
      <input
        type="text"
        value={searchTerm}
        onChange={handleSearchTermChange}
        placeholder="搜索商品"
      />
      <select
        value={category}
        onChange={handleCategoryChange}
      >
        <option value="all">所有类别</option>
        <option value="electronics">电子产品</option>
        <option value="clothing">服装</option>
      </select>
      <button type="button" onClick={performSearch}>搜索</button>
    </form>
  );
}

export default ProductSearchForm;

在这个商品搜索表单中,搜索输入框和类别选择框都是受控组件。用户输入搜索词或选择类别时,组件的 state 实时更新,方便进行实时搜索和筛选逻辑的实现。

7.2 博客系统中的文章编辑器

在博客系统中,文章编辑器可能需要集成第三方富文本编辑库。此时,非受控组件结合 ref 可以方便地实现集成。

import React, { useRef, useEffect } from'react';
import ReactQuill from'react - quill';
import'react - quill/dist/quill.snow.css';

function ArticleEditor() {
  const editorRef = useRef();

  useEffect(() => {
    if (editorRef.current) {
      // 初始化富文本编辑器
      const quill = new ReactQuill(editorRef.current, {
        theme:'snow'
      });
      // 可以在这里添加自定义编辑器逻辑
    }
  }, []);

  return (
    <div ref={editorRef} />
  );
}

export default ArticleEditor;

通过非受控组件的方式,将 ref 传递给 ReactQuill 富文本编辑器库,实现了简单的集成。在这种场景下,不需要对编辑器的每个输入变化进行实时状态管理,非受控组件的方式更为合适。

7.3 在线考试系统中的答题表单

在线考试系统中的答题表单可能部分需要实时验证答案,部分只在提交时获取答案。这就需要混合使用受控与非受控组件。

import React, { useState, useRef } from'react';

function ExamForm() {
  const [question1Answer, setQuestion1Answer] = useState('');
  const question2Ref = useRef();

  const handleQuestion1Change = (e) => {
    setQuestion1Answer(e.target.value);
    // 实时验证逻辑
    if (question1Answer.length > 10) {
      console.log('问题 1 答案长度过长');
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('问题 1 答案: ', question1Answer);
    console.log('问题 2 答案: ', question2Ref.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={question1Answer}
        onChange={handleQuestion1Change}
        placeholder="问题 1: 请简要描述 React 受控组件的特点"
      />
      <textarea
        ref={question2Ref}
        placeholder="问题 2: 请阐述非受控组件的应用场景"
      />
      <button type="submit">提交</button>
    </form>
  );
}

export default ExamForm;

在这个例子中,问题 1 的答案输入框是受控组件,方便进行实时验证;问题 2 的答案输入框是非受控组件,只在提交时获取答案。通过混合使用,满足了不同的业务需求。