普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月24日首页

别再用错了!一分钟让你区分 useRef 和 useState

2025年7月23日 22:11

useRefuseState 都是 React 的 Hook,用于在函数组件中保存数据,但二者的用途和行为有明显区别:

  • useState

    • 用于管理需要触发重新渲染的状态数据
    • 状态变化会导致组件重新渲染,视图会更新。
    • 适合保存会影响界面显示的变量(如输入框内容、组件显隐状态等)。
    • 状态更新是异步的,在同一次 render 中无法立即获取最新值。
  • useRef

    • 用于保存不会引起视图重新渲染的可变数据
    • 适合存储 DOM 引用、定时器 ID、前一状态、临时变量等。
    • 数据更新不会触发组件的 rerender,更像是在整个组件生命周期内都可变的“容器” (ref 对象,current 属性持久有效)。
    • 状态更新是同步的,可在当前 render 流程内立即获取新值。
区别 useState useRef
渲染影响 数据变化会引起组件重新渲染 数据变化不会引起组件重新渲染
数据存储方式 返回 [状态, 设置函数] 返回 ref 对象(通过 .current 获得和设置值)
用途 存放会影响界面显示的状态数据 存放 DOM、定时器、可变临时数据,不影响界面
更新方式 setState 异步更新,无法立即获得最新值 ref.current 同步更新,可立即获得
常用场景 交互状态、表单输入、显隐控制等 获取 DOM、缓存旧值、标记、临时存储中间量等

选择建议

  • 如果数据变化影响 UI,需用 useState
  • 如果只需在渲染间持久保存某个数据且不会影响 UI,用 useRef

注意事项

  • ref.current 的改变不建议作为 useEffectuseMemo 等 hook 的依赖项;
  • useRef 不会参与 React 的响应式更新机制。

国外疯传的 React UI 动效组件库!Vue3 版正式发布!

2025年7月23日 21:22

“这可能是 2025 年最值得收藏的 UI 动效果组件神器。”

React 版在 Twitter 上被疯狂安利,现在 Vue 开发者终于等到了!”

7.gif

🔥 1 周 3 万星,它到底火在哪?

过去一个月,如果你刷过 Twitter / X,大概率见过 React Bits

1.gif

  • 一条「Splash Cursor」10Demo 视频,播放量破 200 万
  • 开发者 @syskey_dmg 直呼**「最艺术的 UI 库」**;
  • @gregberge_ 把它列入**「年度 10 大 React 工具」**。

核心卖点一句话:90+ 个复制即用的动画组件,全部 MIT 免费,支持 CSS / Tailwind 一键切换

✅ Vue 3 官方移植版来了!名字就叫 Vue Bits

作者亲自下场,用 一周时间 完成了 Vue 3 的 1:1 移植。

2.gif

  • 60+ 组件先行上线,每周同步新增
  • 技术栈:Vue 3 + TypeScript + Tailwind
  • 使用方式与 React 版保持一致,零学习成本。

🚀 3 步把动画搬进你的 Nuxt / Vue 项目

  • 安装 CLI(一次即可)
npm i -g jsrepo
  • 拉取任意组件
npx jsrepo add vue-bits/components/GradientButton
  • 直接 <template> 使用
<GradientButton colorFrom="#ff0080" colorTo="#7928ca">
   Get Started
</GradientButton>

不需要额外依赖,不需要复杂配置,复制即用。

先睹为快:5 个必玩特效

  • Splash Cursor
    鼠标所到之处拖出彩色墨渍,像水波一样扩散,瞬间提升页面质感。

3.gif

  • Text Scramble
    字符像黑客终端般高速随机重组,最后定格成你想要的文案,吸睛指数爆表。

4.gif

  • Profile Card
    3D 卡片沿椭圆轨道自动旋转,滑入滑出带惯性,科技感满满。

5.gif

  • Hyperspeed Background
    赛博隧道式穿梭背景,滚动页面时产生无限透视拉伸,沉浸感极强。

6.gif

  • Infinite Menu
    圆形按钮点击后弹性分裂成多个子项,黏连变形再聚合,萌且丝滑。

8.gif

📦 与 React 版差异速查

维度 React Bits Vue Bits
核心框架 React 18 Vue 3
组件数量 90+ 60+(持续同步)
安装方式 jsrepo add reactbits/... jsrepo add vue-bits/...
主题切换 CSS ↔ Tailwind 一键 完全一致

📣 社区声音

“从 React 切到 Nuxt,最舍不得的就是 React Bits。现在 Vue Bits 来了,完美衔接!”
—— @littlesticks(Nuxt 核心团队成员)

“做 SaaS 落地页,UI 动效 1 小时搞定。感谢开源!”
—— @makwanadeepam

🏁 立即体验

把酷炫动画带进你的下一个项目,只需要一次复制粘贴。

Star 收藏,永远不迷路!

封装个组件怎么连ref都拿不到?React你礼貌吗?

作者 G等你下课
2025年7月23日 18:56

React.forwardRef

在 React 的组件化开发中,forward是一个非常重要且实用的 API,它主要用于解决高阶组件(HOC)或其他组件包装方式中,原始组件的属性和引用传递问题。

一、React.forwardRef的基本概念

在正常情况下,ref属性不能像普通属性一样通过组件传递,因为ref并不是组件 props 的一部分。而React.forwardRef则打破了这一限制,让组件可以将ref转发给内部的某个子元素。React.forwardRef是 React 提供的一个函数,它允许组件接收并向子组件传递ref。

其基本语法如下:


const ForwardedComponent = React.forwardRef((props, ref) => {
  return <div ref={ref} {...props} />;
});

const App(){
    const myRef = useRef(null)
    return (
        <ForwardedComponent ref={myRef}/>
    )
}

这里,使用forwardRef接收一个渲染函数,该函数接收props和ref两个参数,并返回一个 React 元素。通过这种方式,当外部组件使用ForwardedComponent并传递ref时,这个ref会被传递到内部的div元素上。

二、React.forwardRef的实现原理

从本质上来说,React.forwardRef创建了一个特殊的组件类型。当 React 渲染这种组件时,会将ref作为第二个参数传递给渲染函数,而不是像普通组件那样忽略ref。

在 React 内部,组件会被分为不同的类型,forwardRef创建的组件会被标记为一种特殊的类型。当处理这种类型的组件时,React 会知道要将ref传递给渲染函数,而不是将其作为普通的ref附加到组件实例上(对于类组件)或忽略(对于函数组件)。

三、React.forwardRef的使用场景

  1. 高阶组件(HOC)中转发 ref

    高阶组件是 React 中复用组件逻辑的一种方式,但它可能会导致ref传递出现问题。例如,当我们创建一个 HOC 来包装组件时,外部传递给 HOC 的ref会指向 HOC 本身而不是被包装的原始组件。这时,使用 React.forwardRef 可以将 ref 转发到原始组件上。

    function withLogging(MyComponent) {
      const ComponentWithLogging = React.forwardRef((props, ref) => {
        console.log('Component rendered');
        return <MyComponent ref={ref} {...props} />;
      });
      return ComponentWithLogging;
    }
    
    const MyComponent = (props) => <div {...props} />;
    // 使用withLogging 包裹 MyComponent 形成 MyComponentWithLogging
    const MyComponentWithLogging = withLogging(MyComponent);
    
    // 使用时,ref会指向MyComponent内部的元素
    const ref = React.createRef();
    <MyComponentWithLogging ref={ref} />;
    
  2. 在 UI 组件库中使用

    在开发 UI 组件库时,很多组件是由多个基础元素组合而成的。为了让使用者能够方便地获取到组件内部的某个关键元素(如输入框、按钮等),组件库的开发者可以使用 React.forwardRef 将 ref 转发到对应的内部元素上。

    例如,一个自定义的输入框组件:

    const CustomInput = React.forwardRef((props, ref) => {
      return (
        <div className="custom-input">
          <input ref={ref} {...props} />
        </div>
      );
    });
    

    使用者在使用CustomInput时,可以像使用普通input一样获取ref,从而操作输入框元素。

  3. 与其他 React 特性结合使用

    React.forwardRef 还可以与其他 React 特性(如useImperativeHandle)结合使用,自定义暴露给父组件的实例值。useImperativeHandle可以让组件指定通过ref 暴露给父组件的方法和属性,而React.forwardRef则负责将ref传递进来。

    const CustomComponent = React.forwardRef((props, ref) => {
      const [value, setValue] = React.useState('');
    
      React.useImperativeHandle(ref, () => ({
        focus: () => {
          // 执行聚焦操作
        },
        getValue: () => value
      }));
    
      return <input value={value} onChange={(e) => setValue(e.target.value)} />;
    });
    

    这样,父组件通过ref可以调用focus方法和getValue方法,而不是直接获取input元素。

四、使用React.forwardRef的注意事项

  1. 只在函数组件中使用

    React.forwardRef的渲染函数是一个函数组件,它不能是类组件。如果需要在类组件中使用类似的功能,可以通过其他方式(如将ref作为普通 props 传递,命名为innerRef等)来实现。

  2. 避免过度使用

    虽然React.forwardRef很有用,但也不要过度使用。在大多数情况下,通过 props 传递数据和回调函数已经足够满足需求。只有当确实需要获取组件内部元素的引用时,才考虑使用React.forwardRef。

  3. 类型定义(TypeScript)

    在使用 TypeScript 开发时,需要为forwardRef创建的组件正确定义类型,以确保类型安全。可以使用ForwardedRef和ComponentPropsWithoutRef等类型工具来辅助定义。

    import React, { forwardRef, ForwardedRef } from 'react';
    
    interface CustomInputProps {
      label: string;
    }
    
    const CustomInput = forwardRef((props: CustomInputProps, ref: ForwardedRef<HTMLInputElement>) => {
      return (
        <div>
          <label>{props.label}</label>
          <input ref={ref} />
        </div>
      );
    });
    

五、总结

React.forwardRef是 React 中一个非常实用的 API,它解决了ref在组件层级中传递的问题,使得高阶组件、UI 组件库等场景下的开发更加便捷。通过理解其基本概念、使用场景和工作原理,并注意使用过程中的一些细节,可以更好地发挥它的作用,提高 React 应用的开发效率和代码质量。

昨天 — 2025年7月23日首页

React中的forwardRef:打破父子组件间的"隔墙"

作者 FogLetter
2025年7月22日 18:15

大家好,我是你们的老朋友FogLetter,今天我们来聊聊React中一个非常实用但容易被忽视的API——forwardRef。这个API就像是在父子组件之间架起了一座桥梁,让我们能够"穿透"组件边界直接访问子组件中的DOM元素或组件实例。

为什么需要forwardRef?

在React的世界里,props是父子组件通信的主要方式,但有时候我们需要更"直接"的访问。想象一下这个场景:

function Parent() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current.focus(); // 想在组件挂载后自动聚焦输入框
  }, []);
  
  return <Child ref={inputRef} />;
}

function Child() {
  return <input type="text" />;
}

这段代码看起来合理,但实际上会报错!为什么呢?因为ref并不是一个真正的prop,它不会被自动传递给子组件。这就是React设计中的一个特殊之处——ref默认情况下是不会向下传递的。

forwardRef的基本用法

这时候,forwardRef就派上用场了。它就像是给组件装了一个"透明窗口",让ref可以穿透组件直接到达内部的DOM节点或组件。

让我们改造上面的例子:

const Child = forwardRef(function Child(props, ref) {
  return <input type="text" ref={ref} />;
});

function Parent() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return <Child ref={inputRef} />;
}

现在,代码可以正常工作啦!forwardRef接收一个渲染函数,这个函数会接收props和ref两个参数,我们需要手动将这个ref传递给内部的DOM元素。

从生活场景理解forwardRef

想象你是一个餐厅经理(父组件),你的服务员(子组件)负责接待顾客。通常,你会通过服务员与顾客交流(props传递)。但有时候,你需要直接与某位VIP顾客(DOM节点)对话。forwardRef就像是你给服务员的一个特殊对讲机,让你可以直接与VIP顾客沟通,而不需要经过服务员转达。

forwardRef的进阶用法

1. 与高阶组件结合

forwardRef在高阶组件(HOC)中特别有用。假设我们有一个withLogging的高阶组件:

function withLogging(WrappedComponent) {
  return forwardRef(function WithLogging(props, ref) {
    useEffect(() => {
      console.log('Component mounted');
      return () => console.log('Component unmounted');
    }, []);
    
    return <WrappedComponent {...props} ref={ref} />;
  });
}

const LoggedInput = withLogging(Input);

这样,即使经过高阶组件包装,ref也能正确传递到底层组件。

2. 转发多个ref

有时候我们需要转发多个ref,可以通过将ref作为prop传递:

function FancyInput(props) {
  const inputRef = useRef();
  const divRef = useRef();
  
  // 将refs暴露给父组件
  useImperativeHandle(props.forwardedRef, () => ({
    input: inputRef.current,
    div: divRef.current
  }));
  
  return (
    <div ref={divRef}>
      <input ref={inputRef} />
    </div>
  );
}

const ForwardedFancyInput = forwardRef((props, ref) => (
  <FancyInput {...props} forwardedRef={ref} />
));

这种方式既保持了封装性,又提供了必要的控制能力。

实际应用场景

1. 表单自动聚焦

const AutoFocusInput = forwardRef(function AutoFocusInput(props, ref) {
  const inputRef = useRef();
  
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus()
  }));
  
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return <input {...props} ref={inputRef} />;
});

function LoginForm() {
  const usernameRef = useRef();
  const passwordRef = useRef();
  
  useEffect(() => {
    usernameRef.current.focus();
  }, []);
  
  return (
    <form>
      <AutoFocusInput ref={usernameRef} placeholder="用户名" />
      <AutoFocusInput ref={passwordRef} placeholder="密码" />
    </form>
  );
}

2. 第三方组件集成

当你需要集成第三方组件库,但又需要访问其内部DOM元素时:

const FancyThirdPartyInput = forwardRef(function(props, ref) {
  return <ThirdPartyInput {...props} innerRef={ref} />;
});

3. 动画控制

const AnimatedBox = forwardRef(function(props, ref) {
  const boxRef = useRef();
  
  useImperativeHandle(ref, () => ({
    animate: () => {
      boxRef.current.animate(...);
    }
  }));
  
  return <div ref={boxRef} className="box" />;
});

function App() {
  const boxRef = useRef();
  
  return (
    <div>
      <AnimatedBox ref={boxRef} />
      <button onClick={() => boxRef.current.animate()}>开始动画</button>
    </div>
  );
}

注意事项

  1. 不要滥用forwardRef:大多数情况下,props已经足够满足组件通信需求。只有在确实需要直接访问DOM节点或组件实例时才使用forwardRef。

  2. 性能考虑:forwardRef创建的组件会有一个额外的渲染层,虽然影响很小,但在性能敏感的场景需要考虑。

  3. 测试影响:使用forwardRef后,测试策略可能需要调整,因为你现在可以直接访问子组件的内部实现。

与Context结合使用

forwardRef也可以与Context API结合使用,实现更灵活的组件设计:

const ThemeContext = createContext('light');

const ThemedButton = forwardRef(function(props, ref) {
  const theme = useContext(ThemeContext);
  
  return (
    <button
      ref={ref}
      style={{ background: theme === 'dark' ? '#333' : '#eee' }}
      {...props}
    />
  );
});

function App() {
  const buttonRef = useRef();
  
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton ref={buttonRef}>Click me</ThemedButton>
    </ThemeContext.Provider>
  );
}

总结

forwardRef是React中一个强大的工具,它打破了组件之间的"隔墙",让我们能够在需要时直接访问子组件的DOM节点或实例。但正如蜘蛛侠的叔叔所说:"能力越大,责任越大",我们应该谨慎使用这个功能,避免破坏组件的封装性。

记住以下几个要点:

  1. 默认情况下,ref不会自动传递
  2. forwardRef可以让你显式地将ref传递给子组件
  3. 结合useImperativeHandle可以控制暴露的内容
  4. 在第三方组件集成、表单控制、动画管理等场景特别有用

希望这篇文章能帮助你更好地理解和使用forwardRef。如果你觉得有用,别忘了点赞收藏,我们下期再见!

useMemo & useCallback :React 函数组件中的性能优化利器

作者 轻语呢喃
2025年7月22日 17:50

在日常开发中,我们经常会遇到组件频繁刷新或计算逻辑重复执行的问题,特别是在处理复杂应用时,这种现象会显著拖慢页面性能。

于是,React 提供了 useMemouseCallback 两个 Hook,它们通过巧妙的缓存机制,帮助我们减少不必要的重复工作。

本文我将从这两个工具的原理、使用场景到具体实践,带你一步步理解它们。


一、useMemo 的定义与核心作用

1.1 定义

useMemo 是 React 提供的一个 Hook,它的核心功能是缓存复杂计算的结果

比如,当我们需要执行一个耗时的计算任务时,如果每次组件刷新都重新执行一遍,不仅浪费资源,还可能导致用户体验变差,useMemo 就像一个“记忆本”,它会记住上一次的计算结果,只有当依赖项发生变化时,才会重新计算,从而避免重复劳动。

1.2 核心特性

  • 惰性求值:这个特性意味着我们不需要每次都立即执行计算,而是等到真正需要结果时才触发,比如,你只需要在依赖项变化时才重新生成数据,其他时候则不修改。

  • 缓存机制useMemo 会把上一次的计算结果保存下来,下次遇到相同的依赖项时,直接返回缓存值,而不是重新执行函数。这种机制类似于浏览器的缓存策略,能显著减少 CPU 占用。

  • 适用于纯函数:为了确保缓存结果的可靠性,useMemo 的计算函数必须是纯函数(即输入相同,输出一定相同)。如果函数内部依赖了外部变量或副作用,缓存可能会失效。

1.3 使用场景

  • 高开销的计算:比如数据过滤、格式化、数学运算等,这些操作如果每次渲染都执行,会明显拖慢性能。

  • 避免重复渲染中的重复计算:当某个值仅依赖于特定状态时,缓存可以避免每次刷新都重新计算。

  • 优化子组件 props:如果计算结果作为 props 传递给子组件,缓存可以防止父组件频繁更新导致的子组件重新渲染。

下面,我将会通过一些典型的案例来对useMemo进行讲解。


二、useMemo 详解

代码案例:
import { useState, useMemo } from 'react';

function App() {
  const [n, setN] = useState(10);

  // 使用 useMemo 缓存斐波那契数列计算结果
  const fibSequence = useMemo(() => {
    console.log('Computing Fibonacci...');
    const result = [];
    for (let i = 0; i < n; i++) {
      if (i <= 1) result.push(i);
      else result.push(result[i - 1] + result[i - 2]);
    }
    return result;
  }, [n]);

  return (
    <div>
      <p>斐波那契数列前{n}项:</p>
      <ul>{fibSequence.map((num, index) => <li key={index}>{num}</li>)}</ul>
      <button onClick={() => setN(n + 1)}>增加项数</button>
    </div>
  );
}

上面的代码,我们通过 useMemo 实现了斐波那契数列的动态计算与渲染,同时避免了不必要的重复计算。

具体来说,当用户修改 n 时,只有当前 n 值对应的数列会被重新计算,而其他状态更新时(例如组件首次加载或按钮点击但 n 未变化),数列计算逻辑会被跳过。

详细分析:
  1. useState 定义状态 n
    用户输入的 n 控制斐波那契数列的项数。这里用 useState(10) 初始化为默认值 10。

  2. useMemo 缓存计算逻辑

    • 函数式参数useMemo 接收一个函数作为第一个参数,这个函数负责执行复杂的计算逻辑。在本例中,函数通过循环生成斐波那契数列。

    • 依赖数组 [n]:第二个参数是依赖数组,只有当 n 发生变化时,useMemo 才会重新执行函数并更新缓存结果。如果 n 未变化,useMemo 直接返回上一次的缓存值。

    • console.log 验证效果:通过打印日志,我们可以观察到 Computing Fibonacci... 只在 n 变化时触发,其他时候被跳过。

  3. 渲染结果

    • 使用 map 方法将缓存后的数列渲染为列表项。由于 useMemo 确保了 fibSequence 的稳定性,即使父组件频繁刷新,只要 n 不变,列表内容也不会重复计算。

    • 其他逻辑:按钮点击事件直接更新 n,而不会干扰数列的缓存逻辑,体现了 useMemo 对依赖项的精准控制。

提示:
  • useMemo 的计算函数必须是纯函数(即输入相同,输出一致),如果函数内部依赖了外部变量或副作用,缓存结果可能不准确。

  • 如果计算逻辑本身非常简单(例如 n * 2),直接执行可能比引入 useMemo 更高效。

useMemo使用前后对比
场景 未使用 useMemo 使用 useMemo
计算触发频率 每次渲染都执行计算 仅在 n 变化时重新计算
性能影响 高频计算导致 CPU 开销大 通过缓存减少冗余计算
子组件更新控制 无法控制子组件更新 React.memo 配合使用可优化子组件更新

三、useCallback 的定义与核心作用

3.1 定义

useCallback 是 React 提供的另一个 Hook,它的核心目标是缓存函数引用

在开发中,我们常常会为组件定义回调函数,但如果每次渲染都生成新的函数实例,即使逻辑没变,子组件也可能因此重新渲染,useCallback 能帮我们“记住”函数的引用地址,只有在依赖项变化时才重新生成函数。

3.2 核心特性

  • 稳定引用:通过缓存函数的引用地址,确保在依赖项不变时,函数引用保持不变。这就像给函数贴上了“标签”,只要依赖项不变,标签就不会更换。

  • 惰性生成:只有当依赖项变化时,才会重新生成函数。这种机制避免了无意义的重复创建。

  • 适用于回调函数:特别适合传递给子组件的事件处理函数或逻辑函数,避免因引用变化导致的子组件重新渲染。

3.3 使用场景

  • 子组件依赖回调函数:如果父组件频繁更新,子组件可能因回调函数引用变化而重新渲染。

  • 优化 useEffectuseMemo 的依赖项:如果依赖项是函数,频繁的函数重新生成可能导致副作用或计算逻辑失效。


四、useCallback 详解

代码案例:
import { useState, useCallback, memo } from 'react';

// 子组件 Button,通过 React.memo 防止不必要的重新渲染
const Button = memo(({ onClick, label }) => {
  console.log('Button rendered');
  return <button onClick={onClick}>{label}</button>;
});

function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  // 使用 useCallback 缓存回调函数
  const handleClick = useCallback(() => {
    console.log('handleClick');
    setNum(num + 1);
  }, [num]);

  return (
    <div>
      <div>Count: {count}</div>
      <div>Num: {num}</div>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
      <Button onClick={handleClick} label="Increase Num" />
    </div>
  );
}

这段代码,我们通过 useCallbackReact.memo 技术实现了父子组件间回调函数的稳定传递,同时避免了子组件因父组件无关状态更新而重复渲染。

count 状态变化时,子组件 Button 不会重新渲染,而只有当 num 状态变化时,回调函数 handleClick 才会重新生成并触发子组件更新。

详细分析:
  1. memo 包裹子组件

    • 这里通过使用 React.memo 包裹 Button 组件,确保其只有在 onClicklabel 的引用地址变化时才重新渲染。这也是优化的核心前提——子组件本身具备“记忆能力”。
  2. useCallback 缓存回调函数

    • 函数式参数useCallback 接收一个函数作为第一个参数,这里定义的是点击事件的逻辑 setNum(num + 1)

    • 依赖数组 [num]:第二个参数是依赖数组,只有当 num 发生变化时,useCallback 才会重新生成新的函数实例,如果 num 未变化,返回的是缓存的函数引用。

    • console.log 验证效果:通过打印日志,可以观察到 handleClick 只有在 num 变化时才会执行,而 count 变化时不会影响函数引用。

  3. 父组件状态更新

    • count 状态变化:点击“Increase Count”按钮时,count 更新,但 handleClick 的引用地址未变化,因此 Button 组件不会重新渲染。

    • num 状态变化:点击“Increase Num”按钮时,num 更新,handleClick 会被重新生成,导致 Button 组件重新渲染。

提示:
  • useCallback 的依赖数组必须包含所有函数内部用到的变量(如本例中的 num),如果遗漏,可能导致函数引用失效或缓存不准确。

  • useCallback 本身不会阻止子组件渲染,但它通过稳定回调函数引用,配合 React.memo 才能真正减少子组件的更新次数。

  • 如果回调函数逻辑非常简单(例如直接调用 setNum),直接内联写法可能比引入 useCallback 更简洁高效。

使用前后对比
场景 未使用 useCallback 使用 useCallback
回调函数引用变化 每次渲染都生成新函数 仅在 num 变化时生成新函数
子组件更新控制 子组件可能因引用变化频繁更新 通过缓存减少子组件不必要的更新
React.memo 协同 无法有效控制子组件更新 React.memo 配合使用可优化渲染性能

五、useMemouseCallback 的对比

特性 useMemo useCallback
作用 缓存复杂计算的结果 缓存函数引用
返回值 返回计算结果 返回函数
使用场景 优化计算逻辑、减少冗余计算 优化回调函数引用,避免子组件不必要的更新
依赖项管理 依赖项变化时重新计算 依赖项变化时重新生成函数
与子组件配合 React.memo 配合优化 props React.memo 配合优化回调函数引用

六、实践建议

  1. 合理选择 Hook

    • 优先使用 useMemo:当需要缓存复杂计算的结果时。
    • 优先使用 useCallback:当需要稳定回调函数引用时。
  2. 避免过度使用

    • 轻量级操作无需缓存:例如简单的数学运算或字符串拼接。
    • 依赖项需准确管理:遗漏依赖项可能导致缓存结果不正确,而过度依赖则失去优化效果。
  3. React.memo 协同优化

    • 通过 useMemouseCallback 稳定 props 和回调函数引用,再结合 React.memo 防止子组件不必要的重新渲染。
  4. 组件拆分与状态隔离

    • 将组件拆分为更小的独立单元,减少每个组件的状态依赖,从而降低性能优化的复杂性。
  5. 性能监控与调试

    • 使用 console.log 或 React DevTools 的性能分析工具,验证缓存是否生效,确保优化效果符合预期。
昨天以前首页

基于 Tiptap + Yjs + Hocuspocus 的富文本协同项目,期待你的参与 😍😍😍

作者 Moment
2025年7月21日 22:11

在线预览地址

Github 地址

如果你对这个项目感兴趣,或者想参与贡献的,可以看 issue 认领需求,如果想加入交流群可以添加我微信 yunmz777 拉你进群。

技术选型

框架

技术 用途 官网链接
Next.js React 全栈框架,支持 SSR、App Router 等特性 nextjs.org
React 19 UI 框架,构建组件化用户界面 react.dev
TypeScript 静态类型系统,提升开发可靠性 www.typescriptlang.org

富文本编辑器体系(Tiptap)

技术 用途 官网链接
Tiptap 基于 ProseMirror 的无头富文本编辑器框架 tiptap.dev
Tiptap Pro 扩展 表格、占位符、数学公式、拖拽、节点 ID、Emoji 等增强功能 Pro 扩展说明
Yjs 数据同步框架,实现多人协同编辑 yjs.dev
@hocuspocus 基于 Yjs 的 WebSocket 协同服务端 hocuspocus.dev

⚙️ 工程化工具链

技术 用途 官网链接
ESLint 代码质量与规范检查工具 eslint.org
Prettier 代码自动格式化 prettier.io
Husky Git 提交钩子,配合 Lint 检查 typicode.github.io/husky
Commitizen + cz-git 规范化 Git 提交信息 commitizen-tools.github.io/commitizen/
Vitest 单元测试框架,Vite 原生支持 vitest.dev
Playwright 端对端浏览器测试 playwright.dev

样式

技术 用途 官网链接
shadcn/ui 基于 Radix UI 封装的现代 React 组件库,支持主题切换、无锁样式、自定义 Tailwind 风格 ui.shadcn.com
Tailwind CSS 原子化 CSS 样式库,与 shadcn 深度集成 tailwindcss.com
Radix UI 无样式的可访问性基础组件,shadcn 的核心依赖 www.radix-ui.com
tailwindcss-animate Tailwind 的动画插件,配合 @keyframes 使用 github.com/jamiebuilds…

其他精选依赖

技术 用途 官网链接
Zustand 轻量级状态管理库,支持持久化、异步逻辑 zustand.pmnd.rs
Framer Motion 高性能动画库,适用于组件过渡、交互反馈等 www.framer.com/motion
Lucide 现代图标库,支持 React 组件直接引入 lucide.dev
Date-fns 常用日期处理函数库,API 简洁,体积小 date-fns.org

目录结构

src/
├── app/                # Next.js 应用目录,包含页面路由、布局配置等
├── components/         # 可复用的 UI 组件库
├── extensions/         # Tiptap 编辑器的自定义扩展功能
├── hooks/              # 自定义 React Hooks
├── services/           # 业务逻辑服务层(如 API 调用、请求封装等)
├── stores/             # 状态管理(如使用 Zustand、Jotai 等)
├── styles/             # 全局样式和样式模块
├── types/              # 全局 TypeScript 类型定义
├── utils/              # 工具函数、辅助方法
├── worker/             # Web Worker 实现,用于异步或性能密集型任务
└── middleware.ts       # Next.js 中间件,用于请求拦截、认证控制等

其中 styles 里面包含了大量的 tiptap 的 css 样式,这些 CSS 文件用于为 Tiptap 提供完整的富文本样式支持。由于 Tiptap 是无头组件,所有样式(如段落、代码块、表格、列表、协同编辑等)都需自行定义。每个 CSS 文件针对一个功能模块进行样式分离,便于维护与扩展。这种拆分方式能保持样式结构清晰、职责明确。

20250714113627

每个不同的文件都代表不同的插件的样式,例如 code.css 代表代码块 <pre><code> 的样式定义,包括背景色、字体、行号样式,适配 CodeBlock 插件(如支持语法高亮的效果)。

对于 components 又分为两个不同的分类,src 目录下的是全局公共组件或者业务性质可共用的,在 app 目录下的是每个页面或者一个 layout 路由组下的业务组件,为了避免渲染成路由,可用 _components 命名的方式命名。

核心模块

Service 封装和调用

service 目录下的 request.ts 为全局的 fetch 封装:

image.png

这段代码封装了一个基于 fetch 的请求工具类,支持统一拦截、超时控制、错误处理与自动重试等高级功能。同时,它提供了 get/post/put/delete/patch 等方法,返回统一格式的响应结果,便于在项目中稳定复用。

对于不同模块的,可以再 service 目录下再新建一个新的目录作为特定模块的封装,例如我有一个 user 模块,index.ts 作为函数的封装,type.ts 作为接口的出参和入参的类型。

image.png

具体调用时可以不用添加 try...catch 语法捕捉,可以在具体调用时传入不同的错误结果不同的处理,由统一的 fetch 实例来处理。

tiptap 扩展

借助 tiptap 的强大的扩展功能,我们可以在原来的基础上扩展任务我们想要的功能,甚至你可以在 tiptap 上扩展一个页面。

如果要创建一个扩展,一般遵循以下原则:

import { Node, NodeViewRendererProps } from "@tiptap/core";
// - Node: 创建扩展的核心 API
// - NodeViewRendererProps: 自定义组件的 props 类型(含 editor、node 等)

import {
  NodeViewWrapper, // 将 React 组件包装为 NodeView
  ReactNodeViewRenderer, // 用于包裹 React 节点,Tiptap 会识别它作为 NodeView 的容器
  useEditorState, // 用于订阅 editor 的状态(如目录、选中状态等)
} from "@tiptap/react";

// 👉 你的自定义组件(实际渲染逻辑)
import MyReactComponent from "./MyReactComponent";

// 👉 正式定义扩展
export const MyNode = Node.create({
  name: "myNode", // 节点名称,必须唯一
  group: "block", // 节点分组,可选 block / inline / list
  atom: true, // 原子节点,不可编辑内部内容
  draggable: true, // 是否允许拖拽移动
  inline: false, // 是否是 inline 类型,默认为 block

  // HTML -> Node 映射(反序列化)
  parseHTML() {
    return [
      {
        tag: 'div[data-type="my-node"]',
      },
    ];
  },

  // Node -> HTML 映射(序列化)
  renderHTML({ HTMLAttributes }) {
    return ["div", { ...HTMLAttributes, "data-type": "my-node" }, ""]; // SSR 输出结构
  },

  // 客户端渲染视图(NodeView)—— 仅在浏览器中执行
  addNodeView() {
    if (typeof window !== "undefined") {
      return ReactNodeViewRenderer(MyReactComponent);
    }
    return null; // SSR 时跳过 NodeView
  },

  // 自定义命令:插入该节点
  addCommands() {
    return {
      insertMyNode:
        () =>
        ({ commands }) => {
          return commands.insertContent({
            type: this.name,
          });
        },
    };
  },
});

创建完成之后可以在这里添加并导出:

image.png

这两个文件都要。

协同

20250714130155

贡献指南

1. Fork 仓库

首先,fork 仓库到你的 GitHub 账户中。这会创建一个你自己的仓库副本。

2. 克隆仓库

在你的本地机器上克隆你刚刚 fork 的仓库:

git clone https://github.com/你的用户名/项目名.git
cd 项目名

3. 添加上游远程仓库

为了保持你的仓库与原始仓库同步,请添加上游远程仓库:

git remote add upstream https://github.com/xun082/DocFlow.git

4. 创建新分支

在开始工作之前,请确保你创建了一个新的分支:

git checkout -b feat/你的分支名

其中 feat/ 是表示“功能开发分支”的前缀,后者为你具体的功能点描述,例如:

feat/login-page             # 开发登录页面功能
feat/user-permission        # 实现用户权限控制
feat/invite-code-refactor   # 重构邀请码功能模块

对于不同的功能有不同的前缀:

前缀 说明
feature/ 新功能开发
fix/ 缺陷修复
hotfix/ 线上紧急修复
refactor/ 重构代码,不涉及功能变更
test/ 添加或修改测试相关内容
chore/ 构建、依赖等杂项维护
docs/ 文档更新

开发流程

在开启之前,请确保你的 NodeJs 版本大于或者等于 20,PNPM 版本大于或者等于 9

1. 安装依赖

在你开始开发之前,请安装所有的依赖:

pnpm install

2. 运行项目

为了确保你在一个正常运行的环境下进行开发,启动项目:

pnpm dev

3. 提交更改

在提交你的更改之前,请确保你进行了适当的代码格式化和 lint:

pnpm lint
pnpm format

然后提交你的更改:

git add .
pnpm commit

5. 同步你的分支

在你准备好提交你的更改之前,请确保你的分支是最新的:

git fetch upstream
git rebase upstream/main

6. 推送分支

将你的分支推送到你自己的仓库:

git push origin feature/你的分支名

7. 创建 Pull Request

在 GitHub 上,导航到你 fork 的仓库,点击 "Compare & pull request" 按钮。请确保你详细描述了你所做的更改。

代码审查

所有的 Pull Request 都会被审查。请注意以下几点:

  • 你的代码是否清晰且易于理解。
  • 你是否遵循了项目的代码风格和规范。
  • 你是否添加了适当的测试。
  • 你的更改是否与现有的代码兼容。

常见问题

如何报告 Bug?

如果你发现了 Bug,请在 GitHub 上创建一个 Issue,并尽可能详细地描述 Bug 及其复现步骤。

如何请求新功能?

如果你有新功能的建议,请在 GitHub 上创建一个 Issue,详细描述你的建议及其潜在的用途。

别让类名打架!CSS 模块化教你给样式上 "保险"

作者 归于尽
2025年7月21日 18:11
作为前端开发者,你是否遇到过这样的场景:辛辛苦苦写好的组件,一引入项目就发现样式全乱了;团队协作时,明明没改对方的代码,却莫名触发了样式冲突;上线前测试一切正常,生产环境突然出现按钮样式错乱…… 这些

React Key:看似平凡,实则暗藏玄机

作者 WildBlue
2025年7月20日 19:44

一、React 的 “神秘钥匙”:Key

image.png

在 React 的奇妙世界里,有一把神秘的 “钥匙”🔑,它虽然看起来普普通通,但却在 React 的运行机制中发挥着至关重要的作用,它就是 ——Key。你可别小瞧这小小的 Key,它可是 React 高效更新 DOM 的关键所在,就像是连接 React 与高性能用户界面的桥梁。

想象一下,你是一个超级英雄,拥有一个神奇的口袋,里面装着各种各样的道具。而 React 中的 Key,就像是你口袋里那个能快速识别和定位道具的神奇标签。当你需要某个道具时,这个标签就能帮助你瞬间找到它,而不是在一堆道具中手忙脚乱地翻找😜。在 React 中,当数据发生变化,需要更新界面时,Key 就起到了这样的快速识别作用,帮助 React 准确地知道哪些元素发生了变化、被添加或被删除。

再打个比方,假如你在玩一款换装游戏,游戏里有很多角色,每个角色都有自己独特的服装和配饰。当你想给某个角色换衣服时,你得先准确地找到这个角色,对吧🧐?React 中的 Key 就像是每个角色的专属 ID,有了它,React 就能轻松地找到对应的元素(角色),然后只更新这个元素(给角色换衣服),而不会影响到其他元素(其他角色)。这样不仅能提高效率,还能保证界面的稳定性和流畅性。

现在,你是不是对 React 中的 Key 有了一些初步的认识呢😎?接下来,让我们一起深入了解它的主要作用,看看这把 “神秘钥匙” 到底有多厉害!

二、为什么 React 如此依赖 Key?

image.png

(一)元素识别

在 React 的世界里,元素识别就像是一场紧张刺激的特工行动,每个元素都像是一个神秘的特工,而 Key 则是他们独一无二的身份标识🔎。

当我们在 React 中渲染一个列表时,比如一个待办事项列表,每个待办事项就是列表中的一个元素。假设我们有这样一个待办事项数组:

const todos = [
  { id: 1, title: '❤️乡❤️' },
  { id: 2, title: '乡❤️乡' },
  { id: 3, title: '❤️乡❤️' }
];

当我们要将这个数组渲染成列表时,我们会使用map方法,像这样:

{todos.map((todo) => <li key={todo.id}>{todo.title}</li>)}

这里,我们使用todo.id作为key。这样一来,React 就可以通过这个key精准地识别每个li元素。就好像特工们都有自己独特的徽章,凭借这个徽章,指挥官(React)就能轻松地辨认出谁是谁。

当数据发生变化时,Key 的作用就更加明显了。比如说,我们又添加了一个新的待办事项:

const newTodos = [
  { id: 4, title: '乡乡' },
  { id: 1, title: '❤️乡❤️' },
  { id: 2, title: '乡❤️乡' },
  { id: 3, title: '❤️乡❤️' }
];

这时,React 通过key就能知道,id为 1、2、3 的待办事项是原来就有的,只是位置可能发生了变化,而id为 4 的待办事项是新增的。于是,React 就可以有针对性地更新 DOM,只添加新的元素,而不会对其他已有的元素进行不必要的操作。

但是,如果我们没有给这些元素设置key,或者使用了错误的key,情况就会变得一团糟😱。React 就像是一个失去了导航的飞行员,无法准确判断哪些元素是新增的、哪些是被删除的,哪些只是位置发生了变化。它可能会错误地认为所有的元素都发生了改变,从而对整个列表进行重新渲染,这不仅浪费性能,还可能导致一些意想不到的问题。

(二)性能优化

Key 对于 React 的性能优化来说,就像是给汽车装上了一台超级引擎,让 React 能够高效地运行🚀。

我们知道,DOM 操作是比较耗费性能的。在 React 中,当数据发生变化时,React 会通过对比新旧虚拟 DOM 树,找出差异,然后将这些差异应用到真实的 DOM 上,这个过程被称为 “调和”(reconciliation)。而 Key 在这个过程中,就像是一把精准的手术刀,帮助 React 快速准确地找到需要更新的部分,避免对整个 DOM 进行不必要的重新渲染。

还是以上面的待办事项列表为例,如果我们给每个待办事项都设置了唯一的key,当其中一个待办事项的标题发生变化时,比如将id为 2 的待办事项标题从 “完成项目” 改为 “完成重要项目”:

const updatedTodos = [
  { id: 1, title: '❤️乡❤️' },
  { id: 2, title: '乡❤️乡' },
  { id: 3, title: '❤️乡❤️' }
];

React 通过key能够迅速定位到id为 2 的这个待办事项对应的 DOM 元素,然后只更新这个元素的文本内容,而不会影响其他元素。这样就大大减少了 DOM 操作的次数,提高了性能。

在实际项目中,这种性能优化的效果是非常显著的。比如说,在一个电商网站的商品列表页面,可能会有成百上千个商品展示。如果没有正确使用key,每次用户进行筛选、排序等操作导致数据变化时,React 可能会对整个商品列表进行重新渲染,这会导致页面卡顿,用户体验极差。而通过合理使用key,React 可以只更新那些真正发生变化的商品元素,让页面能够快速响应用户的操作,提升用户体验。

(三)状态保持

Key 在保持组件状态方面,就像是一个忠诚的管家,时刻守护着组件的状态,确保它们不会在重新渲染时丢失🗃️。

在 React 中,组件的状态(state)是与组件实例紧密关联的。当组件重新渲染时,如果没有key或者key不正确,React 可能会认为这是一个全新的组件实例,从而导致组件的状态被重置。这就好比你出门时把家里的钥匙弄丢了,回来时发现家里的一切都被重新布置了,之前的状态都消失了😫。

最常见的就是表单输入和滚动位置这两个场景。想象一下,你在一个表单中填写了很多内容,当表单数据发生变化导致组件重新渲染时,如果没有正确的key,你之前填写的内容可能会突然消失,这是不是很让人崩溃?又比如,你在一个长列表页面中滚动到了某个位置,当列表数据更新后,如果key不正确,页面可能会跳回到顶部,你又得重新滚动到原来的位置,这体验简直糟糕透顶😡。

而有了正确的key,React 就可以识别出这个组件实例还是原来的那个,从而保持其状态不变。例如,在一个包含输入框的待办事项列表中,每个待办事项都有一个输入框用于用户输入备注信息。如果我们给每个待办事项设置了唯一的key,当用户在某个输入框中输入内容后,即使列表数据发生了变化,这个输入框中的内容也不会丢失,因为 React 知道这个输入框对应的组件实例没有改变,它的状态应该被保留。

通过上面的介绍,相信你已经对 React 中 Key 的重要性有了更深入的理解。它就像一个幕后英雄,默默地在背后为 React 的高效运行、准确更新以及良好的用户体验贡献着自己的力量💪。接下来,我们再看看如果没有 Key,会引发哪些具体的问题。

三、没有 Key,React 世界会怎样?

image.png

(一)状态漂移示例

现在,让我们来揭开没有 Key 时 React 世界的 “混乱面纱”😈。先来看一段没有使用 Key 的代码示例:

import React, { useState } from 'react';
const TodoList = () => {
  const [todos, setTodos] = useState([
    { title: '任务A' },
    { title: '任务B' },
    { title: '任务C' }
  ]);
  const addTodo = () => {
    const newTodo = { title: '新任务' };
    setTodos([newTodo, ...todos]);
  };
  return (
    <div>
      <ul>
        {todos.map((todo) => <li>{todo.title}</li>)}
      </ul>
      <button onClick={addTodo}>在开头添加项目</button>
    </div>
  );
};
export default TodoList;

在这段代码中,我们创建了一个简单的待办事项列表,并且提供了一个按钮,用于在列表开头添加新的待办事项。但是,这里的列表项没有设置key。

当我们运行这段代码,并进行一些操作时,有趣的事情发生了😜。假设我们的初始待办事项列表是[任务A, 任务B, 任务C],当我们点击 “在开头添加项目” 按钮后,新的列表变成了[新任务, 任务A, 任务B, 任务C]。

这时,React 会认为:“哎呀,原来的任务A变成了新任务,任务B变成了任务A,任务C变成了任务B,还新增了一个任务C”。这就好像是一场疯狂的 “角色互换游戏”,每个任务都被错误地匹配和识别了。

这种错误的识别会导致一系列严重的问题,其中最典型的就是状态漂移。比如说,如果我们的待办事项列表中每个任务都有一个复选框,用于标记任务是否完成,当我们没有为列表项设置key时,就会出现复选框状态丢失的情况。原本勾选的任务,在添加新任务后,复选框的勾选状态可能会 “漂移” 到其他任务上,就像复选框被施了魔法一样,自己跑错了位置😱。

再比如,如果每个任务都有一个输入框,用于用户输入一些备注信息。当添加新任务导致列表重新渲染时,输入框中的内容也可能会丢失或者 “漂移” 到其他输入框中。用户辛苦输入的内容,就这么莫名其妙地消失或者跑到了不该去的地方,这体验简直糟糕透顶😡。

还有更离谱的,如果列表是一个长列表,用户已经滚动到了某个位置,当添加新任务后,滚动位置可能会错误地应用到其他元素上,用户又得重新滚动到原来的位置,这真的会让人抓狂😫。

(二)具体问题剖析

为什么会出现这些问题呢🧐?这得从 React 的渲染机制和 Diff 算法说起。

React 采用了虚拟 DOM(Virtual DOM)技术来提高性能。当数据发生变化时,React 会生成一个新的虚拟 DOM 树,然后将其与旧的虚拟 DOM 树进行对比,这个对比的过程就是 Diff 算法。Diff 算法会找出两棵树之间的差异,然后只更新那些真正发生变化的部分,而不是重新渲染整个 DOM,这样可以大大提高渲染效率。

在 Diff 算法中,当比较两个列表时,如果没有key,React 会默认使用数组的索引来识别列表项。但是,这种方式在列表项的顺序发生变化或者有新的项添加、删除时,就会出现问题。因为索引是会随着列表的变化而变化的,所以 React 可能会错误地认为某个列表项是新的或者已经被删除了,从而导致状态丢失、错误绑定等问题。

还是以上面的待办事项列表为例,当我们在列表开头添加新任务时,由于没有key,React 会根据索引来判断列表项的变化。原来索引为 0 的任务A,现在索引变成了 1,React 就会认为它是一个新的元素,而不是原来的任务A,于是就会重新创建这个元素,导致其关联的状态(如复选框的勾选状态、输入框的内容等)丢失。

而如果我们为每个列表项设置了唯一的key,React 在进行 Diff 算法时,就可以通过key准确地识别每个列表项,知道哪些是新增的、哪些是被删除的、哪些只是位置发生了变化,从而正确地更新 DOM,保持组件的状态不变。就像给每个任务都贴上了独一无二的 “身份标签”,无论它们怎么移动、变化,React 都能准确地找到它们,不会再出现混乱的情况😎。

通过上面的分析,相信你已经深刻认识到了没有 Key 时会带来的种种问题。那么,如何正确地使用 Key 呢?别急,接下来我们就详细讲解正确使用 Key 的方法和最佳实践。

四、正确使用 Key,开启 React 高效开发之门

image.png

(一)使用唯一 ID

现在我们已经知道了 Key 的重要性以及没有 Key 会带来的问题,那么如何正确地使用 Key 呢🧐?这就像是掌握了魔法咒语,能让 React 这个魔法世界更加稳定和高效地运行。

在 React 中,使用唯一 ID 作为 Key 是最正确和推荐的做法。就像每个人都有一个独一无二的身份证号码一样,每个列表项也应该有一个唯一的 ID 作为它的 “身份标识”🔑。这样,React 在识别和更新列表项时就能准确无误,不会出现混乱的情况。

我们还是以上面的待办事项列表为例,假设每个待办事项都有一个唯一的id:

const todos = [
  { id: 'todo-1', title: '学习React' },
  { id: 'todo-2', title: '完成项目' },
  { id: 'todo-3', title: '锻炼身体' }
];

当我们渲染这个列表时,就可以使用id作为key:

{todos.map((todo) => <li key={todo.id}>{todo.title}</li>)}

这样,无论列表项如何添加、删除或重新排序,React 都能通过这个唯一的id准确地识别每个列表项,从而正确地更新 DOM,保持组件的状态不变。就像给每个待办事项都贴上了一个独一无二的 “标签”,React 可以轻松地找到它们,进行相应的操作😎。

在实际项目中,我们的数据往往来自于后端接口,后端通常会为每个数据项分配一个唯一的id。我们只需要在渲染列表时,将这个id作为key传递给 React 就可以了。比如,在一个电商项目中,商品列表中的每个商品都有一个唯一的productId,我们在渲染商品列表时,就可以这样做:

const products = [
  { productId: 'prod-1', name: '商品A', price: 100 },
  { productId: 'prod-2', name: '商品B', price: 200 },
  { productId: 'prod-3', name: '商品C', price: 300 }
];
const ProductList = () => {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.productId}>
          {product.name} - ${product.price}
        </li>
      ))}
    </ul>
  );
};

通过这种方式,我们确保了每个商品列表项都有一个唯一的key,React 在更新列表时就能高效地工作,不会出现任何问题。

(二)避免使用索引

虽然在 React 中,使用数组索引作为key是一种简单的做法,但这就像是走了一条看似捷径却充满陷阱的路,会给我们带来很多麻烦😱。

想象一下,你有一个水果列表,最初是[苹果, 香蕉, 橙子],对应的索引分别是0、1、2。你使用索引作为key来渲染这个列表:

const fruits = ['苹果', '香蕉', '橙子'];
const FruitList = () => {
  return (
    <ul>
      {fruits.map((fruit, index) => (
        <li key={index}>{fruit}</li>
      ))}
    </ul>
  );
};

现在,如果你在列表开头添加一个新的水果,比如草莓,列表就变成了[草莓, 苹果, 香蕉, 橙子],索引也相应地变成了0、1、2、3。这时,React 会认为原来索引为0的苹果变成了草莓,原来索引为1的香蕉变成了苹果,以此类推。这就导致 React 会错误地更新 DOM,可能会引发一系列问题,比如组件状态丢失、输入框内容错乱、动画效果异常等😫。

更具体地说,如果你的列表项中包含一些有状态的组件,比如复选框、输入框等,使用索引作为key会导致这些组件的状态在列表重新渲染时出现混乱。例如,有一个待办事项列表,每个事项都有一个复选框用于标记是否完成,当你使用索引作为key时,如果在列表开头添加或删除一个事项,复选框的勾选状态可能会 “漂移” 到其他事项上,用户体验极差😡。

再比如,当你对列表进行排序时,使用索引作为key也会导致 React 错误地认为所有的列表项都发生了变化,从而进行不必要的 DOM 更新,浪费性能。所以,除非你的列表是静态的,不会发生增删改操作,否则一定要避免使用索引作为key❗

(三)杜绝使用随机数

还有一种错误的做法,就是使用随机数作为key,这简直就是在给 React 制造混乱,就像是在暴风雨中迷失方向的船只,让 React 完全不知所措😵。

看下面这段代码:

const items = ['项目A', '项目B', '项目C'];
const ItemList = () => {
  return (
    <ul>
      {items.map((item) => (
        <li key={Math.random()}>{item}</li>
      ))}
    </ul>
  );
};

这里我们使用Math.random()生成一个随机数作为key。看起来好像没什么问题,但实际上每次渲染时,Math.random()都会生成一个新的随机数,这就意味着每个列表项的key都会发生变化。

React 会认为每个列表项都是全新的,每次渲染都会销毁并重新创建整个列表,而不是只更新那些真正发生变化的部分。这不仅会导致性能急剧下降,还会让组件的状态无法保持,所有的努力都白费了😫。

在实际项目中,这种错误的做法可能会导致页面卡顿、响应迟缓,用户体验非常糟糕。所以,一定要杜绝使用随机数作为key,选择一个稳定的、唯一的标识符才是正确的做法💪。

通过以上介绍,相信你已经清楚地知道了如何正确地使用key,避免那些常见的错误。掌握了这些技巧,你在 React 开发中就能更加得心应手,写出高效、稳定的代码。接下来,我们再看看在使用key时还有哪些最佳实践,可以让我们的代码更加优雅和健壮😎。

五、最佳实践:让 Key 发挥最大效能

image.png

(一)使用稳定的唯一标识符

在 React 开发中,为了让 Key 充分发挥其强大的作用,我们需要遵循一些最佳实践。就像是武林高手修炼武功,只有掌握了正确的秘籍,才能发挥出最大的威力😎。

使用稳定的唯一标识符作为 Key 是最佳实践中的 “上乘武功”。在实际项目中,我们常常会遇到各种数据,这些数据往往都有一些天然的唯一标识,比如数据库 ID、邮箱地址、自定义唯一标识符等。

如果我们的数据来自数据库,那么数据库为每个数据项分配的 ID 就是一个非常理想的 Key。比如,在一个用户管理系统中,每个用户都有一个唯一的userId,当我们渲染用户列表时,就可以这样使用:

const users = [
  { userId: 1, name: '张三', age: 25 },
  { userId: 2, name: '李四', age: 30 },
  { userId: 3, name: '王五', age: 35 }
];
const UserList = () => {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.userId}>
          {user.name} - {user.age}岁
        </li>
      ))}
    </ul>
  );
};

这样,无论用户列表如何更新,React 都能通过userId准确地识别每个用户,保证界面的稳定和高效更新。

再比如,如果我们的数据是一些用户的邮箱地址,而邮箱地址本身就是唯一的,我们也可以直接使用邮箱地址作为 Key:

const userEmails = [
  { email: 'zhangsan@example.com', name: '张三' },
  { email: 'lisi@example.com', name: '李四' },
  { email: 'wangwu@example.com', name: '王五' }
];
const EmailList = () => {
  return (
    <ul>
      {userEmails.map((user) => (
        <li key={user.email}>
          {user.name} - {user.email}
        </li>
      ))}
    </ul>
  );
};

这种方式同样能确保 React 在处理列表时的准确性和高效性。

(二)组合多个字段

然而,有时候我们的数据中可能没有一个单独的字段可以作为唯一的标识符,这时候该怎么办呢🧐?别担心,我们还有一招 “组合拳”,那就是组合多个字段来生成唯一的 Key。

比如说,在一个商品管理系统中,每个商品都有一个类别(category)和一个商品 ID(productId),单独的类别或商品 ID 都不能保证唯一性,但将它们组合起来就可以了。我们可以这样做:

const products = [
  { category: '电子产品', productId: 1001, name: '手机' },
  { category: '服装', productId: 2001, name: '衬衫' },
  { category: '电子产品', productId: 1002, name: '电脑' }
];
const ProductList = () => {
  return (
    <ul>
      {products.map((product) => {
        const uniqueKey = `${product.category}-${product.productId}`;
        return (
          <li key={uniqueKey}>
            {product.name} - {product.category} - {product.productId}
          </li>
        );
      })}
    </ul>
  );
};

在这个例子中,我们通过将category和productId用-连接起来,生成了一个唯一的uniqueKey,然后将其作为 Key 传递给 React。这样,React 就能准确地区分每个商品,即使它们的类别或商品 ID 有重复的情况。

在实际场景中,这种组合字段的方式非常实用。比如在一个订单管理系统中,每个订单都有一个订单日期(orderDate)和一个订单号(orderNumber),我们可以将它们组合起来作为 Key,以确保每个订单在列表中都有唯一的标识。通过这种方式,我们可以灵活地应对各种复杂的数据情况,让 React 始终保持高效运行💪。

(三)使用库生成 UUID

如果我们的数据中实在没有合适的字段可以作为唯一标识符,也不想手动组合字段,还有一个简单又强大的方法,那就是使用库生成 UUID(通用唯一识别码)。UUID 就像是一个神奇的 “万能钥匙”,无论在什么情况下,都能为我们生成独一无二的标识符🔑。

在 React 中,我们可以使用uuid库来生成 UUID。首先,我们需要安装uuid库,打开终端,输入以下命令:

npm install uuid

安装完成后,就可以在项目中使用它了。比如,在一个待办事项应用中,当我们创建新的待办事项时,可以为其生成一个唯一的 ID:

import { v4 as uuidv4 } from 'uuid';
const createNewTodo = (title) => {
  return {
    id: uuidv4(),
    title: title,
    completed: false
  };
};
const newTodo = createNewTodo('学习React的Key');
console.log(newTodo);

在这个例子中,我们使用uuidv4()函数生成了一个唯一的 ID,并将其作为待办事项的id。当我们渲染待办事项列表时,就可以使用这个id作为 Key:

const todos = [
  { id: uuidv4(), title: '学习React', completed: false },
  { id: uuidv4(), title: '完成项目', completed: false },
  { id: uuidv4(), title: '锻炼身体', completed: false }
];
const TodoList = () => {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.title} - {todo.completed ? '已完成' : '未完成'}
        </li>
      ))}
    </ul>
  );
};

通过使用uuid库生成 UUID 作为 Key,我们可以轻松地为每个列表项提供唯一的标识符,而不用担心重复的问题。这在处理动态数据时非常方便,能够大大提高我们的开发效率和代码的健壮性😎。

掌握了这些最佳实践,我们在使用 React 中的 Key 时就能更加得心应手,避免很多潜在的问题。就像是拥有了一套强大的武器库,无论遇到什么挑战,都能轻松应对。接下来,我们通过一个实际的演示来更直观地感受 Key 的重要性。

六、实战演示:眼见为实(见码上掘金)

image.png

为了让大家更直观地感受 Key 的重要性,我们来进行一个实战演示。就像是一场精彩的魔术表演,让你亲眼见证有 Key 和没有 Key 时的奇妙差异😎。

首先,我们有一个key-demo.html文件,这个文件里包含了两个部分:一个是没有使用 Key 的待办事项列表,另一个是使用了 Key 的待办事项列表。现在,让我们一起按照以下步骤来揭开这场 “魔术” 的神秘面纱吧🧐!

(一)打开文件

找到key-demo.html文件,然后用你喜欢的浏览器打开它。就像是打开一扇通往 React 奇妙世界的大门,准备迎接即将发生的神奇变化🚪。

(二)在 “没有 Key 的情况” 部分勾选一些复选框

进入页面后,你会看到 “没有 Key 的情况” 下面有一个待办事项列表,每个事项前面都有一个复选框。现在,你可以勾选几个复选框,就像你在实际生活中标记已完成的任务一样。比如说,你勾选了 “任务 A” 和 “任务 C” 前面的复选框✅。

(三)点击 “在开头添加项目” 按钮

接下来,点击 “在开头添加项目” 按钮。这时候,神奇的事情发生了😱!你会发现,原本勾选的 “任务 A” 和 “任务 C” 前面的复选框,它们的勾选状态竟然 “漂移” 到了其他任务上。原本勾选的 “任务 A” 的复选框,现在可能跑到了 “任务 B” 上,而原本勾选的 “任务 C” 的复选框,可能跑到了 “任务 D” 上。这就像是复选框被施了魔法,自己跑错了位置,是不是很让人惊讶?

(四)观察复选框状态如何 “漂移”

仔细观察这些复选框状态的变化,你会深刻体会到没有 Key 时带来的混乱。这种混乱不仅会影响用户体验,还可能导致一些严重的问题,比如用户无法准确标记任务的完成状态,或者系统对任务状态的记录出现错误。这就好比你在整理书架时,每本书都没有编号,当你添加一本新书后,所有书的位置都乱了,你再也找不到你想要的那本书了😫。

(五)在 “有 Key 的情况” 部分重复相同操作

接下来,我们再看看有 Key 时的情况。在页面中找到 “有 Key 的情况” 下面的待办事项列表,同样地,先勾选几个复选框,比如勾选 “任务 1” 和 “任务 3” 前面的复选框✅。然后点击 “在开头添加项目” 按钮。

(六)观察复选框状态如何正确保持

这次,你会发现一个截然不同的结果。无论你怎么添加项目,勾选的复选框状态始终保持正确。原本勾选的 “任务 1” 和 “任务 3” 的复选框,它们的勾选状态不会发生任何变化,依然稳稳地标记着对应的任务。这就是 Key 的神奇之处,它就像是一个忠诚的守护者,确保每个元素的状态在数据变化时都能得到正确的维护🔐。

通过这个实战演示,相信你已经对 React 中 Key 的重要性有了更加直观和深刻的认识。就像我们看到的,有 Key 和没有 Key 时,界面的表现简直是天壤之别。有了 Key,React 就能准确地识别每个元素,保持状态的稳定;而没有 Key,就会导致各种混乱和错误。所以,在 React 开发中,一定要记得为列表项提供唯一的、稳定的 Key 哦💪!

七、总结与展望:掌握 Key,驾驭 React

image.png

到这里,我们对 React 中 Key 的探索之旅就要暂告一段落啦🎉!在这次奇妙的旅程中,我们深入了解了 Key 在 React 世界里的重要地位和神奇作用。

Key 就像是 React 的秘密武器,它是 React 准确识别列表项的 “超级望远镜”,是优化性能的 “高效引擎”,更是保持组件状态的 “忠诚卫士”。有了 Key,React 在面对复杂多变的数据时,能够快速准确地更新 DOM,为用户带来流畅稳定的体验。

我们也见识到了没有 Key 或者错误使用 Key 时的 “混乱场景”,比如复选框状态的 “漂移”、输入框内容的丢失、滚动位置的错乱以及动画效果的异常,这些问题不仅会让用户感到困惑和不满,还会影响整个应用的质量和口碑。

所以,在今后的 React 开发中,大家一定要牢记为列表项提供唯一的、稳定的 Key。就像出门一定要带钥匙一样,这已经成为 React 开发中不可或缺的 “铁律”。无论是使用数据本身的唯一标识符,还是通过组合字段、使用库生成 UUID 等方式,我们都要确保 Key 的唯一性和稳定性,让 React 能够高效地工作。

希望通过这篇文章,大家都能掌握 React 中 Key 的正确使用方法,在 React 开发的道路上一帆风顺🚀。也期待大家能够将所学运用到实际项目中,创造出更多优秀、高效的 React 应用!如果在使用 Key 的过程中遇到了什么有趣的问题或者有自己独特的见解,欢迎在评论区留言分享哦😎!

0基础进大厂,React框架基础篇:创建你的第一个React框架项目——梦开始的地方

2025年7月20日 18:05
引言 作为前端初学者,切了这么多页面,在不知道你是否会有疑问——有很多相同的HTML结构,比如导航栏、侧边栏等待,但是一个页面要有一个HTML文件,但是你知道完全没有必要再写一遍,所以你选择Ctrl+
❌
❌