跳转到内容

React

标签「React」下的 1 篇文章

React 父子组件通信面试题复盘

这是我曾经遇到过的一道 React 面试题,问题很常见:

React 父子组件之间如何通信?

这个问题看起来很基础,但其实很适合继续追问。因为它背后不只是 props 怎么传,而是 React 的数据流、状态放在哪里、组件边界怎么设计、什么时候用 Context、什么时候才需要状态管理库。

如果只是回答“父传子用 props,子传父用回调函数”,当然没错,但面试里还不够。更好的回答应该能顺着这个问题,把 React 的单向数据流讲清楚。

React 默认是单向数据流。

父组件可以通过 props 把数据传给子组件;子组件不能直接修改父组件的数据,如果子组件需要影响父组件,就由父组件传一个函数给子组件,子组件调用这个函数,把变化通知给父组件。

可以先用一句话概括:

父传子用 props,子传父用回调函数;如果兄弟组件之间要通信,就把状态提升到它们共同的父组件。

这是这道题的核心答案。

父传子最简单,就是把数据作为 props 传给子组件。

Parent.tsx
type User = {
id: number;
name: string;
};
function Parent() {
const user: User = {
id: 1,
name: 'xiaoxi',
};
return <UserCard user={user} />;
}
type UserCardProps = {
user: User;
};
function UserCard({ user }: UserCardProps) {
return (
<section>
<h2>{user.name}</h2>
<p>ID: {user.id}</p>
</section>
);
}

这里的数据方向很清楚:

Parent state/data -> props -> Child render

子组件只负责接收和展示,不负责决定这个数据从哪里来。

子组件不能直接修改父组件内部的 state。如果子组件需要触发父组件更新,就由父组件把更新函数传下去。

Counter.tsx
import { useState } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const handleAdd = () => {
setCount((value) => value + 1);
};
return (
<div>
<p>当前数量:{count}</p>
<CounterButton onAdd={handleAdd} />
</div>
);
}
type CounterButtonProps = {
onAdd: () => void;
};
function CounterButton({ onAdd }: CounterButtonProps) {
return <button onClick={onAdd}>加一</button>;
}

这个过程可以理解为:

Child click -> call props function -> Parent setState -> Child receives new props

也就是说,子组件并没有“改父组件”,它只是触发了父组件提供的回调。

有时候子组件不只是触发动作,还需要把自己的数据交给父组件。

比如搜索框输入内容,父组件需要拿到关键词:

SearchBox.tsx
import { useState } from 'react';
function Parent() {
const [keyword, setKeyword] = useState('');
return (
<div>
<SearchBox onSearch={setKeyword} />
<p>当前搜索词:{keyword}</p>
</div>
);
}
type SearchBoxProps = {
onSearch: (keyword: string) => void;
};
function SearchBox({ onSearch }: SearchBoxProps) {
const [value, setValue] = useState('');
const handleSubmit = () => {
onSearch(value);
};
return (
<div>
<input value={value} onChange={(event) => setValue(event.target.value)} />
<button onClick={handleSubmit}>搜索</button>
</div>
);
}

这里的 onSearch(value) 就是典型的子传父。

面试时可以强调:React 里子传父不是反向修改数据,而是调用父组件传入的回调。

如果两个兄弟组件需要共享数据,通常不是让它们互相调用,而是把状态提升到共同父组件。

StateLifting.tsx
import { useState } from 'react';
function Parent() {
const [selectedId, setSelectedId] = useState<number | null>(null);
return (
<div>
<ProductList onSelect={setSelectedId} />
<ProductDetail productId={selectedId} />
</div>
);
}
type ProductListProps = {
onSelect: (id: number) => void;
};
function ProductList({ onSelect }: ProductListProps) {
return (
<ul>
<li onClick={() => onSelect(1)}>商品 1</li>
<li onClick={() => onSelect(2)}>商品 2</li>
</ul>
);
}
type ProductDetailProps = {
productId: number | null;
};
function ProductDetail({ productId }: ProductDetailProps) {
if (productId === null) return <p>请选择一个商品</p>;
return <p>当前商品 ID:{productId}</p>;
}

这就是 React 组件设计里很重要的一点:状态应该放在需要它的最小公共父组件中。

如果状态只属于一个组件,就放在这个组件内部;如果多个子组件都要用,就提升到公共父组件;如果很多层都要用,再考虑 Context 或状态管理库。

表单场景里,受控组件其实也是父子通信的一种体现。

父组件控制输入框的值,子组件只负责展示和触发修改。

ControlledInput.tsx
import { useState } from 'react';
function Parent() {
const [email, setEmail] = useState('');
return <EmailInput value={email} onChange={setEmail} />;
}
type EmailInputProps = {
value: string;
onChange: (value: string) => void;
};
function EmailInput({ value, onChange }: EmailInputProps) {
return (
<input
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder="请输入邮箱"
/>
);
}

这里的核心是:

  • value 从父组件传给子组件。
  • onChange 从父组件传给子组件。
  • 子组件触发 onChange,父组件更新状态。
  • 新状态再通过 value 传回来。

所以受控组件不是“输入框自己管理值”,而是父组件管理值。

如果只是父子组件通信,不需要上来就用 Context。

但如果数据需要跨很多层传递,比如主题、登录用户、语言配置,就可以考虑 Context。

ThemeContext.tsx
import { createContext, useContext } from 'react';
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Layout />
</ThemeContext.Provider>
);
}
function Layout() {
return <Toolbar />;
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <button className={theme}>保存</button>;
}

Context 解决的是 props drilling,也就是一层层传 props 的问题。

但是它不应该被滥用。不是所有父子通信都需要 Context。对于很近的父子组件,直接用 props 更清楚。

有些场景不是传数据,而是父组件需要调用子组件暴露出来的方法。

比如父组件点击按钮,让子组件内部的输入框聚焦。

这时可以使用 forwardRefuseImperativeHandle

FocusInput.tsx
import { forwardRef, useImperativeHandle, useRef } from 'react';
type FocusInputRef = {
focus: () => void;
};
const FocusInput = forwardRef<FocusInputRef>(function FocusInput(_, ref) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
}));
return <input ref={inputRef} placeholder="请输入内容" />;
});
function Parent() {
const inputRef = useRef<FocusInputRef>(null);
return (
<div>
<FocusInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>聚焦输入框</button>
</div>
);
}

不过这种方式要谨慎使用。

React 更推荐声明式的数据流。ref 更适合处理 DOM、聚焦、滚动、播放控制这类命令式场景,不适合作为普通业务数据通信的首选方案。

性能相关:回调函数会不会导致重复渲染

Section titled “性能相关:回调函数会不会导致重复渲染”

面试官可能继续问:父组件每次渲染都会创建新的回调函数,会不会导致子组件重复渲染?

答案是:可能会,但要结合场景看。

如果子组件使用了 memo,并且传入的回调函数每次都是新引用,子组件可能仍然会重新渲染。这时可以使用 useCallback 稳定函数引用。

MemoCallback.tsx
import { memo, useCallback, useState } from 'react';
const Child = memo(function Child({ onAdd }: { onAdd: () => void }) {
return <button onClick={onAdd}>加一</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const handleAdd = useCallback(() => {
setCount((value) => value + 1);
}, []);
return (
<div>
<p>{count}</p>
<Child onAdd={handleAdd} />
</div>
);
}

但不要为了“看起来专业”到处写 useCallback。如果子组件没有性能问题,或者没有使用 memo,盲目使用 useCallback 反而会增加理解成本。

父子通信、兄弟通信、跨层级通信,本质上都是状态在哪里的问题。

可以按下面的顺序判断:

场景推荐方式
父组件给子组件数据props
子组件通知父组件回调函数
兄弟组件共享状态状态提升
多层组件共享稳定数据Context
大量页面共享复杂业务状态Zustand、Redux、Jotai 等状态管理库

状态管理库不是为了替代 props,而是为了解决更大范围、更复杂的数据共享和更新问题。

比如用户信息、购物车、权限、全局弹窗、复杂筛选条件,这类状态可能会跨多个页面或模块使用,就可以考虑状态管理库。

这道题可以这样回答:

React 是单向数据流。父组件向子组件传值用 props;子组件想影响父组件时,父组件传回调函数给子组件,子组件调用回调并把数据传回去。如果兄弟组件需要共享状态,就把状态提升到共同父组件。如果跨层级传递太深,可以用 Context;如果是跨页面、跨模块的复杂共享状态,再考虑 Redux、Zustand 这类状态管理库。特殊情况下,父组件需要调用子组件内部方法,可以用 refforwardRefuseImperativeHandle,但这更适合聚焦、滚动这类命令式场景,不是普通数据通信的首选。

如果面试官继续追问,可以展开这些点:

  • React 为什么强调单向数据流?
  • 状态提升解决什么问题?
  • Context 和状态管理库有什么区别?
  • 受控组件为什么也是父子通信?
  • ref 能不能替代 props?
  • useCallback 是否一定能优化性能?

React 父子通信这道题看似基础,但它可以引申到很多 React 核心思想:

  • 数据从父组件流向子组件。
  • 子组件通过回调通知父组件。
  • 多个组件共享状态时,优先状态提升。
  • 跨层级共享再考虑 Context。
  • 复杂全局状态再考虑状态管理库。
  • ref 是命令式能力,不是普通数据流的替代品。

所以这道题最重要的不是记住几个 API,而是理解 React 组件之间的数据边界:谁拥有状态,谁负责修改状态,谁只是接收状态并渲染。