在 React 应用里,兄弟组件彼此之间并没有直接的通信通道。当左侧列表选中一项,右侧详情需要跟着变化,这种需求非常普遍。解决思路不是让两个兄弟互相调用方法,而是把状态和事件处理函数统一交给它们共同的父组件管理,再通过 props 分发给各自。

一、为什么兄弟组件不能直接通信
React 的数据流是单向的,从父组件通过 props 流向子组件。兄弟组件在组件树中处于同一层级,彼此没有引用关系,也没有共享的私有状态。如果强行在组件 A 里拿到组件 B 的实例去调用方法,就打破了这种单向约定,后续谁改了数据、页面为什么刷新都会变得难以排查。
更合理的做法是把两个兄弟组件都需要的数据,以及修改数据的函数,都提升到它们最近的公共父组件。父组件持有 state,并把 state 值和修改函数分别通过 props 传给 A 和 B。A 触发事件时调用父级传来的函数,父级改完 state,B 自然就收到了新值重新渲染。
二、基础实践:状态提升与事件函数传递
下面用一个简单例子说明。父组件 App 包含 List 和 Detail 两个兄弟组件,List 负责展示可点击的名字,Detail 展示当前选中的人信息。
import React, { useState } from 'react';
function List({ users, onSelect }) {
return (
<ul>
{users.map((u) => (
<li key={u.id} onClick={() => onSelect(u.id)}>
{u.name}
</li>
))}
</ul>
);
}
function Detail({ user }) {
if (!user) return <p>请选择一位用户</p>;
return (
<div>
<h3>{user.name}</h3>
<p>年龄:{user.age}</p>
</div>
);
}
export default function App() {
const users = [
{ id: 1, name: '张三', age: 20 },
{ id: 2, name: '李四', age: 25 }
];
const [selectedId, setSelectedId] = useState(null);
const handleSelect = (id) => {
setSelectedId(id);
};
const currentUser = users.find((u) => u.id === selectedId);
return (
<div>
<List users={users} onSelect={handleSelect} />
<Detail user={currentUser} />
</div>
);
}
在上面的代码中,handleSelect 是定义在 App 里的事件处理函数,通过 onSelect 这个 prop 传递给 List。List 在 <li> 的 onClick 中调用它,把被点击用户的 id 回传。App 用 useState 保存 selectedId,再计算出 currentUser 传给 Detail。这样 List 和 Detail 完全不需要知道对方存在。
这种写法的优点是结构清晰,所有共享状态都在父级,调试时只要看父组件的 state 和传下去的 props。缺点是当组件层级变深,比如 List 外面还包了三层容器,onSelect 就要一层层透传,代码会比较啰嗦。
三、深层嵌套时的优化方案
如果兄弟组件隔得很远,或者中间有很多无关节件,一直用 props 透传处理函数会显得笨重。这时可以用 React 的 Context 来跨层传递。
import React, { createContext, useContext, useState } from 'react';
const SelectContext = createContext(null);
function List({ users }) {
const { onSelect } = useContext(SelectContext);
return (
<ul>
{users.map((u) => (
<li key={u.id} onClick={() => onSelect(u.id)}>
{u.name}
</li>
))}
</ul>
);
}
function Detail() {
const { currentUser } = useContext(SelectContext);
if (!currentUser) return <p>请选择一位用户</p>;
return (
<div>
<h3>{currentUser.name}</h3>
<p>年龄:{currentUser.age}</p>
</div>
);
}
export default function App() {
const users = [
{ id: 1, name: '张三', age: 20 },
{ id: 2, name: '李四', age: 25 }
];
const [selectedId, setSelectedId] = useState(null);
const currentUser = users.find((u) => u.id === selectedId);
return (
<SelectContext.Provider
value={{ onSelect: setSelectedId, currentUser }}
>
<List users={users} />
<Detail />
</SelectContext.Provider>
);
}
这里用 createContext 建立了 SelectContext,在 App 里通过 Provider 把 onSelect 和 currentUser 放进去。List 和 Detail 无论被包在多深的子组件里,都可以直接用 useContext 拿到需要和调用的值,不必再逐层写 props。
使用 Context 时要注意,Provider 的 value 变化会引起所有消费组件重渲染。如果处理函数和状态非常稳定,可以用 useMemo 包裹 value,或者把处理函数单独用 useCallback 固定引用,避免不必要的刷新。对于中小项目,直接提升状态加 props 传递已经足够,不必过早引入 Context。
四、常见误区与建议
一个常见错误是把事件处理函数写在子组件内部去直接改另一个兄弟组件的状态,比如通过 ref 或全局变量。这会使得组件之间产生隐式依赖,单元测试很难写,页面状态也不可预测。始终坚持状态归公共父级、子组件只通过回调通知变更,是更稳妥的模式。
另外,传递事件处理函数时尽量保持函数职责单一。比如 onSelect 只负责把 id 交给父级,不要在里面顺便发请求或改多个 state。父级拿到 id 后再统一协调数据和副作用,逻辑边界清楚,以后接 TypeScript 或做性能优化都更轻松。