0

0

避免React UI不更新:正确处理状态不可变性的实践

霞舞

霞舞

发布时间:2025-11-23 18:13:01

|

907人浏览过

|

来源于php中文网

原创

避免react ui不更新:正确处理状态不可变性的实践

本文深入探讨React应用中UI不更新的常见原因,特别是由于直接修改(mutation)状态而非创建新状态引用导致的渲染问题。我们将通过一个实际的待办事项列表删除案例,详细解析`Array.prototype.splice()`等方法对状态的影响,并提供正确的不可变状态更新策略,确保组件能够按预期重新渲染,从而避免因状态引用未改变而引起的UI不同步问题。

问题复现:React UI未更新的陷阱

在React开发中,一个常见的困惑是:我们更新了组件的状态,但UI却没有立即反映这些变化。尤其是在处理列表数据的增删改操作时,这个问题尤为突出。例如,在一个待办事项列表中,用户删除一个任务后,列表项并没有消失;但当我们在其他输入框中进行操作时,列表却突然更新了。

让我们先来看一下原始代码中的问题所在。

原始 form.jsx 中的 addTask 函数:

// form.jsx
function TaskForm() {
  const initialList = [{task: 'Do something', done: false}];
  const [tasks, setTasks] = useState(initialList);
  const [input, setInput] = useState('');
  // ...

  const addTask = () => {
    if(input.length !== 0) {
      setValidTask('valid');
      setTasks(tasks.push({task: input, done: false})); // 问题所在:push方法会修改原数组并返回新长度
      setTasks(tasks); // 问题所在:将状态设置回被修改的原数组引用
      setInput('');
    }
    else {
      setValidTask('invalid');
    }
    console.log(tasks);
  }
  // ...
  return (
    <>
      {/* ... */}
      
    
  );
}

原始 tasks.jsx 中的 deleteTask 函数:

// tasks.jsx
function Tasks(props) {
  const [tasks, setTasks] = useState(props.tasks); // 问题所在:将props作为初始state,且未处理props更新
  const deleteTask = (index) => {
    tasks.splice(index, 1); // 问题所在:splice方法会修改原数组
    setTasks(tasks); // 问题所在:将状态设置回被修改的原数组引用
  };
  const taskList = props.tasks.map(task => ( // 注意这里渲染的是props.tasks
    
  • {task.task} deleteTask(task.id)} />
  • )); return
      {taskList}
    ; }

    上述代码中,addTask 和 deleteTask 都试图通过直接修改 tasks 数组(使用 push 或 splice)来更新状态。这是导致UI不更新的根本原因。

    根源分析:React状态的不可变性原则

    React通过比较组件的props和state是否发生变化来决定是否重新渲染组件。对于对象和数组这类引用类型数据,React的浅层比较机制只检查它们的引用地址是否改变。

    • Array.prototype.splice() 方法会原地修改(mutate in place)原数组的内容,并返回被删除的元素。
    • Array.prototype.push() 方法也会原地修改原数组,并返回新数组的长度。

    当你在 deleteTask 中执行 tasks.splice(index, 1) 后,tasks 变量仍然指向内存中的同一个数组对象,只是该对象的内容被改变了。随后调用 setTasks(tasks) 时,React会发现你传入的 tasks 引用与上一次的状态引用是相同的,因此它会认为状态没有“改变”,从而跳过重新渲染的步骤。这就是为什么UI不会立即更新的原因。

    解决方案一:最小化修复——创建新的状态引用

    要解决这个问题,核心原则是:永远不要直接修改React的状态对象或数组,而是创建它们的新副本,然后用新副本更新状态。 这样,React就能检测到状态引用的变化,并触发组件重新渲染。

    修复 form.jsx 中的 addTask 函数

    我们将使用ES6的展开运算符(spread syntax)来创建一个新的数组,包含所有旧任务和新添加的任务。

    修正后的 addTask 函数:

    a0.dev
    a0.dev

    专为移动端应用开发设计的AI编程平台

    下载
    // form.jsx (修正部分)
    import { useState } from 'react';
    // ...
    
    function TaskForm() {
      const initialList = [{id: 1, task: 'Do something', done: false}]; // 建议为任务添加唯一ID
      const [tasks, setTasks] = useState(initialList);
      const [input, setInput] = useState('');
      // ...
    
      const addTask = () => {
        if(input.length !== 0) {
          setValidTask('valid');
          // 创建新任务时赋予唯一ID
          const newId = tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 1;
          const newTask = {id: newId, task: input, done: false};
          // 使用展开运算符创建新数组,而不是修改原数组
          setTasks([...tasks, newTask]);
          setInput('');
        }
        else {
          setValidTask('invalid');
        }
        console.log(tasks);
      }
      // ...
    }

    通过 setTasks([...tasks, newTask]),我们创建了一个全新的数组,其引用与之前的 tasks 数组不同。React会检测到这个引用变化,并重新渲染 TaskForm 及其子组件 Tasks。

    修复 tasks.jsx 中的 deleteTask 函数

    对于删除操作,最简洁且符合不可变性原则的方法是使用 Array.prototype.filter()。filter 方法会返回一个新数组,其中包含通过指定测试的所有元素,而不会修改原数组。

    修正后的 deleteTask 函数(在 TaskForm 中实现):

    考虑到 tasks.jsx 存在将 props 作为 useState 初始值的潜在问题,以及更好的状态管理实践,我们应该将 deleteTask 的逻辑提升到父组件 TaskForm 中。

    // TaskForm.jsx (修正部分,添加 handleDeleteTask)
    // ...
    function TaskForm() {
      // ...
      const [tasks, setTasks] = useState(initialList);
      // ...
    
      const handleDeleteTask = (idToDelete) => {
        // 使用 filter 方法创建一个新数组,不包含要删除的任务
        setTasks(tasks.filter(task => task.id !== idToDelete));
      };
    
      return (
        <>
          {/* ... */}
          {/* 将 tasks 数组和 handleDeleteTask 回调函数传递给子组件 */}
          
        
      );
    }

    解决方案二:更优的组件设计——状态提升与纯函数组件

    原始的 tasks.jsx 组件将 props.tasks 作为其自身 useState 的初始值,并且尝试在其内部修改这个局部状态。这种模式被称为“props作为初始状态的陷阱”,因为它会导致父组件的 props.tasks 更新时,子组件的内部 tasks 状态不会自动同步。

    更推荐的做法是,让 Tasks 组件成为一个“纯展示组件”(presentational component),它只负责渲染接收到的 props,并将任何需要修改数据的操作通过回调函数传递回父组件。

    重构后的 tasks.jsx (纯函数组件):

    // tasks.jsx
    import React from 'react'; // 在React 17+中不再强制,但保持习惯
    // 接收 tasks 数组和 onDeleteTask 回调函数作为 props
    function Tasks({ tasks, onDeleteTask }) {
      const taskList = tasks.map(task => (
        
  • {/* 使用 task.id 作为 key,确保唯一性 */} {/* checkbox应为受控组件或只读 */} {task.task} onDeleteTask(task.id)} // 调用父组件传递的删除回调 />
  • )); return
      {taskList}
    ; } export default Tasks;

    这种设计模式使得数据流更加清晰:TaskForm 拥有并管理 tasks 状态,Tasks 组件只负责展示这些任务并通知父组件进行删除操作。

    不可变更新的通用模式

    理解不可变性原则后,我们可以总结出一些常见的不可变更新模式:

    数组操作

    • 添加元素:
      setArray([...oldArray, newItem]);
    • 删除元素:
      setArray(oldArray.filter(item => item.id !== idToDelete));
    • 更新元素:
      setArray(oldArray.map(item =>
        item.id === idToUpdate ? { ...item, keyToUpdate: newValue } : item
      ));
    • 连接/合并数组:
      setArray([...array1, ...array2]);

    对象操作

    • 更新顶级属性:
      setObject({ ...oldObject, keyToUpdate: newValue });
    • 更新嵌套属性:
      setObject({
        ...oldObject,
        nestedObject: {
          ...oldObject.nestedObject,
          nestedKey: newValue
        }
      });

    注意事项与最佳实践

    1. 始终坚持不可变性原则: 这是React状态管理的核心。任何时候修改状态,都应该返回一个新的引用。
    2. 理解JavaScript方法: 熟悉哪些JavaScript数组/对象方法会原地修改数据(如push, pop, splice, sort, reverse, fill, delete操作符),哪些会返回新数据(如map, filter, slice, concat, reduce, Object.assign, 展开运算符)。
    3. 善用ES6展开运算符: 它是创建数组和对象副本最简洁、最常用的方式。
    4. 为列表项提供稳定的 key 属性: 在渲染列表时,key 属性帮助React高效地识别哪些项已更改、添加或删除。使用数据中的唯一ID(如task.id)而不是数组索引作为key,可以避免在列表项顺序变化或删除时出现性能问题和不稳定的行为。
    5. 避免将 props 直接用作子组件的初始 state: 除非你明确知道你在做什么,并且不期望 props 改变时子组件状态也跟着变。通常,props 应该是数据的单一事实来源。如果子组件需要基于 props 派生内部状态,并允许该状态独立演变,可以考虑使用 useEffect 钩子来响应 props 的变化并更新内部状态,或者更好地,将状态提升到父组件。

    总结

    React中UI不更新的问题,往往源于对状态“不可变性”原则的忽视。当状态是引用类型(如数组或对象)时,直接修改其内容而没有改变其引用地址,会导致React无法检测到状态变化,从而跳过重新渲染。通过始终创建状态的新副本(利用展开运算符、filter、map等方法)来更新状态,我们能确保React正确识别状态变化并及时更新UI。同时,合理地进行状态提升和组件设计,能使应用的数据流更加清晰、可维护性更强。遵循这些原则,将有助于构建更稳定、可预测的React应用。

    相关专题

    更多
    js获取数组长度的方法
    js获取数组长度的方法

    在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

    554

    2023.06.20

    js刷新当前页面
    js刷新当前页面

    js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

    374

    2023.07.04

    js四舍五入
    js四舍五入

    js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

    731

    2023.07.04

    js删除节点的方法
    js删除节点的方法

    js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

    477

    2023.09.01

    JavaScript转义字符
    JavaScript转义字符

    JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

    394

    2023.09.04

    js生成随机数的方法
    js生成随机数的方法

    js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

    990

    2023.09.04

    如何启用JavaScript
    如何启用JavaScript

    JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

    656

    2023.09.12

    Js中Symbol类详解
    Js中Symbol类详解

    javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

    551

    2023.09.20

    Golang gRPC 服务开发与Protobuf实战
    Golang gRPC 服务开发与Protobuf实战

    本专题系统讲解 Golang 在 gRPC 服务开发中的完整实践,涵盖 Protobuf 定义与代码生成、gRPC 服务端与客户端实现、流式 RPC(Unary/Server/Client/Bidirectional)、错误处理、拦截器、中间件以及与 HTTP/REST 的对接方案。通过实际案例,帮助学习者掌握 使用 Go 构建高性能、强类型、可扩展的 RPC 服务体系,适用于微服务与内部系统通信场景。

    8

    2026.01.15

    热门下载

    更多
    网站特效
    /
    网站源码
    /
    网站素材
    /
    前端模板

    精品课程

    更多
    相关推荐
    /
    热门推荐
    /
    最新课程
    React 教程
    React 教程

    共58课时 | 3.6万人学习

    国外Web开发全栈课程全集
    国外Web开发全栈课程全集

    共12课时 | 1.0万人学习

    React核心原理新老生命周期精讲
    React核心原理新老生命周期精讲

    共12课时 | 1万人学习

    关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
    php中文网:公益在线php培训,帮助PHP学习者快速成长!
    关注服务号 技术交流群
    PHP中文网订阅号
    每天精选资源文章推送

    Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号