Menu

React 面试题及答案(React 19)

约 30 道从入门到进阶的 React 面试题,每道都附有面试官认可的简短答案,那些看比读更容易理解的题目还配有可运行的代码。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

下面是最常见的 React 面试题,按难度分组,并按面试官期望的方式作答:简短、符合 React 19,并说明规则背后的原因。对于看一眼行为比读文字更直观的题目,答案中带有可运行的编辑器,还有几道题会请你在运行前先预测控制台输出。

点击之前先想一想:预览下方的控制台会打印什么?按钮会显示什么?答案在下面的入门部分。

入门问题

React 是什么?

React 是一个用组件构建用户界面的 JavaScript 库。你编写函数,返回当前数据下 UI 的描述;数据变化时,React 会找出变化的部分并替你更新 DOM。它只负责视图层:路由、数据获取和表单由其他库或 Next.js 之类的框架提供。

JSX 是什么?

JSX 是一种语法扩展,让你在 JavaScript 中编写标记。编译器(Babel、esbuild、SWC)会把每个标签转换成创建 React 元素的调用,所以 <p className="note">{text}</p> 会变成一个带有 type: 'p' 和 props 的对象。因为它本质上是 JavaScript,所以要用 className 代替 class,把表达式写在 {} 中,并且每个标签都必须闭合。JSX 页面详细介绍了这些规则。

组件和元素有什么区别?

组件是接收 props 并返回 UI 的函数(或类)。元素是 JSX 生成的普通对象,描述要渲染什么:<Avatar size={40} /> 是一个 type 为 Avatar 函数的元素。React 调用你的组件把元素变成更多元素,一直到 DOM 标签为止。元素创建成本很低,而且不可变;你永远不会自己调用组件。

props 和 state 有什么区别?

props 由父组件传入,在组件内部是只读的。state 归组件自己所有,通过 setter 随时间变化,每次变化都会安排一次新的渲染。常见的说法是:props 用来配置组件,state 是组件的记忆。同一个值可以在父组件中是 state,在子组件中是 prop。

本页第一个示例打印什么?

它打印 logged: 0,按钮显示 Count: 1,而不是 2。state 是一个快照:在整次渲染中 count 都是常量,所以两次 setCount(count + 1) 计算的都是 0 + 1,而日志读到的是旧值,因为新值只存在于下一次渲染中。要加 2,就用更新函数的形式:调用两次 setCount((c) => c + 1)。

列表为什么需要 key?为什么下标不适合做 key?

React 用 key 把列表中的每一项与上一次渲染中的对应项匹配起来。如果用数组下标作为 key,并在顶部插入一项,所有下标都会偏移,React 会把每一行的 state 留在原来的位置上,结果关联到了错误的数据。

勾选 Ada,再点击“Add to top”:勾选跑到了新的第一行。把 key={index} 改成 key={person.id} 再试一次,勾选就会一直跟着 Ada。在开发环境中,缺少 key 时 React 也会发出警告;预览按生产构建运行,所以这里看不到这个警告。

什么是受控输入框?

值来自 React state 的输入框:你传入 value,并在 onChange 中更新 state,这样 React 就是唯一的数据来源,你可以校验或转换每一次按键。非受控输入框在 DOM 中保存自己的值,需要时再通过 ref 或表单的 FormData 读取。当 UI 需要对用户输入做出反应时,默认选择受控输入框。

什么是状态提升?

当两个组件需要同一份数据时,把 state 移到它们最近的共同父组件中,再连同修改它的函数一起作为 props 传下去。子组件不再各自持有副本,所以它们永远不会不一致。在使用 context 或 store 之前,这是“兄弟组件如何共享 state”的标准答案。

hook 的使用规则是什么?

只在组件或自定义 hook 的顶层调用 hook,不要在条件语句、循环或嵌套函数中调用;并且只在 React 函数中调用,不要在普通 JavaScript 函数中调用。React 通过调用顺序来识别每个 hook,所以每次渲染时顺序都必须完全相同。eslint-plugin-react-hooks 包会强制执行这两条规则。

fragment 是什么?

fragment 把多个元素组合在一起,但不添加 DOM 节点:写作 <>...</>,在列表中需要 key 时写作 <Fragment key={id}>。组件必须返回一个根节点,fragment 让这个根节点可以是多个兄弟元素,这在表格和 flex 布局中很重要,因为多出来的 div 会破坏标记结构。

中级问题

useEffect 的依赖数组是怎么工作的?

React 在渲染提交之后(通常在浏览器绘制之后)运行 effect,之后每当数组中的某个值发生变化(用 Object.is 比较)时,在那次渲染之后再次运行。没有数组时,每次渲染之后都会运行;传入 [] 时,只在第一次渲染之后运行一次。effect 读取的组件中的每个值都应该放进数组,漏掉时 linter 会提醒你。

effect 的清理函数什么时候运行?

effect 返回的函数会在 effect 以新的依赖再次运行之前执行,并在组件卸载时执行一次。你要在这里撤销 effect 设置的东西:清除定时器、移除监听器、关闭连接。

点击“Switch room”并观察控制台:先运行旧房间的清理函数,然后 effect 连接到新房间。“Hide chat”只会运行清理函数。在开发环境中,StrictMode 会在挂载时额外执行一次连接、断开、连接,以检查你的清理函数是否有效;预览按生产构建运行,所以你只会看到真实的顺序。

为什么在 setInterval 中读到的 state 是旧的?

interval 回调是一个闭包,捕获的是创建它的那次渲染中的 count。依赖数组为空时 effect 不会再次运行,所以回调永远读取的都是第一次的 count。

broken 到 1 就停了,因为每次 tick 计算的都是 0 + 1。fixed 会一直计数,因为更新函数接收的是最新的值。把 broken 加到依赖数组中也能解决,代价是每秒都要清除并重新创建 interval。

什么是批处理(batching)?

React 会把多次 state 更新合并成一次渲染。从 React 18 开始,这在任何地方都生效:事件处理函数、定时器、promise 和原生事件监听器。如果你需要在某次更新后立即更新 DOM,可以用 react-dom 中的 flushSync 跳过批处理,但你很少需要它。

点击后只打印一行 render 1 1 1,而不是三次渲染。在 React 18 之前,定时器中的三次 state 更新意味着三次渲染。

这段代码打印什么?(effect 的执行顺序)

先读代码,写下控制台会按顺序打印的四行,然后再看结果。

渲染从上往下:先 render App,再 render Child。effect 从下往上执行,并且在整棵树提交之后:先 effect Child,再 effect App。父组件的 effect 可以依赖子组件的 effect 已经运行完毕。

useMemo 和 useCallback 有什么区别?

useMemo(() => compute(a, b), [a, b]) 缓存计算的结果。useCallback(fn, [deps]) 缓存函数本身,所以它等同于 useMemo(() => fn, [deps])。只有当下游有东西按引用比较时,两者才有用:一个 memo 子组件,或者另一个 hook 的依赖数组。自 React Compiler 1.0(2025 年 10 月)起,构建步骤可以自动加上这些记忆化,但手动记忆化依然有效,你在大多数代码库中仍会看到它。

React.memo 有什么作用?为什么它常常需要 useCallback?

memo(Component) 会在 props 与上次相同时跳过组件的重新渲染,props 用 Object.is 逐个比较。在父组件中内联写的函数每次渲染都是一个新函数,所以它算作变化了的 prop,让 memo 失效。useCallback 能在多次渲染之间保持同一个函数。

清空控制台并点击“Re-render parent”:只出现 render inline handler。另一个子组件拿到的是同一个函数,所以被跳过了。去掉 Button 外面的 memo(...),每次点击两个都会打印,这说明单独使用 useCallback 没有任何作用。

context 是什么?它有什么代价?

context 把一个值传给 provider 下方的所有组件,不需要层层传递 props:先 createContext,然后在 React 19 中写 <ThemeContext value={theme}>(或 <ThemeContext.Provider>),再用 useContext(ThemeContext) 读取。代价是值变化时,每个读取这个 context 的组件都会重新渲染,memo 也拦不住。要控制开销,可以把不相关的值拆到不同的 context 中,用 useMemo 包裹对象类型的值,并且不要把频繁变化的 state 放进全局 context。参见 useContext。

ref 是什么?什么时候用 ref 而不是 state?

useRef(initial) 返回一个对象,它的 current 属性在多次渲染之间保持不变,修改它也不会触发渲染。页面不依赖的值(定时器 id、上一次的值、标志位)以及 DOM 访问(<input ref={inputRef}>,然后 inputRef.current.focus())用 ref。如果这个值要显示在页面上,就应该放在 state 中。

什么是虚拟 DOM?什么是协调?

每次渲染时 React 都会构建一棵新的元素树,并与上一棵比较;这个比较过程就是协调(reconciliation)。如果某个元素的类型变了(div 变成了 section,或 ProfileA 变成了 ProfileB),React 会销毁这棵子树及其 state,再构建新的。如果类型相同,它会保留 DOM 节点,只更新变化了的 props,并用 key 来匹配列表中的子元素。“虚拟 DOM”是这棵内存中的树的通俗叫法;速度来自只修改变化的部分,而不是这棵树本身有多快。

什么是自定义 hook?

名字以 use 开头并调用其他 hook 的函数,让你在组件之间复用有状态的逻辑:useOnlineStatus、useLocalStorage、useDebouncedValue。每个调用它的组件都拥有自己独立的 state;自定义 hook 共享的是逻辑,不是数据。参见自定义 hook。

StrictMode 有什么作用?

<StrictMode> 只在开发环境中开启额外的检查。它会把组件渲染两次以暴露不纯的渲染,在挂载时执行一次 effect 的设置、清理、设置以暴露缺失的清理函数,对 ref 回调也做同样的事,并对已弃用的 API 发出警告。这些在生产构建中都不会发生,所以开发环境中控制台打印两次是正常现象,不是 bug。

高级问题

这段代码打印什么?(render、layout effect、effect)

再来一道预测题。三条日志,对应一次渲染中的三个不同时刻。

顺序是渲染、useLayoutEffect、useEffect,尽管 useEffect 写在前面。layout effect 在 React 更新 DOM 之后、浏览器绘制之前运行,所以适合测量元素;普通 effect 运行得更晚,通常在绘制之后,应该作为默认选择。

什么是错误边界?

错误边界是一个类组件,它捕获子组件渲染时抛出的错误,并显示后备内容,而不是卸载整个应用。它需要 static getDerivedStateFromError(切换到后备内容),通常还需要 componentDidCatch(记录日志)。至今仍没有对应的 hook,所以在以函数组件为主的代码库中,团队会写一个类组件,或者使用 react-error-boundary 包。

点击“Break it”:后备内容只替换了边界内的子组件,按钮还在,React 仍会在控制台中报告被捕获的错误。错误边界捕获不到事件处理函数中的错误、setTimeout 这类异步代码中的错误,以及边界组件自身的错误。这些情况要用 try/catch 再设置 state。

什么是 portal?

react-dom 中的 createPortal(children, domNode) 把子组件渲染到另一个 DOM 节点中,通常是 document.body,同时在 React 树中保持原来的位置。模态框、提示框和 toast 用它来摆脱父元素的 overflow: hidden 或层叠上下文。context 在 portal 中依然有效,事件也会沿 React 树冒泡,所以即使 DOM 节点在别处,portal 内的点击也会到达父组件的 onClick。参见 portals。

Suspense 和 lazy 是什么?

lazy(() => import('./Chart')) 在组件第一次渲染时才加载它的代码,从而拆分你的 bundle。<Suspense fallback={<Spinner />}> 在其内部任何东西仍在加载时显示 fallback:可以是懒加载的代码,也可以是用 use(promise) 或支持 Suspense 的框架读取的数据。边界放在哪里,决定了页面的哪些部分一起等待。

import { lazy, Suspense } from 'react';

const Chart = lazy(() => import('./Chart.jsx'));

export default function Dashboard() {
    return (
        <Suspense fallback={<p>Loading chart...</p>}>
            <Chart />
        </Suspense>
    );
}

useTransition 有什么作用?

它把一次 state 更新标记为非紧急。const [isPending, startTransition] = useTransition() 配合 startTransition(() => setTab('posts')),让 React 保持当前页面的响应,并在更紧急的更新(比如一次按键)到来时中断缓慢的渲染。isPending 告诉你该显示一个提示。在 React 19 中,传入的函数可以是异步的,React 称之为 action。不要用它更新文本输入框自身的值;那个更新必须是紧急的。

什么是 React Server Components?

只在服务器上(或在构建时)运行、把渲染结果而不是代码发送到浏览器的组件。它们可以是 async 的,可以直接读取数据库或文件系统,但不能使用 state、effect 或浏览器 API。用 'use client' 标记的文件开启一个客户端边界,交互组件放在里面;'use server' 标记客户端代码可以调用的服务器函数。你需要通过 Next.js App Router 这样的框架来使用它们;普通的 Vite 应用没有运行它们的服务器。

React 19 有哪些变化?

  • Actions:transition 中的异步函数,等待状态由 React 替你处理。<form action={fn}> 会用表单的 FormData 调用 fn。
  • 新的 hook:useActionState 用于表单的结果和等待状态,useOptimistic 用于乐观更新 UI,react-dom 中的 useFormStatus。
  • use:读取 promise(在 resolve 之前挂起)或 context,与其他 hook 不同,它可以在条件语句中调用。
  • ref 作为 prop:函数组件像接收其他 prop 一样接收 ref,所以新代码不再需要 forwardRef。ref 回调可以返回清理函数。
  • <Context> 直接作为 provider,以及会被提升到文档 head 中的 <title> 和 <meta> 标签。
  • 移除:ReactDOM.render、字符串 ref、旧版 context,以及函数组件上的 defaultProps(改用参数默认值)。
// React 19: ref is a normal prop
function TextInput({ ref, ...props }) {
    return <input ref={ref} {...props} />;
}

React 19 页面为每一项都提供了可运行示例。

如何测试 React 组件?

使用 React Testing Library 和 Vitest 或 Jest 这样的测试运行器。渲染组件,像用户那样查找元素(按角色、标签或文本),用 user-event 进行交互,然后断言页面上可见的内容,而不是内部 state。这样测试在重构后依然有效。

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

test('counts clicks', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    await user.click(screen.getByRole('button', { name: /clicked 0 times/i }));
    expect(screen.getByRole('button')).toHaveTextContent('Clicked 1 times');
});

toHaveTextContent 来自 @testing-library/jest-dom。对于跨页面的完整流程,团队会再加上 Playwright 或 Cypress。

如何发现并修复性能问题?

先测量。打开 React DevTools 的 Profiler 标签页,录制那次缓慢的交互,然后阅读火焰图:它显示哪些组件渲染了、各花了多长时间,以及(开启“record why each component rendered”设置时)为什么渲染。常见的修复方法,按尝试顺序排列:把 state 往下移,让依赖它的组件更少;把缓慢的子树作为 children 传入;用 memo、useMemo 和 useCallback 记忆化开销大的部分;对长列表做虚拟化;用 useTransition 或 useDeferredValue 推迟繁重的更新。要在生产构建或 profiling 构建中分析;开发构建更慢,会夸大渲染时间。

组件为什么应该是纯的?

在 props、state 和 context 相同时,组件应该返回相同的 JSX,并且在渲染时不修改任何外部的东西。React 可能会把一个组件渲染多次、打乱顺序,或者丢弃某次渲染的结果(transition、Suspense、StrictMode),所以写在组件主体中的副作用,比如修改变量或发送请求,执行次数无法预测。副作用首先应该放在事件处理函数中,不是由事件引起的才放在 effect 中。

什么时候不应该使用 effect?

当值可以在渲染时计算出来(过滤后的列表、全名、合计)时;当工作是由用户操作引起的(放到事件处理函数中)时;当你想在 prop 变化时重置 state(用 key)时。effect 用于与 React 之外的东西同步:网络连接、浏览器 API、第三方组件。一连串设置 state 的 effect 是多余渲染和 bug 的常见来源。

常见问题

React 是什么?

React 是一个用组件构建用户界面的 JavaScript 库。你描述当前数据下页面应该是什么样子,数据变化时由 React 更新真实的 DOM。

state 和 props 有什么区别?

props 是父组件传给组件的输入,组件把它们当作只读的。state 是组件自己拥有、通过 setter 随时间改变的数据,每次改变都会让 React 重新渲染这个组件。

React 中的 hooks 是什么?

hooks 是名字以 use 开头的函数,让函数组件可以使用 React 的功能:useState 用于 state,useEffect 用于副作用,useContext 用于 context,useRef 用于不触发渲染的值,等等。

虚拟 DOM 是什么?

它是 React 在内存中对 UI 的描述的通称。每次渲染时 React 都会生成一份新的描述,与上一份进行比较,只把差异应用到真实 DOM 上。这个比较过程叫作协调(reconciliation)。

为什么 React 列表中的 key 很重要?

key 告诉 React 在多次渲染之间列表中的哪一项对应哪一项。使用稳定的 key(比如 id)时,即使列表项被添加、删除或重新排序,React 也能让每一项的 state 和 DOM 节点跟着正确的数据走。用数组下标作为 key,在顺序变化时就会出错。

JSX 是什么?

JSX 是一种语法扩展,让你在 JavaScript 中写类似 HTML 的标记。编译器会把每个标签转换成创建 React 元素的函数调用,所以 <h1>Hi</h1> 会变成一个描述 h1 的普通对象。

Coddy 编程语言插图

和 Coddy 一起学编程

开始