When React hooks landed in version 16.8, they changed everything. No more class components just to manage a bit of state. No more lifecycle method gymnastics. Hooks let you do all of that - and more - in simple, readable functions.
But if you're new to them, it can feel like there's a lot going on. Let's cut through the noise.
The Big Three
1. useState - Managing State
State is data that, when it changes, causes your component to re-render. useState is how you create it.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
useState(0) gives you two things:
count- the current valuesetCount- a function to update it
Every time you call setCount, React re-renders the component with the new value. Simple.
2. useEffect - Side Effects
Side effects are things that happen outside of rendering - fetching data, setting up timers, updating the document title, etc.
import { useState, useEffect } from "react";
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]); // runs whenever userId changes
if (!user) return <p>Loading...</p>;
return <p>Hello, {user.name}!</p>;
}
The second argument ([userId]) is the dependency array. The effect re-runs whenever any value in that array changes.
Watch out: passing an empty array
[]means the effect runs once (on mount). Leaving it out means it runs after every render - usually not what you want.
3. useRef - Persistent Values Without Re-renders
useRef gives you a mutable box that persists across renders but doesn't cause re-renders when changed. Classic use case: DOM references.
import { useRef } from "react";
function FocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<div>
<input ref={inputRef} type="text" placeholder="Type here..." />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</div>
);
}
A Few Golden Rules
- Only call hooks at the top level - never inside
ifstatements or loops - Only call hooks from React functions - not regular JS functions
- Name your custom hooks starting with
use- so React can recognise them
Building a Custom Hook
Once you get comfortable with the basics, you can compose hooks into your own custom ones. This is where things get powerful:
function useLocalStorage(key: string, initialValue: string) {
const [value, setValue] = useState(
() => localStorage.getItem(key) ?? initialValue
);
const setStored = (newValue: string) => {
setValue(newValue);
localStorage.setItem(key, newValue);
};
return [value, setStored] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage("theme", "light");
Now you have a reusable piece of logic that any component can use. That's the real power of hooks - sharing logic without sharing UI.
You've Got the Foundation
Hooks aren't magic. They're just functions with a few special rules. Once useState, useEffect, and useRef feel natural, you'll be ready to explore useCallback, useMemo, useContext, and the rest of the family.
Take it one hook at a time. ⚛️
