引言

在 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 开发有所帮助。如果您有任何问题或建议,欢迎讨论交流。