1663 字
8 分钟
为什么不应该在 Sidebar 组件中直接使用 useSessions Hook
2025-08-21

引言#

在 React 开发中,我们经常面临一个重要的架构决策:是否应该让组件直接使用业务逻辑 hooks,还是通过 props 传递数据和回调函数?本文将通过一个实际的聊天应用案例,分析为什么建议保持组件的数据流清晰,而不是在组件中直接使用业务逻辑 hooks。

问题背景#

在我们的聊天应用中,有一个 Sidebar 组件负责显示聊天会话列表。最初的设计是:

// 当前架构:通过 props 传递数据
<Sidebar
sessions={sessions}
currentSessionId={currentSessionId}
onSelectSession={selectSession}
onNewSession={createNewSession}
onDeleteSession={deleteSession}
isOpen={sidebarOpen}
onToggle={() => setSidebarOpen(!sidebarOpen)}
/>

有人可能会问:为什么不直接在 Sidebar 组件中使用 useSessions hook 呢?

// 不推荐的架构:直接在组件中使用 hook
export function Sidebar() {
const {
sessions,
currentSessionId,
selectSession,
createNewSession,
deleteSession,
} = useSessions();
// ...
}

为什么不建议在组件中直接使用业务逻辑 Hooks?#

1. 关注点分离 (Separation of Concerns)#

组件应该专注于 UI 渲染,而不是业务逻辑

// ✅ 好的做法:纯展示组件
export function Sidebar({
sessions,
onSelectSession,
onNewSession,
onDeleteSession,
}) {
return (
<div>
{sessions.map((session) => (
<button key={session.id} onClick={() => onSelectSession(session.id)}>
{session.title}
</button>
))}
</div>
);
}
// ❌ 不好的做法:组件混合了业务逻辑
export function Sidebar() {
const { sessions, selectSession } = useSessions(); // 业务逻辑混入组件
// ...
}

2. 可测试性 (Testability)#

纯组件更容易进行单元测试

// ✅ 容易测试:只需要模拟 props
test("Sidebar renders sessions correctly", () => {
const mockSessions = [
{ id: "1", title: "Chat 1" },
{ id: "2", title: "Chat 2" },
];
render(<Sidebar sessions={mockSessions} onSelectSession={jest.fn()} />);
expect(screen.getByText("Chat 1")).toBeInTheDocument();
expect(screen.getByText("Chat 2")).toBeInTheDocument();
});
// ❌ 难以测试:需要模拟 hook 和依赖
test("Sidebar with useSessions", () => {
// 需要模拟 useSessions hook
jest.mock("./useSessions", () => ({
useSessions: () => ({
sessions: mockSessions,
selectSession: jest.fn(),
}),
}));
// 测试变得复杂...
});

3. 可复用性 (Reusability)#

纯组件可以在不同场景下复用

// ✅ 可以在不同场景下复用
// 场景1:聊天应用
<Sidebar sessions={chatSessions} onSelectSession={handleChatSelect} />
// 场景2:文件管理器
<Sidebar sessions={fileSessions} onSelectSession={handleFileSelect} />
// 场景3:测试环境
<Sidebar sessions={mockSessions} onSelectSession={mockHandler} />

4. 状态管理集中化 (Centralized State Management)#

所有状态变化都在一个地方管理,便于调试

// ✅ 状态管理集中化
export function useChat() {
const { sessions, selectSession, createNewSession, deleteSession } =
useSessions();
const { messages, sendMessage } = useMessages();
const { user, logout } = useAuth();
// 所有业务逻辑都在这里
const handleSessionSelect = async (sessionId: string) => {
await selectSession(sessionId);
await loadMessages(sessionId);
};
return {
sessions,
onSelectSession: handleSessionSelect,
// ...
};
}

5. 数据流清晰 (Clear Data Flow)#

遵循 React 的数据流原则:props 向下,事件向上

// ✅ 清晰的数据流
App
├── useChat (业务逻辑层)
│ ├── useSessions
│ ├── useMessages
│ └── useAuth
└── Chat (组件层)
└── Sidebar (纯展示组件)
├── sessions (props 向下)
├── onSelectSession (事件向上)
└── onNewSession (事件向上)

6. 便于调试和维护 (Debugging and Maintenance)#

当出现问题时,可以快速定位到业务逻辑层

// ✅ 问题定位清晰
// 如果会话选择有问题,直接查看 useChat 中的 handleSessionSelect
// 如果 UI 显示有问题,直接查看 Sidebar 组件的渲染逻辑
// ❌ 问题定位困难
// 如果直接在 Sidebar 中使用 useSessions,问题可能分散在多个地方

实际案例分析#

让我们看看当前项目中的实现:

当前架构的优势#

// useChat.ts - 业务逻辑集中管理
export function useChat() {
const { sessions, selectSession, createNewSession, deleteSession } =
useSessions();
// 包装业务逻辑,添加额外的处理
const selectSessionWrapper = async (sessionId: string) => {
clearMessages(); // 清除当前消息
setRequest(""); // 清空输入
setError(""); // 清除错误
await selectSession(sessionId);
await loadChatMessages(sessionId); // 加载消息
};
return {
sessions,
onSelectSession: selectSessionWrapper,
// ...
};
}
// Chat.tsx - 组件层
export function Chat() {
const { sessions, onSelectSession, onNewSession, onDeleteSession } =
useChat();
return (
<Sidebar
sessions={sessions}
onSelectSession={onSelectSession}
onNewSession={onNewSession}
onDeleteSession={onDeleteSession}
/>
);
}
// Sidebar.tsx - 纯展示组件
export function Sidebar({
sessions,
onSelectSession,
onNewSession,
onDeleteSession,
}) {
return (
<div>
{sessions.map((session) => (
<button key={session.id} onClick={() => onSelectSession(session.id)}>
{session.title}
</button>
))}
</div>
);
}

如果直接在 Sidebar 中使用 useSessions 的问题#

// ❌ 不推荐的实现
export function Sidebar() {
const { sessions, selectSession, createNewSession, deleteSession } =
useSessions();
const { clearMessages, loadChatMessages } = useMessages(); // 需要额外的 hook
const handleSelectSession = async (sessionId: string) => {
clearMessages(); // 业务逻辑混入组件
await selectSession(sessionId);
await loadChatMessages(sessionId);
};
return (
<div>
{sessions.map((session) => (
<button
key={session.id}
onClick={() => handleSelectSession(session.id)}
>
{session.title}
</button>
))}
</div>
);
}

何时可以考虑在组件中使用 Hooks?#

虽然我们建议保持组件的数据流清晰,但在某些特定情况下,可以考虑在组件中直接使用 hooks:

1. UI 相关的 Hooks#

// ✅ 适合在组件中使用:UI 相关的 hooks
export function Sidebar() {
const [isOpen, setIsOpen] = useState(false);
const isMobile = useMediaQuery("(max-width: 768px)");
return (
<div className={isMobile ? "mobile-layout" : "desktop-layout"}>
{/* ... */}
</div>
);
}

2. 组件内部状态管理#

// ✅ 适合在组件中使用:组件内部状态
export function Sidebar() {
const [searchTerm, setSearchTerm] = useState("");
const [sortBy, setSortBy] = useState("date");
const filteredSessions = useMemo(() => {
return sessions.filter((session) =>
session.title.toLowerCase().includes(searchTerm.toLowerCase()),
);
}, [sessions, searchTerm]);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{filteredSessions.map((session) => (
<SessionItem key={session.id} session={session} />
))}
</div>
);
}

3. 第三方库的 Hooks#

// ✅ 适合在组件中使用:第三方库的 hooks
export function Sidebar() {
const { data, loading, error } = useQuery(["sessions"], fetchSessions);
const { mutate: deleteSession } = useMutation(deleteSessionApi);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return (
<div>
{data.map((session) => (
<SessionItem key={session.id} session={session} />
))}
</div>
);
}

最佳实践总结#

1. 分层架构#

业务逻辑层 (Hooks)
├── useChat
├── useSessions
├── useMessages
└── useAuth
组件层 (Components)
├── Chat
├── Sidebar
├── MessageList
└── ChatInput

2. 数据流原则#

  • Props 向下传递:数据从父组件流向子组件
  • 事件向上传递:用户交互从子组件流向父组件
  • 业务逻辑集中:所有业务逻辑都在 hooks 中处理

3. 组件设计原则#

  • 单一职责:每个组件只负责一个功能
  • 纯函数:组件应该是纯函数,相同的 props 产生相同的输出
  • 可测试:组件应该易于测试
  • 可复用:组件应该可以在不同场景下复用

结论#

虽然在某些情况下可以在组件中直接使用业务逻辑 hooks,但为了保持代码的可维护性、可测试性和可复用性,建议遵循以下原则:

  1. 保持组件的数据流清晰:通过 props 传递数据,通过回调函数处理事件
  2. 集中管理业务逻辑:将所有业务逻辑放在专门的 hooks 中
  3. 关注点分离:组件专注于 UI 渲染,hooks 专注于业务逻辑
  4. 遵循 React 最佳实践:props 向下,事件向上

这种架构模式不仅符合 React 的设计理念,也为项目的长期维护和扩展奠定了良好的基础。


本文基于实际项目经验总结,希望对您的 React 开发有所帮助。如果您有任何问题或建议,欢迎讨论交流。

为什么不应该在 Sidebar 组件中直接使用 useSessions Hook
https://blog.cuixu.cn/posts/frontend/why-not-use-hooks-in-components/
作者
崔旭
发布于
2025-08-21
许可协议
CC BY-NC-SA 4.0