React Hooks are functions that let you use state and other React features without writing a class. They were introduced in React 16.8 to simplify state management and side effects in functional components.
Core React Hooks Explained
useState: Managing State in Functional Components
So, you’re juggling multiple tasks, maybe sipping coffee, and trying to keep track of a counter in your app. That’s where
useState
comes in. It’s like having a sticky note that updates itself. You declare a state variable and a function
to update it. Every time you call the updater, React re-renders the component with the new state. Simple, right? It’s perfect
for handling form inputs, toggles, or any value that changes over time.
- Easy to implement — no classes, just functions and clean code.
- Useful in forms — name, email, checkbox states, all covered.
- Supports multiple state variables — not limited to one per component.
- Pairs well with other hooks like
useEffect
for more dynamic behavior. - Encourages functional, stateless design — simpler to debug.
And the syntax? Barebones. Short. You’ll probably memorize it accidentally after using it twice. No setup drama. Just:
const [count, setCount] = useState(0);
Yeah, that’s it. Then just update setCount
whenever something changes. Done. No lifecycle methods to worry about.
React just… handles it. Feels like cheating, kind of.
The useState
hook allows you to add state to functional components.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Each time the button is clicked, the count increases by one.
useEffect: Handling Side Effects
Mid-scroll through emails, you’re also building a dashboard and — wait, did you forget the API call? Yep. This is where
useEffect
swoops in. Whether it’s fetching data, syncing with localStorage, setting up a listener, or doing
something DOM-ish like changing the title bar — these are all side effects.
useEffect
kicks in after your component renders. You don’t block the UI — React renders first, then handles your
background stuff. You also get to clean things up before it runs again or before the component unmounts. So no memory leaks,
hopefully.
- Runs automatically after render — no need to manually trigger anything.
- Dependency array keeps things efficient — it only reruns when needed.
- Perfect for fetching data, event listeners, subscriptions, timers… whatever’s not pure UI.
- Cleanup function avoids ghosts from past renders — ideal for removing event listeners or timers.
useEffect(() => {
const interval = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(interval);
}, []);
Feels a bit like setting a reminder in your app that keeps repeating until you tell it to stop. Quietly powerful. Also,
depending on how you use the dependency array — it might fire once, or every time something changes. Dangerous? Nah.
Just precise.
The useEffect
hook lets you perform side effects in function components, such as data fetching or manual DOM manipulations.
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <p>Timer: {seconds}s</p>;
}
This sets up a timer that increments every second and cleans up on unmount.
useReducer: Managing Complex State Logic
You start with one input field, then add a checkbox, then a multi-step form — and suddenly your useState
setup
looks like spaghetti. That’s when useReducer
steps in, calmly. It’s built for when state gets… layered.
It’s like Redux-lite. You write a reducer function that takes the current state and an action, then returns a new state.
React keeps it all neat and predictable. No more tangled update logic across multiple setState
calls.
- Cleaner logic for complex state — especially when one update depends on the previous one.
- Organized — actions and state are centralized, readable.
- Good for multi-field forms, modals, toggles, and anything with steps or flows.
- Can be extended easily — works with middleware or external state management too.
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'reset':
return { count: 0 };
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, { count: 0 });
You dispatch actions instead of calling setters directly. It feels a bit more structured — like there’s a process.
Kind of nice when your logic’s getting messy and you’ve got tabs open everywhere, half-paying attention.
useReducer
just helps things behave.
The useReducer
hook is an alternative to useState
for managing complex state logic.
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
</div>
);
}
Ideal for managing state transitions in a predictable way.
useCallback: Memoizing Functions
Okay, so you’re passing a function to a child component… again. And somehow, things keep re-rendering when they shouldn’t.
That’s where useCallback
saves your sanity. It memorizes your function so React doesn’t rebuild it on every render.
Which sounds minor until you’ve got a component re-rendering 37 times just because a prop function changed… slightly.
useCallback
takes a function and an array of dependencies. If none of the dependencies change, the function
reference stays the same. So, your deeply nested child component that depends on that prop? It chills.
- Reduces unnecessary re-renders in child components.
- Useful when passing callbacks to memoized components (like with
React.memo
). - Keeps function references stable across renders.
- Especially helpful with expensive operations wrapped in
useEffect
oruseMemo
.
const handleClick = useCallback(() => {
doSomething();
}, [dependency]);
Is it overkill sometimes? Yeah. But in large apps or performance-heavy components, it’s a subtle win.
Quiet optimization. Less drama. You’ll hardly notice it — which is kind of the point.
The useCallback
hook returns a memoized version of the callback that only changes if one of the dependencies has changed.
import React, { useState, useCallback } from 'react';
function ExpensiveComponent({ onClick }) {
// ...expensive rendering
}
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return <ExpensiveComponent onClick={handleClick} />;
}
Helps prevent unnecessary re-renders of child components.
Comparison Table
Hook | Purpose | Common Use Case |
---|---|---|
useState | Manage state in functional components | Form inputs, toggles |
useEffect | Handle side effects | Data fetching, subscriptions |
useContext | Access context values | Theming, user authentication |
useRef | Persist values across renders | Accessing DOM elements |
useReducer | Manage complex state logic | State transitions, forms |
useCallback | Memoize functions | Optimizing child component renders |
FAQs
- When should I use useReducer over useState?
- Use
useReducer
when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. - Can I use multiple hooks in a single component?
- Yes, you can use multiple hooks in a single component. Just ensure they are called in the same order on every render.
- What is the difference between useEffect and useLayoutEffect?
useEffect
runs after the render is committed to the screen, whereasuseLayoutEffect
runs synchronously after all DOM mutations but before the browser has painted.