Skip to main content

以 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>
<VoteButton
label="赞成"
votes={yesVotes}
onClick={() => setYesVotes(yesVotes + 1)}
/>
<VoteButton
label="反对"
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 => (
<VoteOption
key={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.jsx
import { 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>
);
}
// 自定义Hook
export 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]) => (
<div
key={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 slice
import { 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 (
<div
className={`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大型应用全局状态可预测,工具丰富学习成本,代码量大
事件总线任意组件通信解耦,灵活难以跟踪,调试困难
复合组件相关组件组合高内聚,易用设计模式较复杂

建议:根据项目规模和需求选择合适的通信方式,优先考虑简单直接的方案,随着复杂度增加再考虑更高级的模式。