React 父子组件通信面试题复盘
这是我曾经遇到过的一道 React 面试题,问题很常见:
React 父子组件之间如何通信?
这个问题看起来很基础,但其实很适合继续追问。因为它背后不只是 props 怎么传,而是 React 的数据流、状态放在哪里、组件边界怎么设计、什么时候用 Context、什么时候才需要状态管理库。
如果只是回答“父传子用 props,子传父用回调函数”,当然没错,但面试里还不够。更好的回答应该能顺着这个问题,把 React 的单向数据流讲清楚。
最直接的回答
Section titled “最直接的回答”React 默认是单向数据流。
父组件可以通过 props 把数据传给子组件;子组件不能直接修改父组件的数据,如果子组件需要影响父组件,就由父组件传一个函数给子组件,子组件调用这个函数,把变化通知给父组件。
可以先用一句话概括:
父传子用
props,子传父用回调函数;如果兄弟组件之间要通信,就把状态提升到它们共同的父组件。
这是这道题的核心答案。
父组件向子组件传值
Section titled “父组件向子组件传值”父传子最简单,就是把数据作为 props 传给子组件。
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子组件只负责接收和展示,不负责决定这个数据从哪里来。
子组件通知父组件
Section titled “子组件通知父组件”子组件不能直接修改父组件内部的 state。如果子组件需要触发父组件更新,就由父组件把更新函数传下去。
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也就是说,子组件并没有“改父组件”,它只是触发了父组件提供的回调。
子组件把数据传给父组件
Section titled “子组件把数据传给父组件”有时候子组件不只是触发动作,还需要把自己的数据交给父组件。
比如搜索框输入内容,父组件需要拿到关键词:
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 里子传父不是反向修改数据,而是调用父组件传入的回调。
如果两个兄弟组件需要共享数据,通常不是让它们互相调用,而是把状态提升到共同父组件。
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 或状态管理库。
受控组件也是父子通信
Section titled “受控组件也是父子通信”表单场景里,受控组件其实也是父子通信的一种体现。
父组件控制输入框的值,子组件只负责展示和触发修改。
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
Section titled “跨层级通信:Context”如果只是父子组件通信,不需要上来就用 Context。
但如果数据需要跨很多层传递,比如主题、登录用户、语言配置,就可以考虑 Context。
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 更清楚。
ref 和命令式调用
Section titled “ref 和命令式调用”有些场景不是传数据,而是父组件需要调用子组件暴露出来的方法。
比如父组件点击按钮,让子组件内部的输入框聚焦。
这时可以使用 forwardRef 和 useImperativeHandle。
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 稳定函数引用。
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 反而会增加理解成本。
什么时候用状态管理库
Section titled “什么时候用状态管理库”父子通信、兄弟通信、跨层级通信,本质上都是状态在哪里的问题。
可以按下面的顺序判断:
| 场景 | 推荐方式 |
|---|---|
| 父组件给子组件数据 | props |
| 子组件通知父组件 | 回调函数 |
| 兄弟组件共享状态 | 状态提升 |
| 多层组件共享稳定数据 | Context |
| 大量页面共享复杂业务状态 | Zustand、Redux、Jotai 等状态管理库 |
状态管理库不是为了替代 props,而是为了解决更大范围、更复杂的数据共享和更新问题。
比如用户信息、购物车、权限、全局弹窗、复杂筛选条件,这类状态可能会跨多个页面或模块使用,就可以考虑状态管理库。
面试时怎么回答
Section titled “面试时怎么回答”这道题可以这样回答:
React 是单向数据流。父组件向子组件传值用
props;子组件想影响父组件时,父组件传回调函数给子组件,子组件调用回调并把数据传回去。如果兄弟组件需要共享状态,就把状态提升到共同父组件。如果跨层级传递太深,可以用 Context;如果是跨页面、跨模块的复杂共享状态,再考虑 Redux、Zustand 这类状态管理库。特殊情况下,父组件需要调用子组件内部方法,可以用ref、forwardRef和useImperativeHandle,但这更适合聚焦、滚动这类命令式场景,不是普通数据通信的首选。
如果面试官继续追问,可以展开这些点:
- React 为什么强调单向数据流?
- 状态提升解决什么问题?
- Context 和状态管理库有什么区别?
- 受控组件为什么也是父子通信?
ref能不能替代 props?useCallback是否一定能优化性能?
React 父子通信这道题看似基础,但它可以引申到很多 React 核心思想:
- 数据从父组件流向子组件。
- 子组件通过回调通知父组件。
- 多个组件共享状态时,优先状态提升。
- 跨层级共享再考虑 Context。
- 复杂全局状态再考虑状态管理库。
ref是命令式能力,不是普通数据流的替代品。
所以这道题最重要的不是记住几个 API,而是理解 React 组件之间的数据边界:谁拥有状态,谁负责修改状态,谁只是接收状态并渲染。