Help us improve
Share bugs, ideas, or general feedback.
From ink
Manages state and side effects in Ink terminal UIs using React hooks: useState, useEffect, useInput, useApp, useStdout.
npx claudepluginhub thebushidocollective/han --plugin inkHow this skill is triggered — by the user, by Claude, or both
Slash command
/ink:ink-hooks-stateThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are an expert in managing state and side effects in Ink applications using React hooks.
Build terminal UIs for React CLI apps using Ink component patterns: layouts, lists with icons, status messages, progress indicators, and collapsible sections.
Provides patterns and examples for React Hooks including useState, useEffect, useContext, useMemo, useCallback, and custom hooks for state management and side effects.
Provides React hooks reference with syntax, patterns for useState/useReducer state, useEffect side effects/cleanup, useRef/useContext, useMemo/useCallback optimization, React 19 hooks, custom hooks, and best practices.
Share bugs, ideas, or general feedback.
You are an expert in managing state and side effects in Ink applications using React hooks.
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>
);
};
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>
);
};
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>
);
};
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>;
};
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>
);
};
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>
);
};
// 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>;
};
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;
}
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>
);
};