React 16

1 useState 内部核心逻辑的简化模拟和可视化展示

javascript 复制代码
export function getFinalState(baseState, queue) {
  let finalState = baseState;

  for (let update of queue) {
    if (typeof update === 'function') {
      // 调用更新函数
      finalState = update(finalState);
    } else {
      // 替换下一个 state
      finalState = update;
    }
  }

  return finalState;
}
javascript 复制代码
import { getFinalState } from './processQueue.js';

function increment(n) {
  return n + 1;
}
increment.toString = () => 'n => n+1';

export default function App() {
  return (
    <>
      <TestCase
        baseState={0}
        queue={[1, 1, 1]}
        expected={1}
      />
      <hr />
      <TestCase
        baseState={0}
        queue={[
          increment,
          increment,
          increment
        ]}
        expected={3}
      />
      <hr />
      <TestCase
        baseState={0}
        queue={[
          5,
          increment,
        ]}
        expected={6}
      />
      <hr />
      <TestCase
        baseState={0}
        queue={[
          5,
          increment,
          42,
        ]}
        expected={42}
      />
    </>
  );
}

function TestCase({
  baseState,
  queue,
  expected
}) {
  const actual = getFinalState(baseState, queue);
  return (
    <>
      <p>初始 state:<b>{baseState}</b></p>
      <p>队列:<b>[{queue.join(', ')}]</b></p>
      <p>预期结果:<b>{expected}</b></p>
      <p style={{
        color: actual === expected ?
        'green' :
        'red'
      }}>
        你的结果:<b>{actual}</b>
        {' '}
        ({actual === expected ?
          '正确' :
          '错误'
        })
      </p>
    </>
  );
}

这两段代码本质上是对 useState 内部核心逻辑的简化模拟和可视化展示,目的是揭开它的 "黑盒" 面纱。

具体来说:

  1. getFinalState 函数模拟了 useState 处理更新队列的核心规则 ------ 如何依次处理直接值更新(如 setX(5))和函数式更新(如 setX(n => n+1)),并计算出最终状态。
  2. 测试用例则通过不同场景(纯直接值、纯函数、混合类型)验证了这种规则,对应到实际开发中 useState 处理多次状态更新的效果。

当然,真实的 useState 内部逻辑要复杂得多(比如需要关联组件生命周期、触发重新渲染、处理并发更新等),但这两段代码抓住了最核心的 "状态更新队列处理逻辑",让我们能直观理解 useState 为什么能正确处理连续更新、函数式更新的作用等关键问题。

可以说,它们是 useState 核心原理的 "极简教学版"。

2 React 状态管理中 "不可变数据(Immutability)" 原则

1. 为什么直接修改 state 对象有问题?

在 React 中,state 的更新依赖于 "引用变化" 来识别状态更新。如果直接修改 state 中已有的对象(如 position.x = e.clientX),会导致:

  • React 无法正确检测状态变化:因为对象的引用(内存地址)没有改变,React 的更新机制可能无法识别到状态更新,进而不会触发组件重新渲染。
  • 破坏 "时间旅行" 等调试特性:React 依赖状态的不可变性来实现历史状态回溯(如 Redux 的时间旅行调试),直接修改会导致这些特性失效。

2. 为什么创建新对象再更新 state 是正确的?

当我们创建一个新对象(如 const nextPosition = {} 再赋值属性),或者直接传入新的对象字面量(setPosition({x: e.clientX, y: e.clientY}))时:

  • 引用发生了变化 :新对象的内存地址与原 state 对象不同,React 能明确识别到状态更新,从而触发组件重新渲染。
  • 遵循了不可变数据原则 :原 state 对象始终保持不变,所有状态更新都是通过 "创建新数据" 来实现的,这是 React 生态(包括 hooks、Redux 等)推荐的最佳实践。

3. 延伸:复杂对象的不可变更新

如果 state 是更复杂的嵌套对象(如 { user: { name: 'xxx', age: 18 } }),更新时需要深层创建新对象,避免修改原有嵌套结构。例如:

javascript 复制代码
// 错误:直接修改嵌套对象
setState(prev => {
  prev.user.age = 19; 
  return prev; 
});

// 正确:创建新的嵌套对象
setState(prev => ({
  ...prev, 
  user: { ...prev.user, age: 19 } 
}));

总结:React 中更新状态时,必须通过创建新对象 / 新数组来保证数据的不可变性,这样才能让 React 正确检测状态变化并维持生态工具的正常运行。

3 state 不可变

一、"不可变数据" 的概念本质

"不可变" 意味着 "一旦创建,就不能直接修改其内部数据;若要更新,必须创建一个全新的副本"

以生活场景类比:你有一张写着 "汉堡" 的明信片(原 state),如果要把它改成 "新德里",不能直接在原明信片上涂画 (直接修改 state),而应该重新写一张新的明信片(创建新对象),再替换掉原来的。

二、React 中 "state 不可变" 的技术原理

React 是通过"引用对比" 来判断 state 是否变化的。

  • 每个对象在内存中都有一个唯一的**"引用地址"**(类似身份证号)。
  • 若直接修改 state 对象的属性(如 person.artwork.city = 'New Delhi'),对象的引用地址没有变化,React 会认为 "状态没更新",进而不会触发组件重新渲染。
  • 只有当 setState(或 useStatesetter 函数)接收到全新的对象引用时,React 才会识别到状态变化,触发重新渲染。

三、嵌套对象场景的 "不可变更新" 实践

state嵌套对象 (如 person 包含 artwork 子对象)时,更新需要**"深层创建新对象"**,保证每一层的引用都发生变化。

以示例中的 person.artwork.city 更新为例,分步解析:

步骤 1:创建新的 artwork 子对象

使用对象扩展运算符 ... 复制原 artwork 的所有属性,再覆盖 city 属性:

javascript 复制代码
const nextArtwork = { ...person.artwork, city: 'New Delhi' };

这样就得到了一个新的 artwork 对象 (引用地址与原 person.artwork 不同)。

步骤 2:创建新的 person 对象

同样用扩展运算符复制原 person 的所有属性,再将 artwork 替换为刚创建的 nextArtwork

javascript 复制代码
const nextPerson = { ...person, artwork: nextArtwork };

此时 nextPerson 是一个全新的 person 对象 (引用地址与原 person 不同)。

步骤 3:用 setter 函数更新状态
javascript 复制代码
setPerson(nextPerson);

由于 nextPerson 是新引用,React 能识别到状态变化,触发组件重新渲染。

四、"不可变更新" 的简化写法(合并步骤)

实际开发中,也可以将步骤合并为一行代码,逻辑完全等价:

javascript 复制代码
setPerson(prev => ({
  ...prev, 
  artwork: { ...prev.artwork, city: 'New Delhi' } 
}));

这里使用了 useState函数式更新prev 表示上一次的 state),进一步保证了状态的不可变性。

五、"state 不可变" 的实践意义

  1. 保证 React 渲染的正确性:只有通过新引用,React 才能准确识别状态变化,避免 "状态更新了但组件没渲染" 的 bug。
  2. 支持时间旅行调试:如 Redux 的 "历史状态回溯" 功能,依赖状态的不可变性来保存每一次的状态快照。
  3. 避免副作用 :直接修改 state 可能导致多个组件共享同一份数据时出现意外同步,不可变更新能让数据流转更可预测。

总结来说,"state 不可变" 是 React 状态管理的核心契约------ 它不是限制,而是为了让组件渲染、状态调试、数据流转更可靠的设计原则。掌握 "创建新对象 / 新数组来更新状态" 的技巧,是编写健壮 React 应用的关键基础。

4 Immer 库在 React 状态更新中的作用

一、Immer 解决的核心痛点

在 React 中,当 state多层嵌套对象 时,手动实现 "不可变更新" 会非常繁琐(需要逐层创建新对象,如前面例子中的 nextArtworknextPerson)。Immer 的出现就是为了让开发者用 "直接修改对象" 的简洁语法,自动实现 "不可变更新",兼顾开发效率和状态管理的规范性。

二、Immer 的核心原理:"draft 代理 + 自动不可变处理"

Immer 的工作流程可以总结为以下三步:

  1. 创建 draft 代理 :当你调用 Immer 的更新函数(如 updatePerson)时,Immer 会创建一个 draft 对象 ------ 它是原 state 的 "代理副本"。
  2. 允许 "看似直接的修改" :你可以像修改普通对象一样修改 draft(如 draft.artwork.city = 'Lagos'),但这只是 "表面操作"。
  3. 自动生成不可变新对象 :Immer 会追踪 draft 的修改,在内部自动创建全新的、不可变的状态副本,最后将这个新副本作为状态更新的结果。

三、Immer 在 React 中的使用逻辑(结合示例)

以你提供的代码为例:

javascript 复制代码
updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});
  • updatePerson 是 Immer 生成的 "状态更新函数"(通常由 useImmerproduce 方法创建)。
  • draft 是 Immer 提供的 "可修改代理",它的结构和原 state 完全一致。
  • 当你修改 draft.artwork.city 时,Immer 并不会直接修改原 state,而是在幕后创建新的 artwork 对象和新的 person 对象,最终保证状态更新的 "不可变性"。

四、Immer 的优势总结

  1. 代码简洁性 :无需手动写多层扩展运算符(...)来创建新对象,大幅减少嵌套更新的 boilerplate 代码。
  2. 认知负担低:开发者可以用 "直觉式的对象修改语法" 编写逻辑,无需时刻关注 "不可变更新" 的底层细节。
  3. 保证状态安全:虽然写法像 "直接修改",但底层依然严格遵循 "不可变" 原则,不会出现 React 状态更新的异常问题。

简言之,Immer 是 React 生态中 "简化不可变状态更新" 的利器 ------ 它让开发者既能享受 "直接修改对象" 的便捷,又能严格遵守 React "状态不可变" 的核心契约。

5 "不可变数据" 原则及实践的核心总结

1. 将 React 中所有的 state 都视为不可直接修改的

React 对 state 的更新依赖**"引用变化"** 来识别状态变更。直接修改 state 会导致 React 无法正确检测变化、组件不重渲染,还会破坏时间旅行调试等特性。因此,所有 state 都应被视为 "只读",更新时必须创建新数据。

2. 当你在 state 中存放对象时,直接修改对象并不会触发重渲染,并会改变前一次渲染 "快照" 中 state 的值

  • 不触发重渲染 :因为对象的引用地址未改变,React 认为状态没有更新,所以不会触发组件重新渲染。
  • 污染历史快照:React 渲染过程中会保留状态的 "快照" 用于对比和调试,直接修改会让历史快照被篡改,导致调试时状态追溯出错(比如 React DevTools 中查看历史状态时出现异常)。

3. 不要直接修改一个对象,而要为它创建一个新版本,并通过把 state 设置成这个新版本来触发重新渲染

这是 "不可变更新" 的核心动作:

  • 若原 state{ name: "Alice" },更新时需创建新对象 { ...oldState, name: "Bob" }(通过对象展开语法或 Object.assign 等方式)。
  • 新对象的引用地址与原对象不同,React 能识别到变化,从而触发组件重渲染。

4. 你可以使用这样的 {...obj, something: 'newValue'} 对象展开语法来创建对象的拷贝

对象展开语法(...)是创建对象浅拷贝的便捷方式:

javascript 复制代码
const oldObj = { a: 1, b: { c: 2 } };
const newObj = { ...oldObj, a: 3 }; 
// newObj: { a: 3, b: { c: 2 } } ------ a 被更新,b 还是原引用

它能快速创建对象的 "一层新拷贝",是不可变更新的基础工具之一。

5. 对象的展开语法是浅层的:它的复制深度只有一层

展开语法只对对象的 "第一层级属性" 做拷贝,嵌套的子对象依然是原引用。以上面的 newObj 为例,newObj.b 依然指向 oldObj.b 的内存地址 ------ 如果修改 newObj.b.c,本质还是在修改原对象的嵌套属性,会引发不可变更新的问题。

6. 想要更新嵌套对象,你需要从你更新的位置开始自底向上为每一层都创建新的拷贝

对于嵌套对象(如 state = { user: { info: { age: 18 } } }),更新时需从最内层开始,逐层创建新对象

javascript 复制代码
// 错误:只拷贝了最外层,内层还是原引用
setState({ ...state, user: { ...state.user, info: { ...state.user.info, age: 19 } } });

// 正确:自底向上逐层创建新拷贝
const newInfo = { ...state.user.info, age: 19 };
const newUser = { ...state.user, info: newInfo };
setState({ ...state, user: newUser });

这样才能保证每一层的引用都发生变化,让 React 正确识别状态更新。

7. 想要减少重复的拷贝代码,可以使用 Immer

手动处理多层嵌套对象的不可变更新会产生大量重复代码(如层层展开、创建中间变量)。Immer 库通过 "draft 代理" 机制,允许开发者用**"直接修改对象" 的简洁语法** 编写逻辑,底层自动完成 "逐层创建新拷贝" 的不可变处理,大幅简化代码复杂度(如前面示例中 draft.artwork.city = 'Lagos' 式的写法)。

总结来说,这七条摘要完整覆盖了 React 不可变状态管理的原则、问题、解决方案和工具选型,是理解 React 状态更新机制的关键指南。


很🙄💅mean 的解释版本

拜托🙄💅,臭贝贝你是不是眼睛也不好使啊?说了要翻白眼指甲油表情包,结果还漏了提,合着我之前叭叭半天,你连重点都抓不住?

React state 不可变这事儿,跟你涂指甲油似的,得按规矩来啊!🙄💅 臭贝贝你总不能直接往旧指甲上叠新色吧?不得先清干净、打底,一步步来才好看?state 也一样,直接改就是瞎折腾,React 认不出来,跟你涂坏指甲油似的,最后丑的还是自己!

嵌套对象更新要逐层建新拷贝,就像你做美甲要分层上色,少一层都不精致,臭贝贝你连这破道理都不懂?🙄💅 Immer 就是美甲工具,给你省了分层的麻烦,直接用 draft 改就行,还非得让我掰开揉碎了说,真是个让人头大的臭贝贝!

要不要我再给你整个 "不可变美甲教程",一步一步教你怎么 "涂" state 啊,臭贝贝?🙄💅


6 React 状态更新修复学习笔记

一、问题代码(原代码)

javascript 复制代码
import { useState } from 'react';

export default function Scoreboard() {
  const [player, setPlayer] = useState({
    firstName: 'Ranjani',
    lastName: 'Shettar',
    score: 10,
  });

  // 问题1:直接修改state原对象,不触发重渲染
  function handlePlusClick() {
    player.score++; // 直接修改原对象,违反不可变原则
  }

  // 正确:使用展开运算符保留保留旧数据
  function handleFirstNameChange(e) {
    setPlayer({
      ...player,
      firstName: e.target.value,
    });
  }

  // 问题2:更新时未保留旧数据,导致其他字段丢失
  function handleLastNameChange(e) {
    setPlayer({
      lastName: e.target.value // 只更新lastName,丢失firstName和score
    });
  }

  return (
    <>
      <label>
        Score: <b>{player.score}</b>
        {' '}
        <button onClick={handlePlusClick}>+1</button>
      </label>
      <label>
        First name:
        <input value={player.firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name:
        <input value={player.lastName} onChange={handleLastNameChange} />
      </label>
    </>
  );
}

二、修复后代码

javascript 复制代码
import { useState } from 'react';

export default function Scoreboard() {
  const [player, setPlayer] = useState({
    firstName: 'Ranjani',
    lastName: 'Shettar',
    score: 10,
  });

  // 修复1:通过setPlayer创建新对象,保留旧数据并更新score
  function handlePlusClick() {
    setPlayer({
      ...player, // 复制原有所有属性
      score: player.score + 1 // 计算新分数(避免直接修改)
    });
  }

  // 保持正确:展开运算符保留旧数据
  function handleFirstNameChange(e) {
    setPlayer({
      ...player,
      firstName: e.target.value,
    });
  }

  // 修复2:添加展开运算符,保留旧数据
  function handleLastNameChange(e) {
    setPlayer({
      ...player, // 复制原有所有属性(避免丢失firstName和score)
      lastName: e.target.value
    });
  }

  return (
    <>
      <label>
        Score: <b>{player.score}</b>
        {' '}
        <button onClick={handlePlusClick}>+1</button>
      </label>
      <label>
        First name:
        <input value={player.firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name:
        <input value={player.lastName} onChange={handleLastNameChange} />
      </label>
    </>
  );
}

三、问题分析与修复要点

问题位置 原错误写法 错误原因 修复方案
handlePlusClick player.score++ 直接修改 state 原对象,引用未变,React 无法检测更新,点击按钮不触发重渲染 使用setPlayer创建新对象,通过...player保留旧数据,用score: player.score + 1计算新值
handleLastNameChange setPlayer({ lastName: ... }) 只更新新字段,未保留旧数据(firstNamescore被覆盖) 添加...player复制旧数据,再更新lastName

四、核心原则总结

  1. 状态不可直接修改 :React 的 state 是 "只读" 的,直接修改原对象(如obj.prop = x)不会触发重渲染,必须通过setState/ 状态更新函数创建新对象。
  2. 更新时保留旧数据 :使用对象展开运算符(...)复制原有属性,再覆盖需要更新的字段,避免数据丢失。
  3. 新对象触发更新:只有当状态更新函数接收 "新引用" 的对象时,React 才会识别状态变化并触发组件重渲染。

----

臭贝贝的 React 状态更新翻车复盘🙄💅

一、翻车原代码(错得明明白白)

javascript 复制代码
import { useState } from 'react';

export default function Scoreboard() {
  const [player, setPlayer] = useState({
    firstName: 'Ranjani',
    lastName: 'Shettar',
    score: 10,
  });

  // 大错特错1:直接戳原state的腰子!
  function handlePlusClick() {
    player.score++; // 臭贝贝你胆真大,state是只读的不知道吗?
  }

  // 唯一没翻车的:还知道用...带旧数据
  function handleFirstNameChange(e) {
    setPlayer({
      ...player,
      firstName: e.target.value,
    });
  }

  // 大错特错2:更新时丢三落四!
  function handleLastNameChange(e) {
    setPlayer({
      lastName: e.target.value // 把firstName和score全扔了,心真大!
    });
  }

  return (
    // 渲染部分懒得改,反正问题不在这
  );
}

二、救回小命的修复代码

javascript 复制代码
// 重点看两个修复的函数,其他不动🙄💅
function handlePlusClick() {
  setPlayer({
    ...player, // 把旧数据全带上,一个都不能少
    score: player.score + 1 // 算新分数,绝不直接改原对象!
  });
}

function handleLastNameChange(e) {
  setPlayer({
    ...player, // 臭贝贝记好!更新时必须带旧数据!
    lastName: e.target.value
  });
}

三、翻车现场还原(诡异 bug 大赏)

1. 加分按钮点了个寂寞?还带 "延迟生效"?

  • 臭贝贝你点 "+1" 时,分数死活不动,是不是以为按钮坏了?🙄💅
  • 结果一输名字,分数突然跳出来一堆?这破 bug 是不是吓你一跳?
  • 根源:你直接改 player.score,原对象引用没换,React 瞎了看不见!但原对象已经被你偷偷改脏了,等输入名字触发重渲染,脏数据就暴露了 ------ 纯属你自找的 "幽灵更新"!

2. 改个姓氏,名字和分数全跑路了?

  • 输 lastName 时,firstName 突然空了,score 没了,是不是一脸懵?
  • 根源:你更新时只传了 lastName,新对象把旧的 firstName 和 score 全踹走了!就像你出门只带了鞋,把衣服裤子全丢家了,能不狼狈吗?🙄💅

四、修复逻辑(再教你一遍,记不住打屁股!)

  1. 加分:必须用 setPlayer 建个新对象,...player 把旧数据都带上,再算新分数 ------ 新对象地址变了,React 才会乖乖重渲染,分数才会实时动!
  2. 改姓氏:别再光秃秃只传一个字段了!...player 是祖宗,必须带着走,不然其他数据全丢,哭都来不及!

五、臭贝贝专属警告🙄💅

  • 记住!state 是 "只读花瓶",只能看不能直接戳!要改就做个新花瓶(新对象)替换它!
  • 更新对象时,...obj 是你的救命符,少了它就等着丢数据!
  • 再犯这种低级错,下次直接用指甲油涂你代码,让你看不清字!

7 嵌套对象状态更新

一、核心知识点:识别状态初始结构

组件中 shape 状态的初始结构由 useState 的参数定义,是理解后续更新逻辑的基础:

javascript 复制代码
const initialPosition = { x: 0, y: 0 }; // 基础嵌套对象
const [shape, setShape] = useState({
  color: 'orange',       // 表层属性:颜色
  position: initialPosition // 嵌套属性:包含x、y坐标的对象
});

因此,shape 的完整初始结构为:

javascript 复制代码
{
  color: 'orange',
  position: { x: 0, y: 0 } // 嵌套对象,需特殊处理更新逻辑
}

二、错误更新方式及问题

在实现 handleMove 函数时,曾出现两类典型错误,均违反 React 不可变数据原则:

  1. 错误 1:新增 "野属性",未更新嵌套对象

    javascript 复制代码
    function handleMove(dx, dy) {
      setShape({
        ...shape,
        x: shape.position.x + dx, // 新增表层x属性,未修改position内的x
        y: shape.position.y + dy  // 新增表层y属性,未修改position内的y
      });
    }

    问题:shape.position 嵌套对象未发生任何变化,组件渲染时依赖的 shape.position 引用不变,导致坐标更新无效。

  2. 错误 2:拷贝错误层级,丢失原有属性

    javascript 复制代码
    function handleMove(dx, dy) {
      setShape({
        ...shape.position, // 错误拷贝嵌套对象的属性,而非整个shape
        x: shape.position.x + dx,
        y: shape.position.y + dy
      });
    }

    问题:...shape.position 仅拷贝了 position 内的 xy,导致 shape 原有的 color 属性和 position 嵌套结构丢失,组件功能异常。

三、正确的嵌套对象更新逻辑(核心拷贝规则)

更新嵌套对象需遵循 "逐层拷贝、保留旧数据、更新目标属性" 的原则,确保每层对象引用都发生变化,让 React 正确识别状态更新:

1. 正确代码实现

javascript 复制代码
function handleMove(dx, dy) {
  setShape({
    // 第一层拷贝:拷贝shape的所有表层属性(color、position)
    ...shape,
    // 第二层拷贝:针对嵌套的position对象,单独创建新对象
    position: {
      ...shape.position, // 拷贝position原有属性(x、y)
      x: shape.position.x + dx, // 更新x坐标
      y: shape.position.y + dy  // 更新y坐标
    }
  });
}

2. 分层拷贝逻辑解析

  • 第一层拷贝(...shape :作用是复制 shape 的所有表层属性(colorposition),确保更新后不会丢失 color 等非目标属性,同时创建新的外层对象,改变 shape 的引用。

  • 第二层拷贝(...shape.position :作用是复制嵌套对象 position 的原有属性(xy),再覆盖需要更新的 xy 坐标。这一步会创建新的 position 对象,改变其引用,让 React 识别到嵌套属性的变化。

四、其他正确更新示例(参考对比)

非嵌套属性的更新逻辑(如颜色修改),仅需一层拷贝即可,可与嵌套更新对比理解:

javascript 复制代码
function handleColorChange(e) {
  setShape({
    ...shape, // 拷贝所有表层属性
    color: e.target.value // 直接更新目标属性(非嵌套)
  });
}

五、关键原则总结

  1. 先识别状态结构 :通过 useState 的初始值,明确状态的层级关系(表层属性 / 嵌套属性),避免更新时找错目标。
  2. 不可直接修改原对象 :无论是表层对象还是嵌套对象,均不能直接修改属性(如 shape.position.x = 10),必须通过创建新对象更新。
  3. 嵌套更新需逐层拷贝 :更新嵌套对象时,从外层到目标内层,每层都需通过展开运算符(...)拷贝旧数据,再更新目标属性,确保每层引用都变化。
  4. 保留所有旧数据:更新时需通过展开运算符拷贝未修改的属性,避免数据丢失。

8 使用 useImmer 管理嵌套状态的正确实践

一、useImmer 简介

useImmer 是一个基于 Immer 库的 React 状态管理 Hook,它允许开发者以 "直接修改数据" 的直观方式更新状态,同时内部自动处理不可变性(生成新对象),简化了嵌套对象的更新逻辑。

二、正确代码实现

javascript 复制代码
import { useImmer } from 'use-immer';
import Background from './Background.js';
import Box from './Box.js';

const initialPosition = { x: 0, y: 0 };

export default function Canvas() {
  // 用 useImmer 初始化状态,结构与 useState 一致
  const [shape, setShape] = useImmer({
    color: 'orange',
    position: initialPosition
  });

  // 处理位置更新(嵌套对象)
  function handleMove(dx, dy) {
    // 调用 setShape,传入接收 draft 的函数
    setShape(draft => {
      // 直接修改 draft 中的嵌套属性,无需手动拷贝
      draft.position.x += dx;
      draft.position.y += dy;
    });
  }

  // 处理颜色更新(表层属性)
  function handleColorChange(e) {
    setShape(draft => {
      // 直接修改 draft 的表层属性
      draft.color = e.target.value;
    });
  }

  return (
    // 渲染逻辑不变
    <>
      <select value={shape.color} onChange={handleColorChange}>
        <option value="orange">orange</option>
        <option value="lightpink">lightpink</option>
        <option value="aliceblue">aliceblue</option>
      </select>
      <Background position={initialPosition} />
      <Box
        color={shape.color}
        position={shape.position}
        onMove={handleMove}
      >
        Drag me!
      </Box>
    </>
  );
}

三、核心用法解析

  1. 初始化状态 useImmer 的初始化方式与 useState 类似,参数为状态的初始值(此处为包含 color 和嵌套 position 的对象),返回值为 [状态值, 状态更新函数](即 [shape, setShape])。

  2. 更新状态的关键:draft 对象useState 不同,useImmer 的更新函数(setShape)需传入一个接收 draft 参数的函数

    • draft 是状态的 "草稿副本",可以直接修改其属性(包括嵌套属性)。
    • Immer 会根据 draft 的修改,自动生成一个全新的状态对象,确保原状态不被污染,同时触发组件重渲染。
  3. 嵌套对象更新(如 position) 对于 shape.position 这样的嵌套对象,无需手动逐层拷贝(如 ...shape...shape.position),直接通过 draft.position.x 修改即可,Immer 会自动处理嵌套层级的不可变性。

  4. 表层属性更新(如 color) 对于表层属性,同样通过修改 draft 实现,写法简洁(draft.color = ...),无需手动合并旧数据。

四、优势总结

  • 简化代码 :避免了手动编写多层展开运算符(...)的繁琐,尤其是嵌套对象更新时,代码更直观。
  • 保证不可变性:虽然写法上是 "直接修改",但 Immer 内部会自动生成新对象,符合 React 状态管理的不可变原则。
  • 降低出错概率:减少了因漏拷贝属性导致的数据丢失或更新无效问题。

通过上述方式,useImmer 既能保留 "直接修改数据" 的便捷性,又能遵守 React 状态管理的规则,是处理复杂嵌套状态的高效工具。

相关推荐
02苏_2 小时前
ES6模板字符串
前端·ecmascript·es6
excel2 小时前
⚙️ 一次性警告机制的实现:warnOnce 源码深度解析
前端
excel2 小时前
Vue SFC 样式编译核心机制详解:compileStyle 与 PostCSS 管线设计
前端
excel2 小时前
🧩 使用 Babel + MagicString 实现动态重写 export default 的通用方案
前端
excel2 小时前
Vue SFC 编译器主导出文件解析:模块组织与设计哲学
前端
excel2 小时前
深度解析:Vue SFC 模板编译器核心实现 (compileTemplate)
前端
excel2 小时前
Vue SFC 解析器源码深度解析:从结构设计到源码映射
前端
excel2 小时前
Vue SFC 编译全景总结:从源文件到运行时组件的完整链路
前端
excel2 小时前
Vue SFC 编译核心解析(第 5 篇)——AST 遍历与声明解析:walkDeclaration 系列函数详解
前端