hooks/ — React Hooks 集合
目录: src/hooks/
Claude Code TUI 用 React + Ink 渲染。hooks/ 里是所有自定义 React Hooks——连接 UI 与底层状态/服务。
为什么这么多 Hooks?
终端 UI 的状态比想象中复杂:
- 对话流 — 消息不断更新
- 任务状态 — 任务随时创建/完成
- 文件状态 — Edit 后要刷新
- 权限询问 — 弹出/关闭
- 流式输出 — token 一个个来
每种状态一个 hook,避免组件再嵌一层状态地狱。
核心 Hooks
useMessages
function useMessages(): {
messages: Message[]
addMessage: (msg: Message) => void
updateLast: (delta: Partial<Message>) => void
clear: () => void
} {
const [messages, setMessages] = useState<Message[]>([])
const addMessage = useCallback((msg: Message) => {
setMessages(prev => [...prev, msg])
}, [])
// 流式更新最后一条
const updateLast = useCallback((delta) => {
setMessages(prev => {
const last = prev[prev.length - 1]
return [...prev.slice(0, -1), { ...last, ...delta }]
})
}, [])
return { messages, addMessage, updateLast, clear }
}
useStream
处理 LLM 的流式响应:
function useStream(): {
isStreaming: boolean
currentText: string
startStream: () => void
appendChunk: (chunk: string) => void
endStream: () => void
} {
const [isStreaming, setStreaming] = useState(false)
const [text, setText] = useState('')
const appendChunk = useCallback((chunk: string) => {
setText(prev => prev + chunk)
}, [])
return { isStreaming, currentText: text, ... }
}
useTasks
任务列表订阅:
function useTasks(): {
tasks: Task[]
running: Task[]
completed: Task[]
} {
const [tasks, setTasks] = useState<Task[]>([])
useEffect(() => {
const unsub = taskService.subscribe(setTasks)
return unsub
}, [])
const running = tasks.filter(t => t.status === 'running')
const completed = tasks.filter(t => t.status === 'completed')
return { tasks, running, completed }
}
useKeybindings
function useKeybindings(bindings: Record<string, () => void>) {
useEffect(() => {
const handler = (input: string, key: Key) => {
const shortcut = keyToString(input, key)
if (bindings[shortcut]) bindings[shortcut]()
}
process.stdin.on('keypress', handler)
return () => process.stdin.off('keypress', handler)
}, [bindings])
}
// 用法
useKeybindings({
'ctrl+c': () => cancel(),
'shift+tab': () => togglePlanMode(),
'ctrl+t': () => toggleTaskPanel(),
})
usePermissionPrompt
function usePermissionPrompt(): {
current: PermissionRequest | null
approve: (decision: Decision) => void
deny: () => void
} {
const [current, setCurrent] = useState<PermissionRequest | null>(null)
useEffect(() => {
return permissionService.onPrompt(setCurrent)
}, [])
const approve = (decision) => {
current?.resolve(decision)
setCurrent(null)
}
return { current, approve, deny }
}
useTokenCount
function useTokenCount(): {
input: number
output: number
cached: number
cost: number
} {
const [usage, setUsage] = useState({ ... })
useEffect(() => {
return costService.subscribe(setUsage)
}, [])
return usage
}
useSession
function useSession(): {
id: string
model: string
startedAt: number
turnCount: number
} {
return useContext(SessionContext)!
}
useWorkingDirectory
function useWorkingDirectory(): {
cwd: string
setCwd: (path: string) => void
projectRoot: string
} {
const [cwd, setCwd] = useState(process.cwd())
useEffect(() => {
process.chdir(cwd)
}, [cwd])
return { cwd, setCwd, projectRoot: findProjectRoot(cwd) }
}
useThrottledRender
减少 Ink 重渲染:
function useThrottledRender<T>(value: T, ms: number = 50): T {
const [throttled, setThrottled] = useState(value)
const lastUpdate = useRef(0)
useEffect(() => {
const now = Date.now()
const elapsed = now - lastUpdate.current
if (elapsed >= ms) {
setThrottled(value)
lastUpdate.current = now
} else {
const t = setTimeout(() => {
setThrottled(value)
lastUpdate.current = Date.now()
}, ms - elapsed)
return () => clearTimeout(t)
}
}, [value])
return throttled
}
流式输出时尤其有用——每个 token 都重渲染太慢。
useTerminalSize
function useTerminalSize(): { cols: number; rows: number } {
const [size, setSize] = useState({
cols: process.stdout.columns ?? 80,
rows: process.stdout.rows ?? 24
})
useEffect(() => {
const handler = () => setSize({
cols: process.stdout.columns ?? 80,
rows: process.stdout.rows ?? 24
})
process.stdout.on('resize', handler)
return () => process.stdout.off('resize', handler)
}, [])
return size
}
useInterval
function useInterval(callback: () => void, delay: number | null) {
useEffect(() => {
if (delay === null) return
const id = setInterval(callback, delay)
return () => clearInterval(id)
}, [callback, delay])
}
// 用法
useInterval(() => {
updateTaskStatuses()
}, 1000)
复合 Hook 案例
useCoordinatedInput
组合多个 hook:
function useCoordinatedInput() {
const { addMessage } = useMessages()
const { startStream } = useStream()
const { prompt } = useUserInput()
const permission = usePermissionPrompt()
// 阻塞输入当有权限询问
const canInput = !permission.current
const submit = async (text: string) => {
if (!canInput) return
addMessage({ role: 'user', content: text })
startStream()
await runAgent(text)
}
return { canInput, submit, prompt }
}
订阅模式
Hooks 大量用 subscribe + cleanup:
useEffect(() => {
const unsub = service.subscribe(listener)
return unsub // cleanup
}, [])
性能优化
memoize
const filteredMessages = useMemo(
() => messages.filter(m => m.visible),
[messages]
)
lazy init
const [config, _] = useState(() => loadConfig()) // 只算一次
测试 Hooks
import { renderHook } from '@testing-library/react-hooks'
test('useMessages adds message', () => {
const { result } = renderHook(() => useMessages())
act(() => {
result.current.addMessage({ role: 'user', content: 'hi' })
})
expect(result.current.messages).toHaveLength(1)
})
值得学习的点
- 按职责分离 hook — 每个 hook 一个 concern
- 订阅 + cleanup — 防止泄漏
- throttle 渲染 — 流式输出必备
- 组合高于继承 — 用 hook 组合而非 HOC
- lazy init — 避免重复计算
- useInterval 的陷阱 — closure 过期
- Ink 终端特殊性 — resize 监听