以 Vote(投票)组件学习 React 组件通信
让我们通过一个投票组件的案例,全面学习 React 的各种组件通信方式。
1. 基础案例:父子组件通信
父组件 → 子组件(Props 向下传递)
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// VoteButton.jsx - 子组件function VoteButton({ label, onClick, votes }) {return (<button onClick={onClick}>{label} ({votes} 票)</button>);}// VoteSystem.jsx - 父组件function VoteSystem() {const [yesVotes, setYesVotes] = useState(0);const [noVotes, setNoVotes] = useState(0);return (<div><h2>投票系统</h2><VoteButtonlabel="赞成"votes={yesVotes}onClick={() => setYesVotes(yesVotes + 1)}/><VoteButtonlabel="反对"votes={noVotes}onClick={() => setNoVotes(noVotes + 1)}/></div>);}
子组件 → 父组件(回调函数)
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// VoteOption.jsx - 子组件function VoteOption({ option, onVote }) {return (<div className="vote-option"><h3>{option.title}</h3><p>{option.description}</p><button onClick={() => onVote(option.id)}>投票给这个选项</button></div>);}// VotePoll.jsx - 父组件function VotePoll() {const [votes, setVotes] = useState({});const handleVote = (optionId) => {setVotes(prev => ({...prev,[optionId]: (prev[optionId] || 0) + 1}));};const options = [{ id: 1, title: "React", description: "前端框架" },{ id: 2, title: "Vue", description: "渐进式框架" },{ id: 3, title: "Angular", description: "企业级框架" }];return (<div>{options.map(option => (<VoteOptionkey={option.id}option={option}onVote={handleVote}/>))}<pre>{JSON.stringify(votes, null, 2)}</pre></div>);}
2. Context API - 跨层级通信
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// VoteContext.jsximport { createContext, useContext, useState } from 'react';const VoteContext = createContext();// 提供者组件export function VoteProvider({ children }) {const [totalVotes, setTotalVotes] = useState(0);const [voteHistory, setVoteHistory] = useState([]);const addVote = (voteData) => {setTotalVotes(prev => prev + 1);setVoteHistory(prev => [...prev, {id: Date.now(),...voteData,timestamp: new Date().toISOString()}]);};const resetVotes = () => {setTotalVotes(0);setVoteHistory([]);};return (<VoteContext.Provider value={{totalVotes,voteHistory,addVote,resetVotes}}>{children}</VoteContext.Provider>);}// 自定义Hookexport function useVote() {const context = useContext(VoteContext);if (!context) {throw new Error('useVote必须在VoteProvider内使用');}return context;}// VoteButton.jsx - 使用Context的组件function VoteButton({ option }) {const { addVote } = useVote();const handleClick = () => {addVote({ option, user: "当前用户" });};return (<button onClick={handleClick}>投票给 {option}</button>);}// VoteStats.jsx - 另一个使用Context的组件function VoteStats() {const { totalVotes, voteHistory } = useVote();return (<div><h3>总票数: {totalVotes}</h3><ul>{voteHistory.map(vote => (<li key={vote.id}>{vote.user} 投票给 {vote.option} - {vote.timestamp}</li>))}</ul></div>);}// App.jsx - 使用Provider包裹function App() {return (<VoteProvider><div className="app"><h1>全局投票系统</h1><VoteButton option="React" /><VoteButton option="Vue" /><VoteButton option="Angular" /><VoteStats /></div></VoteProvider>);}
3. 状态提升 - 兄弟组件通信
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// VoteControls.jsx - 控制组件function VoteControls({ onVote }) {return (<div className="controls"><button onClick={() => onVote('yes')}>👍 赞成</button><button onClick={() => onVote('no')}>👎 反对</button><button onClick={() => onVote('abstain')}>🤷 弃权</button></div>);}// VoteDisplay.jsx - 显示组件function VoteDisplay({ votes }) {const total = Object.values(votes).reduce((a, b) => a + b, 0);return (<div className="display"><h3>投票结果</h3><p>赞成: {votes.yes} 票</p><p>反对: {votes.no} 票</p><p>弃权: {votes.abstain} 票</p><p>总计: {total} 票</p></div>);}// VoteChart.jsx - 图表组件function VoteChart({ votes }) {return (<div className="chart">{Object.entries(votes).map(([type, count]) => (<divkey={type}className="chart-bar"style={{ height: `${count * 20}px` }}>{type}: {count}</div>))}</div>);}// VoteContainer.jsx - 父组件(状态提升)function VoteContainer() {const [votes, setVotes] = useState({yes: 0,no: 0,abstain: 0});const handleVote = (type) => {setVotes(prev => ({...prev,[type]: prev[type] + 1}));};return (<div className="vote-container"><VoteControls onVote={handleVote} /><VoteDisplay votes={votes} /><VoteChart votes={votes} /></div>);}
4. 使用 Redux/状态管理库
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// voteSlice.js - Redux Toolkit sliceimport { createSlice } from '@reduxjs/toolkit';const voteSlice = createSlice({name: 'votes',initialState: {items: {},isLoading: false,error: null},reducers: {voteAdded: (state, action) => {const { pollId, optionId } = action.payload;if (!state.items[pollId]) {state.items[pollId] = {};}state.items[pollId][optionId] = (state.items[pollId][optionId] || 0) + 1;},votesCleared: (state, action) => {const { pollId } = action.payload;if (state.items[pollId]) {state.items[pollId] = {};}}}});export const { voteAdded, votesCleared } = voteSlice.actions;export default voteSlice.reducer;// VoteItem.jsx - 连接Redux的组件import { useSelector, useDispatch } from 'react-redux';import { voteAdded } from './voteSlice';function VoteItem({ pollId, option }) {const dispatch = useDispatch();const voteCount = useSelector(state =>state.votes.items[pollId]?.[option.id] || 0);const handleVote = () => {dispatch(voteAdded({ pollId, optionId: option.id }));};return (<div className="vote-item"><h4>{option.text}</h4><p>票数: {voteCount}</p><button onClick={handleVote}>投票</button></div>);}
5. 自定义事件(发布-订阅模式)
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// voteEventBus.js - 事件总线class VoteEventBus {constructor() {this.listeners = {};}emit(event, data) {if (this.listeners[event]) {this.listeners[event].forEach(callback => callback(data));}}on(event, callback) {if (!this.listeners[event]) {this.listeners[event] = [];}this.listeners[event].push(callback);}off(event, callback) {if (this.listeners[event]) {this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);}}}export const voteEventBus = new VoteEventBus();// VoteEmitter.jsx - 事件发射器function VoteEmitter() {const emitVote = () => {voteEventBus.emit('vote', {userId: 'user123',choice: 'yes',timestamp: Date.now()});};return (<button onClick={emitVote}>发送投票事件</button>);}// VoteListener.jsx - 事件监听器function VoteListener() {const [votes, setVotes] = useState([]);useEffect(() => {const handleVote = (data) => {setVotes(prev => [...prev, data]);};voteEventBus.on('vote', handleVote);return () => {voteEventBus.off('vote', handleVote);};}, []);return (<div><h3>收到的事件:</h3><pre>{JSON.stringify(votes, null, 2)}</pre></div>);}
6. 复合组件模式
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// Vote.jsx - 复合组件function Vote({ children, defaultValue = {} }) {const [votes, setVotes] = useState(defaultValue);const [selected, setSelected] = useState(null);const contextValue = {votes,selected,setSelected,addVote: (option) => {setVotes(prev => ({...prev,[option]: (prev[option] || 0) + 1}));}};return (<VoteContext.Provider value={contextValue}><div className="vote">{children}</div></VoteContext.Provider>);}// 子组件Vote.Option = function VoteOption({ value, children }) {const { selected, setSelected } = useContext(VoteContext);return (<divclassName={`option ${selected === value ? 'selected' : ''}`}onClick={() => setSelected(value)}>{children}</div>);};Vote.Submit = function VoteSubmit() {const { selected, addVote } = useContext(VoteContext);const handleSubmit = () => {if (selected) {addVote(selected);alert(`已投票给: ${selected}`);}};return (<button onClick={handleSubmit} disabled={!selected}>提交投票</button>);};Vote.Results = function VoteResults() {const { votes } = useContext(VoteContext);return (<div className="results"><h3>投票结果</h3><ul>{Object.entries(votes).map(([option, count]) => (<li key={option}>{option}: {count} 票</li>))}</ul></div>);};// 使用复合组件function App() {return (<Vote defaultValue={{ React: 10, Vue: 5, Angular: 3 }}><h2>请选择你喜欢的框架:</h2><Vote.Option value="React">⚛️ React</Vote.Option><Vote.Option value="Vue">🟢 Vue</Vote.Option><Vote.Option value="Angular">🔴 Angular</Vote.Option><Vote.Submit /><Vote.Results /></Vote>);}
总结对比
| 通信方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Props传递 | 父子组件简单通信 | 简单直接,易于理解 | 深度嵌套时繁琐 |
| 回调函数 | 子向父通信 | 灵活,双向通信 | 多层传递麻烦 |
| Context API | 跨层级通信 | 避免props drilling | 可能引起不必要的重渲染 |
| 状态提升 | 兄弟组件通信 | 集中状态管理 | 状态提升到高层组件 |
| Redux | 大型应用全局状态 | 可预测,工具丰富 | 学习成本,代码量大 |
| 事件总线 | 任意组件通信 | 解耦,灵活 | 难以跟踪,调试困难 |
| 复合组件 | 相关组件组合 | 高内聚,易用 | 设计模式较复杂 |
建议:根据项目规模和需求选择合适的通信方式,优先考虑简单直接的方案,随着复杂度增加再考虑更高级的模式。