Loading...
Loading...
Refactor and review state management in React and TypeScript applications. Use when: refactoring component state, reviewing useState usage, choosing between local and global state, preventing unnecessary re-renders, selecting state management libraries (Zustand, Jotai, Redux), applying discriminated unions, deriving state, managing refs vs state, or eliminating prop drilling.
npx skill4agent add mmncit/skills react-stateuseStateuseRefuseState// Avoid: separate variables for related data
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
// Prefer: single object for related data
const [formData, setFormData] = useState({
firstName: "",
lastName: "",
email: "",
});// Avoid: spreading directly (risks stale closures)
setFormData({ ...formData, email: newEmail });
// Prefer: callback with previous state
setFormData((prev) => ({ ...prev, email: newEmail }));// Avoid: duplicated state
const [hotels, setHotels] = useState<Hotel[]>([]);
const [selectedHotel, setSelectedHotel] = useState<Hotel | null>(null);
// Prefer: store only the ID, derive the object
const [hotels, setHotels] = useState<Hotel[]>([]);
const [selectedHotelId, setSelectedHotelId] = useState<string | null>(null);
const selectedHotel = hotels.find((h) => h.id === selectedHotelId) ?? null;useStateuseState| Scenario | Use Instead |
|---|---|
| Static values that never change | |
| Values computable from existing state/props | Derive inline during render |
| Values that don't need to trigger re-renders | |
useRefuseRefconst scrollPosition = useRef(0);
useEffect(() => {
const handleScroll = () => {
scrollPosition.current = window.scrollY;
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);const timerId = useRef<ReturnType<typeof setTimeout> | null>(null);
const startTimer = () => {
timerId.current = setTimeout(() => {
// ...
}, 1000);
};
const stopTimer = () => {
if (timerId.current) clearTimeout(timerId.current);
};type RequestStatus = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<RequestStatus>("idle");statustype RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: Data }
| { status: "error"; error: Error };
const [state, setState] = useState<RequestState>({ status: "idle" });
// TypeScript narrows the type automatically
if (state.status === "success") {
console.log(state.data); // ✓ data is available
}type FormState =
| { submitted: false; values: FormValues }
| { submitted: true; values: FormValues; response: ServerResponse };| Approach | Characteristics | Best For |
|---|---|---|
| Store-based (e.g. Zustand, Redux) | Centralized state, controlled transitions via actions/events, indirect mutation | Complex logic, strict state control |
| Atomic / Signal-based (e.g. Jotai, Recoil, Signals) | Decentralized atoms, reactive subscriptions, updatable from anywhere | Data that changes freely from external sources, fine-grained reactivity |
useSelector// Only re-renders when `count` changes, not the entire store
const count = useStore((state) => state.count);| Anti-Pattern | Problem | Fix |
|---|---|---|
| Impossible states ( | Use a single |
| Creates render cascades and stale data | Derive B from A inline during render |
| Causes re-renders on every update with no visual change | Use |
| Storing full objects when only an ID is needed | Stale references when source array updates | Store the ID, derive the object via |
| All consumers re-render on every change | Use a store with selectors or split into separate contexts |
useStateuseRefstatussetState(prev => ...)