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)[].