Use when managing state and side effects in Ink applications using React hooks for terminal UIs.
Provides React hooks for managing state and side effects in Ink terminal UIs, including useState, useEffect, useInput, useApp, and custom hooks for async operations and intervals.
/plugin marketplace add TheBushidoCollective/han/plugin install jutsu-gleam@hanThis skill cannot use any tools. It operates in read-only mode without the ability to modify files or execute commands.
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>
);
};