React 常用技术知识全景:从组件到 Hooks 的系统理解

React 常用技术知识全景:从组件到 Hooks 的系统理解

你写 React 时经常会遇到这些问题:为什么 setState 后马上读不到新值?为什么列表不能用数组下标当 key?为什么 useEffect 会重复执行?

这些问题不是孤立 API 细节,而是 React 的核心模型在真实代码里的表现。

如果能把组件、状态、渲染、Effect 和数据流串起来,React 代码会更容易维护,面试里的追问也能回答得更有条理。

本文面向已经会写基础 JSX、想系统整理 React 常用知识的前端开发者。文章以 React 19 文档中的当前推荐模型为参考,但大部分内容同样适用于 React 18 的函数组件和 Hooks 开发。

1. React 的核心:用组件描述 UI

React 的第一层心智模型是:UI 是组件树,组件是返回 JSX 的 JavaScript 函数。

jsx 复制代码
export default function ProfileCard() {
  return (
    <article>
      <h2>Ada Lovelace</h2>
      <p>Frontend Engineer</p>
    </article>
  );
}

这个组件没有状态,也没有副作用。给定同样的输入,它应该返回同样的 UI 描述。

边界与失效场景:组件函数不要在渲染过程中修改外部变量、发请求、注册事件或直接操作 DOM。React 官方规则强调组件和 Hooks 应保持纯粹;与外部系统同步应放到事件处理函数或 Effect 中。

面试回答模板:

React 组件本质上是描述 UI 的函数。组件接收 props、读取 state,返回 JSX。React 根据组件返回的描述来计算需要展示的界面。

2. JSX:JavaScript 里的 UI 描述语法

JSX 不是 HTML 字符串,而是 JavaScript 的语法扩展。它让我们在 JavaScript 中描述 UI 结构。

jsx 复制代码
const user = {
  name: "Ada",
  avatarUrl: "https://example.com/avatar.png",
};

export default function UserAvatar() {
  return (
    <img
      src={user.avatarUrl}
      alt={`${user.name}'s avatar`}
      width={80}
      height={80}
    />
  );
}

JSX 中使用 {} 插入 JavaScript 表达式。属性名通常使用 DOM 对应的 React 写法,例如 classNamehtmlFor

边界与失效场景:JSX 中只能放表达式,不能直接写 if 语句或 for 语句。需要条件渲染时,用提前 return、三元表达式或 &&;需要列表渲染时,用 map() 生成 JSX 数组。

3. props:父组件向子组件传递输入

props 是组件的外部输入。它适合表达"父组件决定,子组件消费"的数据。

jsx 复制代码
function Badge({ tone, children }) {
  return <span className={`badge badge-${tone}`}>{children}</span>;
}

export default function App() {
  return <Badge tone="success">Published</Badge>;
}

children 是一种特殊 prop,代表组件标签中间包裹的内容。

边界与失效场景:子组件不要直接修改 props。props 代表父组件传入的快照,子组件需要变化时,应通过事件回调通知父组件更新状态。

jsx 复制代码
function CounterButton({ count, onIncrement }) {
  return (
    <button type="button" onClick={onIncrement}>
      Count: {count}
    </button>
  );
}

export default function CounterPanel() {
  const [count, setCount] = React.useState(0);

  return (
    <CounterButton
      count={count}
      onIncrement={() => setCount((currentCount) => currentCount + 1)}
    />
  );
}

边界与失效场景:上面的代码需要 React 在作用域中。如果使用现代 JSX transform,项目可不显式 import React from "react";为了让片段单独复制更清晰,也可以写成 import { useState } from "react"

4. state:组件自己的记忆

state 是组件实例内部的记忆。状态变化会请求 React 重新渲染组件。

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

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

useState(0) 返回当前状态和更新函数。点击按钮时调用 setCount,React 会安排一次重新渲染。

边界与失效场景:setCount 不会立刻改变当前这次渲染里的 count 变量。React 官方文档把 state 描述为"快照":一次渲染中的事件处理函数读到的是那次渲染对应的状态值。

5. State 是快照:为什么 setState 后马上读不到新值

看这段代码:

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

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
    console.log(count);
  }

  return (
    <button type="button" onClick={handleClick}>
      Count: {count}
    </button>
  );
}

点击按钮后,console.log(count) 打印的是当前渲染中的旧值。setCount 请求 React 用新值进行下一次渲染,不会修改当前闭包里的 count

边界与失效场景:如果下一次状态依赖上一次状态,应使用函数式更新,避免闭包里的旧值影响计算。

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

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount((currentCount) => currentCount + 1);
    setCount((currentCount) => currentCount + 1);
    setCount((currentCount) => currentCount + 1);
  }

  return (
    <button type="button" onClick={handleClick}>
      Count: {count}
    </button>
  );
}

边界与失效场景:函数式更新适合"新状态依赖旧状态"的场景。如果新状态完全来自用户输入或接口返回值,可以直接传入新值。

6. 渲染与提交:React 如何把状态变化变成页面变化

React 更新 UI 可以理解成三个阶段:

  1. 触发渲染:组件首次挂载,或 state、props、context 变化。
  2. 渲染组件:React 调用组件函数,得到新的 JSX 描述。
  3. 提交更新:React 把必要变化应用到 DOM。
flowchart TD A[&#34;用户点击按钮&#34;] --> B[&#34;调用 setState&#34;] B --> C[&#34;React 安排一次渲染&#34;] C --> D[&#34;调用组件函数&#34;] D --> E[&#34;生成新的 JSX 描述&#34;] E --> F[&#34;比较前后 UI 描述&#34;] F --> G[&#34;提交必要 DOM 更新&#34;]

边界与失效场景:渲染阶段应该保持纯粹。不要在组件函数体里直接调用 setState、发请求或写 DOM;这些行为会让渲染过程不可预测。

调试信号:

  • 在组件函数顶部写 console.log("render", props, state) 观察渲染次数。
  • 使用 React DevTools 查看组件树、props、state。
  • 使用浏览器 Performance 面板观察长任务和提交阶段耗时。

7. 条件渲染:让 UI 跟随状态分支

React 中条件渲染本质上就是 JavaScript 条件表达式。

jsx 复制代码
function EmptyState() {
  return <p>No data yet.</p>;
}

function UserList({ users }) {
  if (users.length === 0) {
    return <EmptyState />;
  }

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default function App() {
  return <UserList users={[{ id: "u1", name: "Ada" }]} />;
}

边界与失效场景:如果使用 condition && <Component />,要确认 condition 是布尔值。0 && <Component /> 会渲染出 0,这在计数场景中会制造 UI 噪声。

8. 列表与 key:告诉 React 每一项是谁

渲染列表时,JSX 元素需要稳定的 key

jsx 复制代码
const todos = [
  { id: "t1", text: "Read React docs" },
  { id: "t2", text: "Write notes" },
];

export default function TodoList() {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

React 使用 key 判断数组中的每个组件对应哪条数据。当列表插入、删除、排序时,稳定的 key 能帮助 React 正确保留或重置对应项的状态。

边界与失效场景:不要在会增删排序的列表里使用数组下标作为 key。下标跟"位置"绑定,不跟"数据身份"绑定;顺序变化后,组件状态会跟着位置走,造成输入框内容错位等问题。

9. 表单:受控组件让状态成为唯一来源

受控组件把输入值交给 React state 管理。

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

export default function SearchBox() {
  const [query, setQuery] = useState("");

  function handleSubmit(event) {
    event.preventDefault();
    console.log("search:", query.trim());
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="query">Keyword</label>
      <input
        id="query"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <button type="submit">Search</button>
    </form>
  );
}

边界与失效场景:受控输入必须同时提供 valueonChange。如果只传 value 不更新状态,输入框会变成不可编辑。处理用户输入时要考虑空字符串、前后空格和提交时机。

10. 事件处理:事件里适合处理用户动作

事件处理函数用于响应用户交互,例如点击、输入、提交。

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

export default function LikeButton() {
  const [liked, setLiked] = useState(false);

  function handleClick() {
    setLiked((currentLiked) => !currentLiked);
  }

  return (
    <button type="button" aria-pressed={liked} onClick={handleClick}>
      {liked ? "Liked" : "Like"}
    </button>
  );
}

边界与失效场景:事件处理函数不要写成 onClick={handleClick()},这样会在渲染时立即调用。正确写法是 onClick={handleClick}onClick={() => handleClick(id)}

11. Effect:用于同步 React 外部系统

useEffect 不是"组件加载后运行代码"的通用出口。React 官方文档把 Effect 定义为:让组件与 React 外部系统同步。

外部系统包括:

  • setIntervalclearInterval 管理的 timer。
  • window.addEventListener 事件订阅。
  • WebSocket、第三方组件、地图、视频播放器。
  • 需要与当前 props/state 同步的外部资源。
jsx 复制代码
import { useEffect, useState } from "react";

export default function WindowWidth() {
  const [width, setWidth] = useState(() => window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return <p>Window width: {width}</p>;
}

边界与失效场景:这段代码只能在浏览器环境运行,因为它读取了 window。如果用于服务端渲染或 React Server Components 环境,需要把访问 window 的逻辑限制在客户端组件或 Effect 中。

12. 不是所有逻辑都需要 Effect

如果一个值可以从 props 或 state 直接计算出来,就不要再放一份 state,也不要用 Effect 同步它。

不推荐:

jsx 复制代码
import { useEffect, useState } from "react";

export default function FullName({ firstName, lastName }) {
  const [fullName, setFullName] = useState("");

  useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
  }, [firstName, lastName]);

  return <p>{fullName}</p>;
}

推荐:

jsx 复制代码
export default function FullName({ firstName, lastName }) {
  const fullName = `${firstName} ${lastName}`;

  return <p>{fullName}</p>;
}

边界与失效场景:Effect 适合"同步外部系统",不适合把可计算数据从一份 state 复制到另一份 state。多余的 Effect 会增加渲染次数和状态不一致的机会。

13. Effect 生命周期:同步、清理、重新同步

Effect 的生命周期不是"组件生命周期"的一一映射,而是围绕同步过程展开:

  1. 组件渲染后,React 运行 Effect。
  2. 依赖变化时,React 先运行上一次 Effect 的清理函数。
  3. React 再运行本次渲染对应的新 Effect。
  4. 组件卸载时,React 运行最后一次清理函数。
jsx 复制代码
import { useEffect } from "react";

function createConnection(roomId) {
  return {
    connect() {
      console.log("connect:", roomId);
    },
    disconnect() {
      console.log("disconnect:", roomId);
    },
  };
}

export default function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();

    return () => {
      connection.disconnect();
    };
  }, [roomId]);

  return <p>Room: {roomId}</p>;
}

边界与失效场景:依赖数组要包含 Effect 中读取的响应式值,例如 props、state 和组件内定义的变量。为了"少执行"而漏写依赖,会让 Effect 使用过期值。

调试信号:

  • 在 Effect body 和 cleanup 中分别打印 console.log("connect", value)console.log("cleanup", value)
  • 使用 React DevTools 检查 props/state 是否按预期变化。
  • 排查重复订阅时,检查 cleanup 是否确实移除了旧订阅。

14. Hooks 规则:只能在顶层调用

Hooks 是 React 的特殊函数,调用位置有明确限制:

  • 只能在组件或自定义 Hook 顶层调用。
  • 不要在条件、循环、嵌套函数中调用 Hooks。

错误写法:

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

export default function Panel({ enabled }) {
  if (enabled) {
    const [count, setCount] = useState(0);
    return <button onClick={() => setCount(count + 1)}>{count}</button>;
  }

  return <p>Disabled</p>;
}

推荐写法:

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

export default function Panel({ enabled }) {
  const [count, setCount] = useState(0);

  if (!enabled) {
    return <p>Disabled</p>;
  }

  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

边界与失效场景:React 依赖 Hooks 的调用顺序来关联每个 Hook 的状态。条件调用会让不同渲染之间的 Hook 顺序不一致,导致状态错位。

15. 自定义 Hook:复用状态逻辑,而不是复用 UI

自定义 Hook 是以 use 开头的函数,用来复用状态逻辑。

jsx 复制代码
import { useEffect, useState } from "react";

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(() => navigator.onLine);

  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }

    function handleOffline() {
      setIsOnline(false);
    }

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    return () => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, []);

  return isOnline;
}

export default function NetworkBadge() {
  const isOnline = useOnlineStatus();

  return <p>{isOnline ? "Online" : "Offline"}</p>;
}

边界与失效场景:自定义 Hook 复用的是逻辑,不共享状态。每次调用 useOnlineStatus() 的组件都会拥有自己的 Hook 状态和 Effect 同步过程。

16. 状态设计:把状态放在真正拥有它的地方

React 状态设计常见原则:

  1. 如果数据只影响一个组件,放在这个组件内部。
  2. 如果多个兄弟组件需要共享,提升到最近的共同父组件。
  3. 如果很多远距离组件需要共享,再考虑 Context 或状态库。
  4. 如果数据可以计算得到,不要额外存成 state。
flowchart TD A[&#34;多个组件都需要同一份状态?&#34;] -->|否| B[&#34;放在使用它的组件内部&#34;] A -->|是| C[&#34;是否有最近共同父组件?&#34;] C -->|是| D[&#34;状态提升到共同父组件&#34;] C -->|否| E[&#34;考虑 Context 或外部状态库&#34;] D --> F[&#34;通过 props 和事件回调传递&#34;] E --> G[&#34;按读写频率拆分状态边界&#34;]

边界与失效场景:状态提升会让父组件承担更多协调逻辑。把所有状态都放到全局会让数据流变得不透明,调试和复用成本上升。

17. 更新对象和数组:不要直接修改 state

React state 中的对象和数组应该按不可变方式更新。

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

export default function ProfileEditor() {
  const [profile, setProfile] = useState({
    name: "Ada",
    city: "London",
  });

  function handleCityChange(event) {
    setProfile((currentProfile) => ({
      ...currentProfile,
      city: event.target.value,
    }));
  }

  return (
    <label>
      City
      <input value={profile.city} onChange={handleCityChange} />
    </label>
  );
}

边界与失效场景:不要写 profile.city = "Paris"; setProfile(profile);。这会复用同一个对象引用,React 和开发者都更难判断哪里发生了变化。

数组更新示例:

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

export default function TodoList() {
  const [todos, setTodos] = useState([
    { id: "t1", text: "Read docs", done: false },
  ]);

  function toggleTodo(id) {
    setTodos((currentTodos) =>
      currentTodos.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  }

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <label>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => toggleTodo(todo.id)}
            />
            {todo.text}
          </label>
        </li>
      ))}
    </ul>
  );
}

边界与失效场景:mapfilter、展开运算符适合普通对象和数组的不可变更新。深层嵌套状态会让更新代码变复杂,应优先调整状态结构。

18. Context:解决跨层级传递,不替代所有状态管理

Context 适合传递主题、语言、当前用户、权限等跨层级数据。

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

const ThemeContext = createContext("light");

function Toolbar() {
  const theme = useContext(ThemeContext);

  return <div data-theme={theme}>Toolbar theme: {theme}</div>;
}

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

边界与失效场景:Context 更新会影响读取该 Context 的组件。高频变化的数据不要随意塞进一个巨大的 Context;可以按领域拆分 Context,或使用更适合高频更新的状态方案。

19. refs:保存不触发渲染的可变值

useRef 返回一个稳定对象,修改 ref.current 不会触发渲染。

jsx 复制代码
import { useRef } from "react";

export default function FocusInput() {
  const inputRef = useRef(null);

  function handleFocus() {
    inputRef.current?.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button type="button" onClick={handleFocus}>
        Focus
      </button>
    </>
  );
}

边界与失效场景:ref 适合保存 DOM 引用、timer id、上一次值等不参与渲染的数据。参与 UI 展示的数据应使用 state,否则界面不会随着值变化更新。

20. memo、useMemo、useCallback:只在有明确原因时使用

React 中常见的性能 API:

  • memo:让组件在 props 没变时跳过重新渲染。
  • useMemo:缓存一次计算结果。
  • useCallback:缓存函数引用。
jsx 复制代码
import { memo, useMemo, useState } from "react";

const ProductList = memo(function ProductList({ products }) {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
});

export default function ProductPage({ products }) {
  const [keyword, setKeyword] = useState("");

  const filteredProducts = useMemo(() => {
    const normalizedKeyword = keyword.trim().toLowerCase();

    if (normalizedKeyword.length === 0) {
      return products;
    }

    return products.filter((product) =>
      product.name.toLowerCase().includes(normalizedKeyword)
    );
  }, [keyword, products]);

  return (
    <>
      <input
        value={keyword}
        onChange={(event) => setKeyword(event.target.value)}
      />
      <ProductList products={filteredProducts} />
    </>
  );
}

边界与失效场景:useMemo 不是语义保证,而是性能优化提示。只有计算成本、引用稳定性或子组件跳过渲染确实重要时再使用。没有测量或明确原因时,先保持代码简单。

21. 错误边界:捕获渲染阶段错误

错误边界用于捕获子组件渲染过程中的错误,并展示备用 UI。当前 React 官方文档仍使用 class component 定义错误边界。

jsx 复制代码
import { Component } from "react";

export class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    console.error("ErrorBoundary caught:", error, info);
  }

  render() {
    if (this.state.hasError) {
      return <p>Something went wrong.</p>;
    }

    return this.props.children;
  }
}

边界与失效场景:错误边界不捕获事件处理函数里的错误。事件里的错误要用 try-catch 或统一异常处理逻辑处理。

22. React 技术知识调试清单

遇到 React 问题时,可以按这条线排查:

  1. UI 没更新:确认是否调用了 state setter,是否直接修改了对象或数组。
  2. 状态值旧:确认是否在当前渲染快照里读取 state,是否需要函数式更新。
  3. Effect 重复执行:确认依赖数组和 cleanup,检查开发环境 Strict Mode 下的额外检查。
  4. 列表状态错位:检查 key 是否稳定,是否使用了数组下标。
  5. 组件重复渲染:用 React DevTools 和 console.log 定位是谁触发更新。
  6. 表单不可输入:检查受控组件是否同时有 valueonChange
  7. DOM 操作失效:检查 ref 是否拿到节点,操作是否发生在浏览器环境。

23. 面试回答模板

问题 回答重点
React 组件是什么? 返回 JSX 的 JavaScript 函数,用 props、state 和 context 描述 UI。
props 和 state 区别? props 是父组件传入的外部输入;state 是组件自己的记忆,更新会触发渲染。
为什么 state 更新后马上读不到新值? state 是当前渲染的快照,setter 请求下一次渲染,不修改当前闭包变量。
key 有什么作用? key 标识列表项身份,帮助 React 在增删排序时匹配组件和数据。
useEffect 适合做什么? 同步 React 外部系统,例如事件订阅、timer、连接、第三方组件。
为什么 Hooks 不能写在条件里? React 依赖 Hooks 调用顺序关联状态,条件调用会破坏顺序。
ref 和 state 区别? ref 可变但不触发渲染;state 更新会触发渲染,适合驱动 UI。
Context 解决什么问题? 解决跨层级读取共享数据的问题,不等于所有状态都该放全局。

总结

React 常用知识可以用一条主线串起来:组件负责描述 UI,props 传入外部输入,state 保存组件记忆,渲染把输入变成界面,Effect 负责同步外部系统,key 帮 React 识别列表项身份。

写 React 时,先保证组件纯粹、状态结构清晰、数据流单向,再根据测量结果处理性能问题。

面试回答也按这条线展开:定义概念,解释机制,给最小代码,再补充边界场景。

参考资料

相关推荐
用户298698530145 小时前
在 React 中使用 JavaScript 合并 Excel 文件
前端·javascript·react.js
橘子星5 小时前
JavaScript this 指向全解实战指南
前端·javascript
何出无名之师5 小时前
AIDL的一次调用链路追踪之二,如何和驱动打交道
前端
weedsfly5 小时前
JS垃圾回收:从原理到项目实战,彻底根治内存泄漏
前端·javascript·面试
Jcc5 小时前
虚拟 DOM 是什么?从 Snabbdom 理解 Vue 的 DOM 更新机制
前端
user62229864925815 小时前
Vue 常用技术知识全景:从响应式到组件通信的系统理解
前端
feiyu_gao5 小时前
一个人 + AI:246 commits 做出设计系统 CLI 的故事
前端·ai编程·交互设计
奶油mm5 小时前
从 0 到 1 搭建高可用 Redis Cluster:踩坑、优化与生产实践
前端
掘金安东尼5 小时前
Agent Loop 深度调研:把决定权交给模型的一次换代,为什么发生在现在
前端