React 函数组件与 Hook 实践指南

本文聚焦两件事:函数组件怎么写,以及不同 Hook 解决什么问题。 全文分为五部分:

  • 一、函数组件基础:组件怎么写、有哪些坑。
  • 二、Hook 基础:Hook 是什么、几条必须记住的规则。
  • 三、常用 Hook:日常开发天天用。
  • 四、非常用 Hook:特定场景 / 进阶。
  • 五、工程实践:自定义 Hook、性能、陷阱清单。

示例基于 React 19。多数基础 API 从 React 16.8 就有,React 19 新增的 API 会标注版本。


一、函数组件基础

1.1 函数组件是什么

一个函数组件,就是一个返回 JSX 的函数:

jsx 复制代码
function Welcome({ name }) {
  return <h1>你好,{name}</h1>;
}
  • 组件就是函数:和写普通函数一样,用组合代替继承。
  • props 是只读的 :子组件不能修改父组件传来的 props,数据只能自上而下流动。
  • 渲染要纯净 :渲染期间不要改外部变量、发请求、操作 DOM。同样的 props + state,应该渲染出同样的结果。
  • 其他逻辑使用 hook 和事件 :渲染期间只负责算 UI。需要和外部系统同步(订阅、定时器、请求)交给 useEffect 等 Hook;响应用户交互则写进事件处理函数。

StrictMode 下,开发环境会故意把渲染函数调用两次、把 Effect 按「挂载 → 卸载 → 再挂载」跑一遍(生产环境不会)。它靠「让不纯的代码把结果放大一倍」来暴露问题:比如渲染里改了外部变量,数字就会翻倍。

jsx 复制代码
// 🔴 渲染期间改了外部变量
let renderCount = 0;

function Bad() {
  renderCount++; // 每次渲染都在改它
  return <p>渲染了 {renderCount} 次</p>;
}
// StrictMode 下渲染被调用两遍 → 页面每次显示 +2

1.2 渲染模型:渲染即快照

每次渲染,都是把组件函数重新调用一次 。这一次渲染里的 props、state、函数,都被「冻结」在那一刻------也就是闭包快照。

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setTimeout(() => {
      alert(count); // 🔴 弹出的是「点击那一刻」的 count
    }, 3000);
  }

  return (
    <>
      <p>{count}</p>
      <button onClick={handleClick}>3 秒后提示</button>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
    </>
  );
}

setTimeout 闭包捕获了旧值,即使 count 更新了,还是会使用上次渲染时的 count 值。要读「最新值」又不触发渲染,用 ref 把当前值存下来,回调里读 ref.current:

jsx 复制代码
const countRef = useRef(count);

// 不要直接在渲染里写 ref,放到 Effect 中同步
useEffect(() => {
  countRef.current = count; // 每次 count 变化后同步最新值
}, [count]);

function handleClick() {
  setTimeout(() => {
    alert(countRef.current); // ✅ 读到最新值
  }, 3000);
}

1.3 组件的类型和 key

React 靠元素类型 + key 判断一个组件是「复用」还是「重建」:

  • 同位置同类型 → 复用,保留内部 state;
  • 类型变化 → 卸载重建,state 重置;
  • key 变化 → 视为另一个组件,同样重建。

用 key 主动重置状态

当同一个组件承载不同数据、你希望它们互不共享状态时:

jsx 复制代码
// 切换用户时 key 变化 → Chat 重新挂载,草稿自动清空
<Chat key={userId} userId={userId} />

不要在组件内部定义组件

jsx 复制代码
// 🔴 每次渲染都创建「新类型」,导致整棵子树卸载重建:state 丢失、性能变差
function Parent() {
  function Child() { return <p>hi</p>; }
  return <Child />;
}

1.4 函数组件 vs 类组件

结论:新代码一律用「函数组件 + Hook」。

类组件没有废弃,React 19 仍然支持,但新代码通常选择函数组件。许多新 API(例如 useActionState)只提供给函数组件;存量老代码不必为了使用 Hook 而一次性重写,两种组件可以在同一棵组件树中共存。

类组件目前唯一难以替代 的场景是错误边界 (componentDidCatch / getDerivedStateFromError 没有 Hook 等价物,通常借助 react-error-boundary 库)。

生命周期 → Hook 对照

类组件生命周期 函数组件替代方案
constructor useState(初始化函数) / useReducer
componentDidMount useEffect(fn, [])
componentDidUpdate useEffect(fn, [deps])
componentWillUnmount useEffect 的清理函数 return () => {}
shouldComponentUpdate / PureComponent React.memo + useMemo / useCallback
getDerivedStateFromProps 渲染期直接推导(尽量别用)
getSnapshotBeforeUpdate useLayoutEffect(时机接近,非精确等价)
componentDidCatch / getDerivedStateFromError 无 Hook 替代 ,仍需类组件或 react-error-boundary
forceUpdate() useReducer((x) => x + 1, 0)(反模式,不推荐)

二、Hook 基础

2.1 Hook 是什么、为什么需要它

Hook 是让函数组件拥有状态和副作用能力的函数 ,都以 use 开头。

它主要解决逻辑复用:类组件时代要有状态地复用逻辑,得靠 HOC(高阶组件:用函数「包住」组件、返回增强后的新组件)或 render props(把函数作为 prop 传入,用参数接收内部状态),层层嵌套、难以阅读;用自定义 Hook,逻辑可以直接抽成一个函数,在多处调用。

jsx 复制代码
function useOnline() { /* 一段可复用的逻辑 */ }

function A() { const online = useOnline(); /* ... */ }
function B() { const online = useOnline(); /* ... */ } // 直接复用

2.2 Hook 调用规则

Hook 只能在函数组件(或自定义 Hook)的顶层调用,不能写在条件、循环、嵌套函数里。

原因很简单:React 靠调用顺序 来对应每个 Hook 的状态------第 1 次调用对应第 1 个 Hook,第 2 次对应第 2 个。所以两次渲染之间,Hook 的数量和顺序必须一致。

错误写法:

jsx 复制代码
// 🔴 条件里调用:count 变化后 Hook 数量变了,状态错配 / 直接报错
if (count > 0) {
  const [name, setName] = useState('a');
}

// 🔴 提前 return:后面的 Hook 不执行了
function Profile({ user }) {
  if (!user) return null;
  const [name, setName] = useState(user.name);
}

// 🔴 写在事件处理里:会报 Invalid hook call
function Component() {
  function handleClick() {
    const [x] = useState();
  }
}

正确写法:所有 Hook 先无条件调用完,再做条件判断 / 返回。

jsx 复制代码
function Profile({ user }) {
  const [name, setName] = useState(user?.name ?? ''); // ✅ Hook 在前
  if (!user) return null;                             // ✅ 条件在后
  // ...
}

装上 eslint-plugin-react-hooks,写错的当下就会报警。

2.3 依赖数组机制

带依赖数组的 Hook(useEffect / useLayoutEffect / useMemo / useCallback)都会在每次渲染时比较依赖。依赖没变时,Effect 不会重新同步,Memo/Callback 会复用上一次的结果或函数。

useEffect 的三种写法:

jsx 复制代码
// ① []:通常只在挂载后执行一次;不要在里面直接读取会变化的值
useEffect(() => { sync(count); }, []);

// ② [count]:count 变化时才执行,读到最新值
useEffect(() => { sync(count); }, [count]);

// ③ 不写:每次渲染后都执行,永远读最新
useEffect(() => { sync(count); });

三、常用 Hook

3.1 useState ------ 组件自己的记忆

声明一个会触发重新渲染的状态变量,返回「当前值 + 更新函数」。它是函数组件记住数据、变化后刷新界面的主要方式。

适用场景

  • 表单输入、开关、选中项等简单可变状态;
  • 需要触发重新渲染的本地数据。

示例:受控输入框

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

function NameForm() {
  const [name, setName] = useState('');

  return (
    <label>
      姓名:
      <input value={name} onChange={(e) => setName(e.target.value)} />
    </label>
  );
}

3.2 useEffect ------ 与外部系统同步

在渲染提交后执行副作用,把组件和 React 之外的世界同步。依赖变化或首次挂载时运行,组件卸载或依赖再次变化前会先执行清理函数。

适用场景

  • 请求数据、订阅事件、启动定时器,并在卸载时清理;
  • 同步 document.title、localStorage 等副作用。

示例:订阅与清理

jsx 复制代码
useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();
  return () => connection.disconnect(); // 清理函数
}, [roomId]);

示例:请求数据 + 取消

jsx 复制代码
useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/user/${userId}`, { signal: controller.signal })
    .then((res) => res.json())
    .then(setUser)
    .catch((err) => {
      if (err.name !== 'AbortError') console.error(err);
    });

  return () => controller.abort();
}, [userId]);

3.3 useRef ------ 不触发渲染的「盒子」

返回一个带 current 属性的可变盒子,读写它都不会引起重新渲染。可以把它理解成「跨渲染存活的普通变量」,和 state 不同,改它不会刷新界面。

适用场景

  • 获取 DOM 节点并调用其方法(聚焦、滚动、播放);
  • 保存定时器 id、上一次的值、请求标记等「渲染之外」的数据。

示例:操作 DOM

jsx 复制代码
function TextInput() {
  const inputRef = useRef(null);

  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>聚焦</button>
    </>
  );
}

示例:保存定时器 id

jsx 复制代码
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  function start() {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
  }

  function stop() {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  }
  // ...
}

3.4 useContext ------ 跨层级读取共享数据

读取最近的 Context Provider 提供的值,让深层组件不必一层层通过 props 传递。适合跨层级共享主题、语言、登录用户等数据。

适用场景

  • 主题、语言、当前登录用户、权限等全局或大范围共享的数据;
  • 常配合自定义 Hook 封装,对外暴露更友好的接口。

示例:主题

jsx 复制代码
import { createContext, useContext } from 'react';

// 默认值只在没有 Provider 时使用;设为 undefined 便于发现漏写 Provider
const ThemeContext = createContext(undefined);

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>按钮</button>;
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

示例:封装成自定义 Hook

jsx 复制代码
function useTheme() {
  const theme = useContext(ThemeContext);
  if (theme === undefined) throw new Error('useTheme 必须在 Provider 内使用');
  return theme;
}

3.5 useMemo ------ 缓存计算结果

在依赖不变时复用上一次的计算结果,避免每次渲染都重跑昂贵运算。返回缓存下来的值(计算结果、对象或数组)。

适用场景

  • 大列表过滤 / 排序 / 统计等耗时计算;
  • 需要给子组件或 Effect 稳定对象 / 数组引用。

示例

jsx 复制代码
const visibleTodos = useMemo(
  () => filterTodos(todos, tab),
  [todos, tab]
);

// 稳定对象引用,避免子组件因「每次都是新对象」而重渲染
const options = useMemo(() => ({ page, size }), [page, size]);

3.6 useCallback ------ 缓存函数引用

在依赖不变时返回同一个函数引用,避免函数在每次渲染时被重新创建。本质上是「缓存函数的 useMemo」。

适用场景

  • 把回调传给用 memo 包裹的子组件,避免其无谓重渲染;
  • 函数作为其他 Hook(如 useEffect)的依赖时,保持引用稳定。

示例

jsx 复制代码
const handleDelete = useCallback((id) => {
  setTodos((prev) => prev.filter((t) => t.id !== id));
}, []);

// <TodoItem onDelete={handleDelete} />

3.7 useReducer ------ 复杂状态逻辑

把多个状态的更新逻辑集中到一个纯函数 reducer 里,通过 dispatch(action) 触发,类似简化版 Redux,返回 [state, dispatch]。

适用场景

  • 多个互相关联的状态字段,更新逻辑复杂;
  • 状态变化能清晰表示成一系列「动作」(action);
  • 想把更新逻辑抽出来单独测试。

示例:计数器状态机

jsx 复制代码
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw Error('未知 action:' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  );
}

3.8 useTransition ------ 把更新标记为非阻塞

把某次状态更新标记为「过渡更新」,让它可以被紧急更新(如输入)打断,从而保持界面响应。返回 [isPending, startTransition]。

适用场景

  • 切换 Tab、筛选、路由跳转等可能渲染较重的内容;
  • 用 isPending 展示加载反馈。

示例:切换标签页不卡输入

jsx 复制代码
function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function select(nextTab) {
    startTransition(() => {
      setTab(nextTab); // 这次更新可被打断
    });
  }

  return (
    <>
      <button onClick={() => select('home')}>首页</button>
      <button onClick={() => select('posts')}>文章</button>
      {isPending && <Spinner />}
      {tab === 'home' ? <Home /> : <Posts />}
    </>
  );
}

3.9 useDeferredValue ------ 让慢部分「滞后」更新

拿到某个值的「延迟版本」:先用旧值渲染保证响应,再在后台用最新值追上。它的更新可以被更高优先级的更新打断。

适用场景

  • 搜索联想、实时过滤大列表:输入框要即时,结果列表可以稍慢;
  • 当值来自 props、无法在产生更新的地方调用 startTransition 时,可以用它把慢组件的更新推迟。

示例:输入即时、列表延迟

jsx 复制代码
function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);

  return (
    <>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <SlowList text={deferredText} />
    </>
  );
}

3.10 useId ------ 生成稳定的无障碍 id

生成一个在服务端和客户端都一致、且在本组件内唯一的 id,专门用于 id / htmlFor、aria-* 等无障碍属性。

适用场景

  • label 与表单控件关联(htmlFor / id);
  • aria-describedby、aria-labelledby 等无障碍属性。

示例

jsx 复制代码
function PasswordField() {
  const id = useId();

  return (
    <>
      <label htmlFor={id}>密码</label>
      <input id={id} type="password" />
    </>
  );
}

四、非常用 Hook

4.1 useLayoutEffect ------ 在浏览器绘制前同步执行

useEffect 的同步版本,在 DOM 更新后、浏览器重绘前立即执行。因为它会阻塞绘制,只有确实需要避免闪烁时才用。

适用场景

  • 需要读布局信息(尺寸、位置)并同步调整 UI,避免画面闪烁;
  • 工具提示定位、滚动位置恢复、测量元素。

示例:测量并定位工具提示

jsx 复制代码
function Tooltip({ targetRef }) {
  const [rect, setRect] = useState(null);
  useLayoutEffect(() => {
    const node = targetRef.current;
    if (node) setRect(node.getBoundingClientRect());
  }, [targetRef]);
  return rect && <div style={{ top: rect.bottom }}>提示</div>;
}

4.2 useImperativeHandle ------ 定制对外暴露的 ref

配合 ref 使用,把子组件对外暴露的内容「收窄」成一组指定方法,而不是整个 DOM 节点。

适用场景

  • 封装第三方输入组件时,只暴露 focus()、clear()、scrollTo() 等;
  • 库 / 设计系统组件。

示例

jsx 复制代码
import { useRef, useImperativeHandle } from 'react';

// React 19:ref 可以像普通 prop 一样接收
function FancyInput({ ref }) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus() { inputRef.current.focus(); },
    clear() { inputRef.current.value = ''; },
  }), []);

  return <input ref={inputRef} />;
}

// 父组件:<FancyInput ref={inputRef} /> 后调用 inputRef.current.focus()

4.3 useSyncExternalStore ------ 订阅外部 store

安全地订阅 React 之外的数据源,并在数据变化时触发重新渲染,保证并发渲染下数据一致。是状态库与 React 对接的标准方式。

适用场景

  • 集成第三方状态库(Redux、Zustand 等内部就用它);
  • 订阅浏览器暴露的可变值(网络状态、媒体查询、窗口尺寸)。

示例:订阅网络状态

jsx 复制代码
function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

const getSnapshot = () => navigator.onLine;
const useOnlineStatus = () =>
  useSyncExternalStore(subscribe, getSnapshot, () => false);

// 用法:在组件里像普通 Hook 一样调用
function StatusBar() {
  const isOnline = useOnlineStatus();
  return <p>{isOnline ? '在线' : '离线'}</p>;
}

4.4 useActionState ------ 管理 Action 的状态(React 19)

把表单提交 / 异步 Action 的「结果状态 + pending 状态 + 触发函数」打包管理,返回 [state, formAction, isPending]。

适用场景

  • 表单提交,需要展示校验错误、提交中状态;
  • 配合 Server Actions / <form action={...}> 使用。

示例:登录表单

jsx 复制代码
import { useActionState } from 'react';

async function submitAction(prevState, formData) {
  const name = formData.get('name');
  if (!name) return { error: '姓名不能为空' };

  await saveToServer(name);
  return { success: true };
}

function NameForm() {
  const [state, formAction, isPending] = useActionState(submitAction, null);

  return (
    <form action={formAction}>
      <input name="name" />
      {state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
      <button disabled={isPending}>
        {isPending ? '提交中...' : '提交'}
      </button>
    </form>
  );
}

4.5 useOptimistic ------ 先展示乐观结果(React 19)

在异步请求真正返回前,先把「预期结果」显示给用户,等真实结果到达后自动替换;请求失败则会回滚。

适用场景

  • 点赞、发消息、加购物车等「先给反馈、再等服务端」的交互;
  • 配合 useActionState / <form action> / startTransition 使用。

示例:点赞乐观更新

jsx 复制代码
function LikeButton({ likes, saveLike }) {
  const [optimisticLikes, addOptimistic] = useOptimistic(
    likes,
    (current, change) => current + change
  );

  async function action() {
    addOptimistic(1);
    await saveLike();
  }

  return <form action={action}><button>赞 {optimisticLikes}</button></form>;
}

4.6 useFormStatus ------ 在子组件读取表单提交状态(react-dom)

在表单的子组件 里读取最近一次提交的状态(主要是 pending),无需父级通过 props 透传。从 react-dom 引入,不是 react。

适用场景

  • 提交按钮、加载指示器放在子组件里,却想知道父级 <form> 是否在提交;
  • 配合 <form action={...}>(Server Actions / useActionState)使用。

示例:子组件读取提交状态

jsx 复制代码
function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? '提交中...' : '提交'}</button>;
}

function Form() {
  return <form action={submitAction}><SubmitButton /></form>;
}

4.7 useDebugValue ------ 给自定义 Hook 加调试标签

为自定义 Hook 在 React DevTools 中显示一个可读的标签,属于纯开发体验的优化。

适用场景

  • 自定义 Hook 希望在 DevTools 中展示更有意义的标签;
  • 排查库或团队内部 Hook 问题时辅助定位。

示例:给自定义 Hook 加标签

jsx 复制代码
function useOnlineStatus() {
  const online = useSyncExternalStore(subscribe, getSnapshot);
  useDebugValue(online ? '在线' : '离线');
  return online;
}

4.8 useInsertionEffect ------ 在 DOM 变更前插入样式

在 React 修改 DOM 之前触发,专供 CSS-in-JS 库在布局读取前插入动态样式。

适用场景

  • 样式库需要在布局读取前把 <style> 插入 DOM;
  • 普通业务代码几乎不会用到。

示例:注入动态样式

jsx 复制代码
useInsertionEffect(() => {
  const style = document.createElement('style');
  style.textContent = `.box { color: ${color}; }`;
  document.head.appendChild(style);
  return () => style.remove();
}, [color]);

4.9 useEffectEvent ------ 在 Effect 中读取最新值(React 19.2)

让 Effect 中的回调读到最新的 props / state,同时 Effect 不会因为这个值变化而重建。

适用场景

  • Effect 的响应式依赖(如 roomId)决定它何时重新同步------这些值要留在依赖数组里;
  • 但 Effect 里注册的回调(连接、订阅、定时器等)又需要读取最新 的 theme 等值;Effect 不重建时,普通闭包只能读到旧值;
  • 把这类「只读最新值、不决定是否重建」的逻辑放进 Effect Event,即可同时做到「不重建」和「读到最新值」。

示例:连接回调读取最新主题

jsx 复制代码
const onConnected = useEffectEvent(() => {
  showNotification('连接成功', theme); // 读取最新 theme
});

useEffect(() => {
  const connection = createConnection(roomId);
  connection.on('connected', onConnected);
  connection.connect();
  return () => connection.disconnect();
}, [roomId]); // theme 变化不会重建连接

五、工程实践

5.1 自定义 Hook ------ 真正的「复用逻辑」

把多个组件共享的状态逻辑,抽成一个以 use 开头的函数,就是自定义 Hook。它本质是普通 JavaScript 函数,内部可以调用其它 Hook。

jsx 复制代码
// useWindowSize.js
import { useState, useEffect } from 'react';

export function useWindowSize() {
  const [size, setSize] = useState(() => ({
    width: window.innerWidth,
    height: window.innerHeight,
  }));

  useEffect(() => {
    const onResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  return size;
}

这个例子适用于浏览器环境;如果项目使用 SSR,需要避免在服务端初始化时直接访问 window,并为首屏准备一个服务端可用的初始值。

jsx 复制代码
// 使用
const { width } = useWindowSize();

5.2 渲染性能

在写 useMemo / useCallback 之前,先考虑两个更简单、收益更大的手段。

手段一:状态下沉(colocation)

把 state 放到真正用到它的最近组件,而不是一股脑堆在顶层。state 在谁那里,谁变化时就只重渲染那棵小树。

jsx 复制代码
// 🔴 hover 状态放在大列表里,每次 hover 都重渲染整个列表
function List() {
  const [hovered, setHovered] = useState(null);
  return items.map((item) => (
    <Row key={item.id} onHover={() => setHovered(item.id)} />
  ));
}

// ✅ 把 hover 状态下沉到 Row 内部,List 不再受影响

手段二:用 children 做插槽,隔离重渲染

children 是由父组件 渲染后作为「元素」传进来的。拥有自己 state 的组件重渲染时,并不会重新创建这个元素,因此 children 可以跳过重渲染------不写 memo 也能优化:

jsx 复制代码
function ColorBox({ children }) {
  const [color, setColor] = useState('red');

  // color 变化只会重渲染 ColorBox 自身;
  // children 是同一个元素引用,ExpensiveList 不会重渲染。
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {children}
    </div>
  );
}

// 把昂贵的子树作为 children 传进去
<ColorBox>
  <ExpensiveList />
</ColorBox>

前提是 <ExpensiveList /> 由 ColorBox 的上层 创建并传入,而不是写在 ColorBox 内部。

5.3 常见陷阱 checklist

提交前对照一遍,能挡掉大部分低级问题:

  • Hook 都在顶层,没写在条件 / 循环 / 提前 return 前后。
  • useEffect 的依赖数组写全了里面用到的响应式值。
  • 没在渲染期间做副作用(改外部变量、发请求、操作 DOM)。
  • state 是返回新对象更新的,没有直接改原对象。
  • 列表没有用数组下标当 key;key 来自数据本身。
  • 没有在组件内部定义组件。
  • 需要读最新值的地方用了函数式更新或 ref,而不是直接读闭包旧值。
  • 表单优先受控(value + onChange)。
  • 没有「见计算就包 useMemo」。
  • 能靠 props / state 推导的值,没有用 useEffect 存一份。
相关推荐
颜进强1 小时前
17 · 自定义 Provider 四形态:useValue / useClass / useFactory / useExisting 到底在选什么
前端·后端·ai编程
传人once1 小时前
网页文字滚动效果如何做
前端·css·html·jquery·html5
liangshanbo12151 小时前
面试题:React 中 Lane 和 Scheduler 是怎么配合的?
前端·react.js·前端框架
liangshanbo12152 小时前
请求拦截器相关面试题
java·前端·javascript
ShawnLiaoking2 小时前
ADS305 Big Data Lecuture4 Web Scraping
大数据·前端
liangshanbo12152 小时前
React面试题:Hooks 为什么不能在分支和循环里面写?
前端·javascript·react.js
FungLeo2 小时前
成为全栈·Next.js 网站前台篇·响应式、可访问性与错误状态:内容站不能只验桌面首页
react.js·响应式设计·next.js·移动端适配·成为全栈·web 可访问性
开开心心就好2 小时前
视频压缩工具推荐,绿色版免安装双击即用
java·前端·人工智能·spring·智能手机·intellij-idea·excel
西红柿炖牛腩3542 小时前
GLB 转 OBJ/PLY 后体积没变?3D 模型不减面变小靠什么
前端·javascript·3d