ink-hooks-state

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Ink Hooks and State Management

Ink Hooks与状态管理

You are an expert in managing state and side effects in Ink applications using React hooks.
您是在基于React Hooks的Ink应用中管理状态与副作用的专家。

Core Hooks

核心Hooks

useState - Local State

useState - 本地状态

tsx
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>
  );
};
tsx
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

useEffect - 副作用处理

tsx
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>
  );
};
tsx
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

useInput - 键盘输入处理

tsx
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>
  );
};
tsx
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

useApp - 应用控制

tsx
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>;
};
tsx
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

useStdout - 终端尺寸获取

tsx
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>
  );
};
tsx
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

useFocus - 焦点管理

tsx
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>
  );
};
tsx
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

自定义Hooks

tsx
// 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>;
};
tsx
// 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

异步状态管理

tsx
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;
}
tsx
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

基于Promise的流程控制

tsx
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>
  );
};
tsx
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

最佳实践

  1. Cleanup: Always cleanup in useEffect return functions
  2. Dependencies: Correctly specify dependency arrays
  3. Refs: Use useRef for mutable values that don't trigger re-renders
  4. Callbacks: Use useCallback to memoize event handlers
  5. Unmount Safety: Check mounted state before setting state in async operations
  1. 清理操作:始终在useEffect的返回函数中执行清理
  2. 依赖项:正确指定依赖数组
  3. Refs:使用useRef存储不会触发重渲染的可变值
  4. 回调函数:使用useCallback对事件处理函数进行记忆化
  5. 卸载安全:在异步操作中设置状态前检查组件是否已挂载

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
  • 忘记清理定时器与间隔器
  • useEffect中遗漏依赖项
  • 在已卸载的组件上设置状态
  • 未处理键盘输入的边缘情况
  • 依赖项错误导致的无限重渲染循环