Hook Inside Condition
Calling a hook inside an if block messes up the order hooks run in.
Hooks are special functions that React gives you, which let function components do things like store data (state), run side effects, work with references, and reuse logic. Before hooks existed, you had to do most of this inside class components. Hooks made React code cleaner and easier to break into small, reusable pieces.
Before hooks came along:
Hooks made things better by making it easier to:
A hook is just a normal JavaScript function, but it has special behavior inside React. You can only call hooks inside React function components, or inside your own custom hooks.
const [count, setCount] = useState(0);if statements), or nested functions.React keeps track of hooks based on the order they are called in, on every single render. If that order changes between renders, React can end up connecting the wrong state to the wrong hook.
On every render, React runs your component function from top to bottom, step by step.
useState is used to store data (state) that belongs to a single component.
const [state, setState] = useState(initialValue);open, loading, or error.useReducer is a better fit.import { useState } from "react";
function Counter() { const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;}useEffect lets you run side-effect code after React has updated the screen.
useMemo if the calculation is expensive.useEffect for those.| Dependency array | Behavior |
|---|---|
[] | Runs once after mount |
[x] | Runs when x changes |
| no array | Runs after every render |
import { useEffect, useState } from "react";
function Clock() { const [time, setTime] = useState(new Date());
useEffect(() => { const id = setInterval(() => setTime(new Date()), 1000);
return () => { clearInterval(id); }; }, []);
return <p>{time.toLocaleTimeString()}</p>;}useRef stores a value that can change, and that value stays the same across re-renders - but changing it does not cause the component to re-render.
Changing ref.current does NOT re-renderimport { useRef } from "react";
function FocusField() { const inputRef = useRef(null);
return ( <> <input ref={inputRef} /> <button onClick={() => inputRef.current?.focus()}>Focus</button> </> );}This example combines controlled state with useRef to build a real-world form flow - it auto-focuses fields, resets the form, and focuses the field that has a validation error.
import { FormEvent, useRef, useState } from "react";
export default function SignupForm() { const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [error, setError] = useState("");
const nameRef = useRef<HTMLInputElement | null>(null); const emailRef = useRef<HTMLInputElement | null>(null);
function handleSubmit(e: FormEvent) { e.preventDefault(); setError("");
if (!name.trim()) { setError("Name is required"); nameRef.current?.focus(); return; }
if (!email.includes("@")) { setError("Enter a valid email"); emailRef.current?.focus(); return; }
console.log("Submitted:", { name, email }); setName(""); setEmail(""); nameRef.current?.focus(); }
return ( <form onSubmit={handleSubmit}> <input ref={nameRef} value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" /> <input ref={emailRef} value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" /> <button type="submit">Create Account</button> {error && <p>{error}</p>} </form> );}useMemo saves (caches) the result of a calculation that takes a lot of effort to run, so React doesn’t have to redo it every time.
import { useMemo } from "react";
const visibleUsers = useMemo(() => { return users.filter((u) => u.active).sort((a, b) => a.name.localeCompare(b.name));}, [users]);useCallback saves (caches) a function so it doesn’t get recreated on every render.
useCallback just by default.import { useCallback } from "react";
const handleClick = useCallback(() => { console.log("Click");}, []);useReducer is useful when your state logic is complex, or when you have many related pieces of state that update together.
const [state, dispatch] = useReducer(reducer, initialState);import { useReducer } from "react";
type State = { count: number };type Action = { type: "increment" } | { type: "decrement" } | { type: "reset" };
function reducer(state: State, action: Action): State { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; case "reset": return { count: 0 }; default: return state; }}
export default function CounterReducer() { const [state, dispatch] = useReducer(reducer, { count: 0 });
return ( <div> <p>{state.count}</p> <button onClick={() => dispatch({ type: "increment" })}>+</button> <button onClick={() => dispatch({ type: "decrement" })}>-</button> <button onClick={() => dispatch({ type: "reset" })}>Reset</button> </div> );}useLayoutEffect runs right after React updates the DOM, but before the browser actually paints anything on the screen.
useEffect for those instead.useEffect -> after paintuseLayoutEffect -> before paintA custom hook is just a function that uses other hooks inside it, so you can reuse the same logic across multiple components.
The function name must start with use.
import { useState } from "react";
function useCounter(initial) { const [count, setCount] = useState(initial);
function increment() { setCount((c) => c + 1); }
return { count, increment };}function App() { const { count, increment } = useCounter(0);
return <button onClick={increment}>{count}</button>;}Every component has some internal data that React keeps track of, called a Fiber. Your hook values are stored there, in the same order you called them.
Each hook node stores data like:
{ memoizedState, queue, next}Hook Inside Condition
Calling a hook inside an if block messes up the order hooks run in.
Missing Effect Dependency
If you forget a dependency, you can end up with old (stale) values and confusing bugs.
Missing Cleanup
Timers and listeners need to be cleaned up, or they can cause memory leaks.
Overusing Memo Hooks
Using useMemo and useCallback everywhere adds extra complexity, even when there’s no real benefit.
if (isOpen) { useState(0); // Wrong}const [count, setCount] = useState(0);
if (!isOpen) { return null;}| Problem | Hook |
|---|---|
| Local value that changes UI | useState |
| Complex state transitions | useReducer |
| Side effects / fetch / subscriptions | useEffect |
| DOM access / mutable box | useRef |
| Expensive computed value | useMemo |
| Stable callback function | useCallback |
| Measure layout before paint | useLayoutEffect |
Keep these simple ideas in mind:
Hooks = ordered state slots for a componentReact stores hook values outside your function, then gives them back on next render