Build Your First Custom Hook (That You’ll Actually Use)
Back when I first started writing React components, I treated hooks like little convenience tools. useState
, useEffect
, done. But as apps grew, my components turned into bloated messes. Like, 150+ lines of logic, state, effects, handlers—all crammed into a single function. And when I needed the same logic somewhere else? Copy-paste city. Then came custom hooks, and I realized: this is the separation of concerns React promised me. If you're tired of duplicating logic, debugging spaghetti, or explaining your component to your future self (who hates you), custom hooks are your new best friend.
Why Extract Logic? (And Your Future Self Will Thank You)
Ever written something like this?
function SignupForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isDirty, setIsDirty] = useState(false);
const [errors, setErrors] = useState({});
useEffect(() => {
// some validation or tracking logic
}, [email, password]);
// ...more logic, handlers, state
}
Now imagine needing similar logic in LoginForm
, ResetPasswordForm
, and ProfileForm
.
Copy. Paste. Cry.
This is where custom hooks shine. They let you extract logic, reuse it cleanly, test it independently, and stop bloating your components with 20 useState
s. You’re basically turning shared behaviors into their own little units—the Lego bricks of your UI logic.
If you've ever thought “ugh, I already wrote this once,” it's probably time to extract a hook.
Anatomy of a Custom Hook (It’s Just a Function… with Superpowers)
A custom hook is just a function that starts with use
and can call other hooks inside it.
Let’s say we want to extract some input logic:
function useInput(initialValue = '') {
const [value, setValue] = useState(initialValue);
const onChange = (e) => setValue(e.target.value);
const reset = () => setValue(initialValue);
return { value, onChange, reset };
}
Then inside your form:
function SignupForm() {
const email = useInput('');
const password = useInput('');
return (
<>
<input type="email" {...email} />
<input type="password" {...password} />
<button onClick={email.reset}>Clear Email</button>
</>
);
}
No more repetitive state declarations or handlers. It’s clean. It’s readable. And if you ever need to tweak the behavior (say, add validation), you do it once inside useInput
.
Bonus: It makes your component look like what it actually is—a form—not a state manager.
useInput: A Practical Hook You’ll Reuse Over and Over
Let’s break it down.
1. State inside the hook:
const [value, setValue] = useState(initialValue);
Each input field gets its own independent state.
2. Handler logic bundled in:
const onChange = (e) => setValue(e.target.value);
You don’t have to define this in every component again. You get it for free.
3. Reset helper:
const reset = () => setValue(initialValue);
This small extra saves a ton of UI headaches.
4. Return a usable object:
return { value, onChange, reset };
This lets you spread it directly into your <input {...input} />
without wiring up props manually.
The result? Super reusable, super readable. I’ve used variations of useInput
in every React project I’ve worked on in the last five years.
Hook Naming Conventions and Dev Ergonomics (aka Don't Be a Goblin)
Please don’t name your hook useThing
or useStuffLogic
.
Here’s a rule of thumb I live by:
`use` prefix is non-negotiable. The name should describe what it gives you or manages. * Keep it short, but meaningful.
Bad:
useCrazyInputStateHandlerLogic
Better:
useInput
, useForm
, useAuth
, useToggle
, usePagination
Also, return objects, not arrays, unless the order is obvious (like [data, setData]
). Objects make your hook’s API self-documenting:
const { value, onChange, reset } = useInput();
Ergonomic, predictable, readable.
And if you're feeling spicy? Add prop types or JSDoc for your hook to make it dev-friendly when you come back after a 3-month break (aka a lifetime in startup time).
Where Beginners Mess This Up (Trust Me, I Did Too)
The biggest trap? Putting side effects directly inside custom hooks without considering dependencies.
I once wrote this in a custom data-fetching hook:
useEffect(() => {
fetchData();
}, []);
Then used it like this:
const data = useMyFetchHook(query); // but query changed!
Boom. The hook only ran once. But the query
changed later. No update. No re-fetch. Silent bug.
Lesson? Be explicit. If your hook takes a parameter that should re-run the effect, include it in the useEffect
dependency array. Or even better—let the consumer call a refetch
method manually.
And please: don’t call hooks conditionally inside your custom hooks. That breaks the Rules of Hooks, and the React gods will smite you.
Ready to take this one step further and start composing hooks into powerful reusable libraries? Let’s jump into: Hook Composition: Building Libraries from Hooks