React 组件间通信

React组件间通信是构建复杂应用的关键。了解不同通信方式的适用场景,可以帮助你设计更灵活、可维护的组件结构。

组件通信模式概览

父组件
子组件A
子组件B
箭头表示数据流向:↔ 双向通信,→ 单向数据流
通信方式 适用场景 复杂度 优点 缺点
Props(父传子) 父子组件简单数据传递 ★☆☆☆☆ 简单直观,React核心特性 只能单向传递
回调函数(子传父) 子组件通知父组件 ★★☆☆☆ 实现简单,符合单向数据流 回调地狱(多层嵌套)
状态提升 兄弟组件间通信 ★★★☆☆ 避免引入额外状态管理 可能导致父组件臃肿
Context API 跨层级组件通信 ★★★★☆ 避免props drilling 可能影响性能,适合低频更新
事件总线/发布订阅 任意组件间通信 ★★★★☆ 完全解耦,灵活 类型不安全,难以调试
状态管理库(Redux) 大型应用全局状态 ★★★★★ 可预测的状态管理 学习成本高,模板代码多

1. Props - 父组件向子组件传递数据

// 父组件
function ParentComponent() {
  const user = {
    name: '张三',
    age: 25,
    email: 'zhangsan@example.com'
  };

  const items = ['项目1', '项目2', '项目3'];

  return (
    <div>
      <h2>父组件</h2>
      {/* 传递单个数据 */}
      <ChildComponent1 name="李四" />

      {/* 传递对象 */}
      <ChildComponent2 user={user} />

      {/* 传递数组 */}
      <ChildComponent3 items={items} />

      {/* 传递函数 */}
      <ChildComponent4 onAction={() => console.log('父组件收到事件')} />
    </div>
  );
}

// 子组件1 - 接收基本类型props
function ChildComponent1(props) {
  return <div>用户名: {props.name}</div>;
}

// 子组件2 - 接收对象props
function ChildComponent2({ user }) { // 使用解构语法
  return (
    <div>
      <p>姓名: {user.name}</p>
      <p>年龄: {user.age}</p>
      <p>邮箱: {user.email}</p>
    </div>
  );
}

// 子组件3 - 接收数组props
function ChildComponent3(props) {
  return (
    <ul>
      {props.items.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

// 子组件4 - 接收函数props
function ChildComponent4({ onAction }) {
  return (
    <button onClick={onAction}>
      触发父组件函数
    </button>
  );
}
Props特性:
  • Props是只读的(不可直接修改)
  • 支持类型检查(使用PropTypes或TypeScript)
  • 可以设置默认值(defaultProps)
  • 可以传递任何JavaScript值(包括函数和JSX)

Props类型检查与默认值

import PropTypes from 'prop-types';

function UserProfile({ user, isOnline, onFollow, children }) {
  return (
    <div className={`user-profile ${isOnline ? 'online' : 'offline'}`}>
      <h3>{user.name}</h3>
      <p>年龄: {user.age}</p>
      <p>状态: {isOnline ? '在线' : '离线'}</p>
      <button onClick={onFollow}>关注</button>
      {children}
    </div>
  );
}

// 定义props类型检查
UserProfile.propTypes = {
  user: PropTypes.shape({
    name: PropTypes.string.isRequired,
    age: PropTypes.number
  }).isRequired,
  isOnline: PropTypes.bool,
  onFollow: PropTypes.func,
  children: PropTypes.node
};

// 设置默认值
UserProfile.defaultProps = {
  isOnline: false,
  onFollow: () => console.log('关注函数未提供')
};

// 使用组件
function App() {
  const user = { name: '王五', age: 30 };

  return (
    <UserProfile user={user} isOnline={true}>
      <p>这是children内容</p>
    </UserProfile>
  );
}

2. 回调函数 - 子组件向父组件通信

// 父组件
function CounterParent() {
  const [count, setCount] = useState(0);

  // 回调函数,接收子组件传递的数据
  const handleIncrement = (incrementValue) => {
    setCount(prevCount => prevCount + incrementValue);
  };

  const handleReset = () => {
    setCount(0);
  };

  return (
    <div>
      <h2>父组件计数器: {count}</h2>
      {/* 将回调函数传递给子组件 */}
      <CounterChild
        onIncrement={handleIncrement}
        onReset={handleReset}
      />
    </div>
  );
}

// 子组件
function CounterChild({ onIncrement, onReset }) {
  const [step, setStep] = useState(1);

  return (
    <div>
      <h3>子组件控制器</h3>
      <div>
        <label>步长: </label>
        <input
          type="number"
          value={step}
          onChange={(e) => setStep(parseInt(e.target.value) || 1)}
        />
      </div>
      <div>
        {/* 调用父组件传递的回调函数 */}
        <button onClick={() => onIncrement(step)}>
          增加 {step}
        </button>
        <button onClick={() => onIncrement(-step)}>
          减少 {step}
        </button>
        <button onClick={onReset}>重置</button>
      </div>
    </div>
  );
}

父子组件通信演示

父组件

计数: 0

子组件

3. 状态提升 - 兄弟组件间通信

当多个组件需要共享同一状态时,将状态提升到它们最近的共同父组件中。

// 父组件 - 持有共享状态
function TemperatureCalculator() {
  const [temperature, setTemperature] = useState('');
  const [scale, setScale] = useState('c');

  // 将状态和更新函数传递给子组件
  const handleCelsiusChange = (temp) => {
    setTemperature(temp);
    setScale('c');
  };

  const handleFahrenheitChange = (temp) => {
    setTemperature(temp);
    setScale('f');
  };

  // 温度转换函数
  const toCelsius = (fahrenheit) => {
    return (fahrenheit - 32) * 5 / 9;
  };

  const toFahrenheit = (celsius) => {
    return (celsius * 9 / 5) + 32;
  };

  // 根据当前单位和温度计算另一单位的温度
  const celsius = scale === 'f' ? toCelsius(parseFloat(temperature) || 0) : parseFloat(temperature) || 0;
  const fahrenheit = scale === 'c' ? toFahrenheit(parseFloat(temperature) || 0) : parseFloat(temperature) || 0;

  return (
    <div>
      <h2>温度转换器</h2>
      <TemperatureInput
        scale="c"
        temperature={celsius.toString()}
        onTemperatureChange={handleCelsiusChange}
      />
      <TemperatureInput
        scale="f"
        temperature={fahrenheit.toString()}
        onTemperatureChange={handleFahrenheitChange}
      />
      <BoilingVerdict celsius={celsius} />
    </div>
  );
}

// 子组件1 - 摄氏温度输入
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
  const scaleNames = {
    c: '摄氏温度',
    f: '华氏温度'
  };

  return (
    <fieldset>
      <legend>请输入{scaleNames[scale]}:</legend>
      <input
        value={temperature}
        onChange={(e) => onTemperatureChange(e.target.value)}
      />
    </fieldset>
  );
}

// 子组件2 - 显示结果
function BoilingVerdict({ celsius }) {
  if (celsius >= 100) {
    return <p>水会沸腾</p>;
  }
  return <p>水不会沸腾</p>;
}

4. Context API - 跨层级组件通信

// 创建Context
import React, { createContext, useState, useContext } from 'react';

// 1. 创建Context
const UserContext = createContext();
const ThemeContext = createContext();

// 2. 创建Provider组件
function AppProviders({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  const login = (userData) => {
    setUser(userData);
  };

  const logout = () => {
    setUser(null);
  };

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <UserContext.Provider value={{ user, login, logout }}>
      <ThemeContext.Provider value={{ theme, toggleTheme }}>
        {children}
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

// 3. 创建自定义Hook简化使用
function useUser() {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('useUser必须在UserProvider内使用');
  }
  return context;
}

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme必须在ThemeProvider内使用');
  }
  return context;
}

// 4. 在任意层级的组件中使用
// 深层子组件 - 不需要通过props层层传递
function DeepChildComponent() {
  const { user } = useUser();
  const { theme, toggleTheme } = useTheme();

  return (
    <div className={`deep-child ${theme}`}>
      {user ? (
        <div>
          <p>欢迎, {user.name}!</p>
          <p>邮箱: {user.email}</p>
        </div>
      ) : (
        <p>请先登录</p>
      )}
      <button onClick={toggleTheme}>
        切换主题 (当前: {theme})
      </button>
    </div>
  );
}

// 5. 在应用中使用
function App() {
  return (
    <AppProviders>
      <Header />
      <MainContent />
      <DeepChildComponent />
      <Footer />
    </AppProviders>
  );
}
Context使用注意事项:
  • Context的value变化会导致所有消费组件重新渲染
  • 合理拆分Context,避免单个Context过大
  • 对于频繁更新的数据,考虑使用状态管理库
  • 使用memo或useMemo优化性能敏感组件

5. 事件总线/发布订阅模式

// 事件总线实现
class EventBus {
  constructor() {
    this.events = new Map();
  }

  // 订阅事件
  on(eventName, callback) {
    if (!this.events.has(eventName)) {
      this.events.set(eventName, []);
    }
    this.events.get(eventName).push(callback);

    // 返回取消订阅函数
    return () => {
      this.off(eventName, callback);
    };
  }

  // 取消订阅
  off(eventName, callback) {
    if (!this.events.has(eventName)) return;

    const callbacks = this.events.get(eventName);
    const index = callbacks.indexOf(callback);
    if (index > -1) {
      callbacks.splice(index, 1);
    }
  }

  // 发布事件
  emit(eventName, data) {
    if (!this.events.has(eventName)) return;

    const callbacks = this.events.get(eventName);
    // 使用slice创建副本,防止在回调中取消订阅导致遍历问题
    callbacks.slice().forEach(callback => {
      try {
        callback(data);
      } catch (error) {
        console.error(`事件 ${eventName} 处理错误:`, error);
      }
    });
  }

  // 一次性订阅
  once(eventName, callback) {
    const onceCallback = (data) => {
      callback(data);
      this.off(eventName, onceCallback);
    };
    this.on(eventName, onceCallback);
  }
}

// 创建全局事件总线实例
const eventBus = new EventBus();

// 组件A - 发布事件
function ComponentA() {
  const [message, setMessage] = useState('');

  const sendMessage = () => {
    eventBus.emit('message', {
      from: 'ComponentA',
      text: message,
      timestamp: new Date().toISOString()
    });
    setMessage('');
  };

  return (
    <div>
      <h3>组件A (发布者)</h3>
      <input
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        placeholder="输入消息"
      />
      <button onClick={sendMessage}>发送消息</button>
    </div>
  );
}

// 组件B - 订阅事件
function ComponentB() {
  const [receivedMessages, setReceivedMessages] = useState([]);

  useEffect(() => {
    // 订阅消息事件
    const unsubscribe = eventBus.on('message', (data) => {
      console.log('ComponentB收到消息:', data);
      setReceivedMessages(prev => [...prev, data]);
    });

    // 组件卸载时取消订阅
    return unsubscribe;
  }, []);

  return (
    <div>
      <h3>组件B (订阅者)</h3>
      <div>收到消息数: {receivedMessages.length}</div>
      <ul>
        {receivedMessages.map((msg, index) => (
          <li key={index}>
            [{new Date(msg.timestamp).toLocaleTimeString()}]
            {msg.from}: {msg.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

// 组件C - 也订阅事件
function ComponentC() {
  const [lastMessage, setLastMessage] = useState(null);

  useEffect(() => {
    const unsubscribe = eventBus.on('message', (data) => {
      console.log('ComponentC收到消息:', data);
      setLastMessage(data);
    });

    return unsubscribe;
  }, []);

  return (
    <div>
      <h3>组件C (订阅者)</h3>
      {lastMessage && (
        <div>
          <p>最新消息:</p>
          <p>来源: {lastMessage.from}</p>
          <p>内容: {lastMessage.text}</p>
        </div>
      )}
    </div>
  );
}

// 在应用中使用
function EventBusApp() {
  return (
    <div>
      <ComponentA />
      <ComponentB />
      <ComponentC />
    </div>
  );
}

6. 使用自定义Hook进行组件通信

// 创建共享状态的自定义Hook
function useSharedState(initialValue) {
  const [state, setState] = useState(initialValue);

  // 创建ref来存储回调函数
  const callbacksRef = useRef(new Set());

  // 更新状态并通知所有订阅者
  const updateState = useCallback((newValue) => {
    setState(newValue);
    // 调用所有订阅的回调函数
    callbacksRef.current.forEach(callback => {
      callback(newValue);
    });
  }, []);

  // 订阅状态变化
  const subscribe = useCallback((callback) => {
    callbacksRef.current.add(callback);

    // 返回取消订阅函数
    return () => {
      callbacksRef.current.delete(callback);
    };
  }, []);

  return { state, updateState, subscribe };
}

// 使用自定义Hook的组件A
function ComponentWithHookA() {
  const { state: sharedValue, updateState } = useSharedState('初始值');
  const [localValue, setLocalValue] = useState('');

  const handleUpdate = () => {
    updateState(localValue);
  };

  return (
    <div>
      <h3>组件A (更新者)</h3>
      <p>当前共享值: {sharedValue}</p>
      <input
        value={localValue}
        onChange={(e) => setLocalValue(e.target.value)}
        placeholder="输入新值"
      />
      <button onClick={handleUpdate}>更新共享值</button>
    </div>
  );
}

// 使用自定义Hook的组件B
function ComponentWithHookB() {
  const { state: sharedValue, subscribe } = useSharedState('初始值');
  const [currentValue, setCurrentValue] = useState(sharedValue);

  useEffect(() => {
    // 订阅共享状态变化
    const unsubscribe = subscribe((newValue) => {
      setCurrentValue(newValue);
    });

    return unsubscribe;
  }, [subscribe]);

  return (
    <div>
      <h3>组件B (订阅者)</h3>
      <p>监听到的共享值: {currentValue}</p>
    </div>
  );
}

7. 使用Ref实现组件间方法调用

// 使用forwardRef和useImperativeHandle暴露组件方法
import React, { forwardRef, useImperativeHandle, useRef } from 'react';

// 子组件 - 暴露方法给父组件
const ChildWithMethods = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);
  const inputRef = useRef();

  // 暴露方法给父组件
  useImperativeHandle(ref, () => ({
    // 重置计数器
    resetCounter: () => {
      setCount(0);
    },

    // 增加计数器
    increment: (value = 1) => {
      setCount(prev => prev + value);
    },

    // 获取当前值
    getCount: () => {
      return count;
    },

    // 聚焦输入框
    focusInput: () => {
      inputRef.current?.focus();
    },

    // 设置值
    setValue: (value) => {
      setCount(value);
    }
  }));

  return (
    <div>
      <h3>子组件</h3>
      <p>内部计数: {count}</p>
      <input
        ref={inputRef}
        type="text"
        placeholder="可被父组件聚焦的输入框"
      />
    </div>
  );
});

// 父组件 - 调用子组件方法
function ParentCallingChild() {
  const childRef = useRef();

  const handleReset = () => {
    childRef.current?.resetCounter();
  };

  const handleIncrement = () => {
    childRef.current?.increment(5);
  };

  const handleGetCount = () => {
    const count = childRef.current?.getCount();
    alert(`当前计数: ${count}`);
  };

  const handleFocusInput = () => {
    childRef.current?.focusInput();
  };

  const handleSetValue = () => {
    childRef.current?.setValue(100);
  };

  return (
    <div>
      <h2>父组件</h2>
      <div>
        <button onClick={handleReset}>重置子组件计数</button>
        <button onClick={handleIncrement}>子组件计数+5</button>
        <button onClick={handleGetCount}>获取子组件计数</button>
        <button onClick={handleFocusInput}>聚焦子组件输入框</button>
        <button onClick={handleSetValue}>设置子组件计数为100</button>
      </div>
      <ChildWithMethods ref={childRef} />
    </div>
  );
}
Ref通信注意事项:
  • 这是命令式API,应谨慎使用(React提倡声明式)
  • 适合需要直接操作DOM的场景(如聚焦、播放视频)
  • 避免过度使用,优先考虑props和状态提升
  • 在函数组件中结合useImperativeHandle使用

8. 状态管理库(以Redux为例)

// Redux配置 (简化示例)
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';

// 1. 定义action类型
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const SET_VALUE = 'SET_VALUE';

// 2. 创建reducer
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + action.payload };
    case DECREMENT:
      return { count: state.count - action.payload };
    case SET_VALUE:
      return { count: action.payload };
    default:
      return state;
  }
}

// 3. 创建store
const store = createStore(counterReducer);

// 4. 创建action创建函数
const increment = (value = 1) => ({
  type: INCREMENT,
  payload: value
});

const decrement = (value = 1) => ({
  type: DECREMENT,
  payload: value
});

const setValue = (value) => ({
  type: SET_VALUE,
  payload: value
});

// 组件A - 读取状态
function ComponentA() {
  const count = useSelector(state => state.count);

  return (
    <div>
      <h3>组件A</h3>
      <p>当前计数: {count}</p>
    </div>
  );
}

// 组件B - 派发action
function ComponentB() {
  const dispatch = useDispatch();
  const [inputValue, setInputValue] = useState('');

  return (
    <div>
      <h3>组件B</h3>
      <div>
        <button onClick={() => dispatch(increment())}>+1</button>
        <button onClick={() => dispatch(decrement())}>-1</button>
        <button onClick={() => dispatch(increment(5))}>+5</button>
      </div>
      <div>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="设置计数"
        />
        <button onClick={() => {
          const value = parseInt(inputValue);
          if (!isNaN(value)) {
            dispatch(setValue(value));
            setInputValue('');
          }
        }}>
          设置值
        </button>
      </div>
    </div>
  );
}

// 组件C - 同时读取和派发
function ComponentC() {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();

  return (
    <div>
      <h3>组件C</h3>
      <p>计数: {count}</p>
      <button onClick={() => dispatch(setValue(0))}>
        重置
      </button>
    </div>
  );
}

// 应用入口
function ReduxApp() {
  return (
    <Provider store={store}>
      <ComponentA />
      <ComponentB />
      <ComponentC />
    </Provider>
  );
}

通信方式选择指南

简单通信

适用场景:

  • 父子组件直接通信
  • 数据流简单明确
  • 小型应用

推荐方案:

Props 回调函数
中等复杂度

适用场景:

  • 兄弟组件通信
  • 跨少数层级通信
  • 共享状态较少

推荐方案:

状态提升 Context API 自定义Hook
复杂通信

适用场景:

  • 大型应用全局状态
  • 任意组件间通信
  • 需要状态持久化

推荐方案:

Redux MobX 事件总线

最佳实践与常见问题

避免Props Drilling(层层传递props)

// 不好的做法:props drilling
function App() {
  const [user, setUser] = useState({ name: '张三' });

  return (
    <div>
      <Header user={user} />
      <MainContent user={user} />
    </div>
  );
}

function MainContent({ user }) {
  return (
    <div>
      <Sidebar user={user} />
      <Content user={user} />
    </div>
  );
}

function Content({ user }) {
  return (
    <div>
      <Article user={user} /> {/* 实际可能不需要user */}
    </div>
  );
}

// 好的做法:使用Context
const UserContext = createContext();

function App() {
  const [user, setUser] = useState({ name: '张三' });

  return (
    <UserContext.Provider value={user}>
      <Header />
      <MainContent />
    </UserContext.Provider>
  );
}

// 深层组件直接使用Context,不需要中间组件传递
function Article() {
  const user = useContext(UserContext);
  // 直接使用user
}

性能优化

// 使用React.memo避免不必要的重新渲染
const MemoizedChild = React.memo(function Child({ data, onClick }) {
  console.log('Child组件渲染');
  return (
    <div>
      <p>数据: {data}</p>
      <button onClick={onClick}>点击</button>
    </div>
  );
});

// 使用useCallback缓存回调函数
function Parent() {
  const [count, setCount] = useState(0);

  // 使用useCallback避免每次渲染都创建新函数
  const handleClick = useCallback(() => {
    console.log('按钮被点击');
  }, []); // 空依赖数组,函数只创建一次

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        更新计数: {count}
      </button>
      <MemoizedChild data="固定数据" onClick={handleClick} />
    </div>
  );
}

错误处理与边界

// 创建错误边界组件
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

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

  componentDidCatch(error, errorInfo) {
    // 可以在这里记录错误日志
    console.error('组件通信错误:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h3>组件通信出错</h3>
          <p>错误信息: {this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false })}>
            重试
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// 使用错误边界包裹组件
function App() {
  return (
    <ErrorBoundary>
      <ComponentA />
      <ComponentB />
    </ErrorBoundary>
  );
}
组件通信最佳实践:
  1. 优先使用props和回调函数进行父子组件通信
  2. 使用状态提升处理兄弟组件间简单通信
  3. 对于跨层级通信,考虑使用Context API
  4. 避免过度使用事件总线,保持组件间关系清晰
  5. 大型复杂应用考虑使用状态管理库
  6. 使用TypeScript或PropTypes进行类型检查
  7. 添加错误边界处理通信异常
  8. 性能优化:合理使用memo、useCallback、useMemo

通信方式选择器

请选择通信场景查看推荐方案