Hooks với TypeScript

#useState — khi nào cần generic?

TypeScript infer được type từ initial value. Chỉ cần generic khi initial value không phản ánh đủ type:

// ✅ Inference đủ — không cần generic
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [active, setActive] = useState(false);
 
// ✅ Cần generic — initial null nhưng sau sẽ là User
const [user, setUser] = useState<User | null>(null);
 
// ✅ Cần generic — array rỗng không có type information
const [items, setItems] = useState<string[]>([]);

#useRef — hai use cases khác nhau

useRef có hai chữ ký khác nhau và hay bị nhầm:

// 1. Ref để gắn vào DOM element — initial phải là null
const inputRef = useRef<HTMLInputElement>(null);
 
useEffect(() => {
  inputRef.current?.focus(); // current có thể null trước khi mount
}, []);
 
return <input ref={inputRef} />;
// 2. Mutable value không trigger re-render — initial là value thật
const timerRef = useRef<number | null>(null);
 
function start() {
  timerRef.current = window.setInterval(() => {
    // ...
  }, 1000);
}
 
function stop() {
  if (timerRef.current !== null) {
    clearInterval(timerRef.current);
  }
}

Dấu hiệu chọn đúng: nếu ref gắn vào JSX element thì initial = null, nếu là store mutable value thì initial = giá trị thật.

#useReducer cho state phức tạp

type State = {
  status: 'idle' | 'loading' | 'success' | 'error';
  data: User[] | null;
  error: string | null;
};
 
type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: User[] }
  | { type: 'FETCH_ERROR'; payload: string };
 
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, status: 'loading', error: null };
    case 'FETCH_SUCCESS':
      return { status: 'success', data: action.payload, error: null };
    case 'FETCH_ERROR':
      return { status: 'error', data: null, error: action.payload };
  }
}
 
const [state, dispatch] = useReducer(reducer, {
  status: 'idle',
  data: null,
  error: null,
});

Discriminated union cho Action đảm bảo mỗi case chỉ access đúng payload của nó.

#Custom hook với generic

Generic hooks cho phép tái sử dụng logic mà vẫn giữ được type safety:

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });
 
  const setStoredValue = (newValue: T) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  };
 
  return [value, setStoredValue] as const;
}
 
// Usage — TypeScript infer T = { theme: string }
const [settings, setSettings] = useLocalStorage('settings', { theme: 'light' });

as const ở return đảm bảo TypeScript biết đây là tuple [T, (v: T) => void], không phải array (T | (v: T) => void)[].