Claude-skill-registry ink-hooks-state
Use when managing state and side effects in Ink applications using React hooks for terminal UIs.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/ink-hooks-state" ~/.claude/skills/majiayu000-claude-skill-registry-ink-hooks-state && rm -rf "$T"
manifest:
skills/data/ink-hooks-state/SKILL.mdsource content
Ink Hooks and State Management
You are an expert in managing state and side effects in Ink applications using React hooks.
Core Hooks
useState - Local State
import { Box, Text } from 'ink'; import React, { useState } from 'react'; const Counter: React.FC = () => { const [count, setCount] = useState(0); return ( <Box> <Text>Count: {count}</Text> </Box> ); };
useEffect - Side Effects
import { useEffect, useState } from 'react'; const DataLoader: React.FC<{ fetchData: () => Promise<string[]> }> = ({ fetchData }) => { const [data, setData] = useState<string[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); useEffect(() => { fetchData() .then((result) => { setData(result); setLoading(false); }) .catch((err: Error) => { setError(err); setLoading(false); }); }, [fetchData]); if (loading) return <Text>Loading...</Text>; if (error) return <Text color="red">Error: {error.message}</Text>; return ( <Box flexDirection="column"> {data.map((item, i) => ( <Text key={i}>{item}</Text> ))} </Box> ); };
useInput - Keyboard Input
import { useInput } from 'ink'; import { useState } from 'react'; const InteractiveMenu: React.FC<{ onExit: () => void }> = ({ onExit }) => { const [selectedIndex, setSelectedIndex] = useState(0); const items = ['Option 1', 'Option 2', 'Option 3']; useInput((input, key) => { if (key.upArrow) { setSelectedIndex((prev) => Math.max(0, prev - 1)); } if (key.downArrow) { setSelectedIndex((prev) => Math.min(items.length - 1, prev + 1)); } if (key.return) { // Handle selection } if (input === 'q' || key.escape) { onExit(); } }); return ( <Box flexDirection="column"> {items.map((item, i) => ( <Text key={i} color={i === selectedIndex ? 'cyan' : 'white'}> {i === selectedIndex ? '> ' : ' '} {item} </Text> ))} </Box> ); };
useApp - App Control
import { useApp } from 'ink'; import { useEffect } from 'react'; const AutoExit: React.FC<{ delay: number }> = ({ delay }) => { const { exit } = useApp(); useEffect(() => { const timer = setTimeout(() => { exit(); }, delay); return () => clearTimeout(timer); }, [delay, exit]); return <Text>Exiting in {delay}ms...</Text>; };
useStdout - Terminal Dimensions
import { useStdout } from 'ink'; const ResponsiveComponent: React.FC = () => { const { stdout } = useStdout(); const width = stdout.columns; const height = stdout.rows; return ( <Box> <Text> Terminal size: {width}x{height} </Text> </Box> ); };
useFocus - Focus Management
import { useFocus, useFocusManager } from 'ink'; const FocusableItem: React.FC<{ label: string }> = ({ label }) => { const { isFocused } = useFocus(); return ( <Text color={isFocused ? 'cyan' : 'white'}> {isFocused ? '> ' : ' '} {label} </Text> ); }; const FocusableList: React.FC = () => { const { enableFocus } = useFocusManager(); useEffect(() => { enableFocus(); }, [enableFocus]); return ( <Box flexDirection="column"> <FocusableItem label="First" /> <FocusableItem label="Second" /> <FocusableItem label="Third" /> </Box> ); };
Advanced Patterns
Custom Hooks
// useInterval hook function useInterval(callback: () => void, delay: number | null) { const savedCallback = useRef(callback); useEffect(() => { savedCallback.current = callback; }, [callback]); useEffect(() => { if (delay === null) return; const id = setInterval(() => savedCallback.current(), delay); return () => clearInterval(id); }, [delay]); } // Usage const Spinner: React.FC = () => { const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; const [frame, setFrame] = useState(0); useInterval(() => { setFrame((prev) => (prev + 1) % frames.length); }, 80); return <Text color="cyan">{frames[frame]}</Text>; };
Async State Management
function useAsync<T>(asyncFunction: () => Promise<T>) { const [state, setState] = useState<{ loading: boolean; error: Error | null; data: T | null; }>({ loading: true, error: null, data: null, }); useEffect(() => { let mounted = true; asyncFunction() .then((data) => { if (mounted) { setState({ loading: false, error: null, data }); } }) .catch((error: Error) => { if (mounted) { setState({ loading: false, error, data: null }); } }); return () => { mounted = false; }; }, [asyncFunction]); return state; }
Promise-based Flow Control
interface PromiseFlowProps { onComplete: (result: string[]) => void; onError: (error: Error) => void; execute: () => Promise<string[]>; } const PromiseFlow: React.FC<PromiseFlowProps> = ({ onComplete, onError, execute }) => { const [phase, setPhase] = useState<'pending' | 'success' | 'error'>('pending'); useEffect(() => { execute() .then((result) => { setPhase('success'); onComplete(result); }) .catch((err: Error) => { setPhase('error'); onError(err); }); }, [execute, onComplete, onError]); return ( <Box> {phase === 'pending' && <Text color="yellow">Processing...</Text>} {phase === 'success' && <Text color="green">Complete!</Text>} {phase === 'error' && <Text color="red">Failed!</Text>} </Box> ); };
Best Practices
- Cleanup: Always cleanup in useEffect return functions
- Dependencies: Correctly specify dependency arrays
- Refs: Use useRef for mutable values that don't trigger re-renders
- Callbacks: Use useCallback to memoize event handlers
- Unmount Safety: Check mounted state before setting state in async operations
Common Pitfalls
- Forgetting to cleanup intervals and timeouts
- Missing dependencies in useEffect
- Setting state on unmounted components
- Not handling keyboard input edge cases
- Infinite re-render loops from incorrect dependencies