React Hooks are functions that let you use state and other React features in functional components. Before Hooks, you had to use class components to access features like state and lifecycle methods. Hooks changed everything by making functional components powerful enough to handle any React feature.
What Are Hooks?
Hooks are built-in functions provided by React that let you “hook into” React features. They follow a simple naming convention—all hooks start with the word “use” (like useState, useEffect, useContext). Hooks can only be used inside functional components and at the top level, not inside loops, conditions, or nested functions.
useState Hook
Purpose: Manage component state in functional components.
What it does: Stores data that can change. When data changes, the component updates automatically.
Syntax:
const [count, setCount] = useState(0);
count = current value | setCount = function to change it | 0 = initial value
Example:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
When to use: Any time you need data that changes and updates the UI.
useEffect Hook
Purpose: Run side effects after the component renders.
Side effects are actions like fetching data from an API, subscribing to events, setting timers, or updating the document title. useEffect lets you perform these actions after React has updated the DOM.
How it works:
useEffect(() => {
// This code runs after render
console.log('Component rendered!');
}, []);
The function inside useEffect is called the effect. The dependency array (second argument) controls when the effect runs. An empty array [] means run only once after the first render. If you omit the array, the effect runs after every render. If you include variables, the effect runs only when those variables change.
Example:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]); // Run when userId changes
return user ? <h1>{user.name}</h1> : <p>Loading...</p>;
}
Cleanup function: Sometimes you need to clean up after an effect. For example, if you subscribe to an event listener, you should unsubscribe when the component unmounts. Return a function from your effect to handle cleanup.
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
When to use: For API calls, setting up timers, updating document title, or any action that happens outside the React component.
useContext Hook
Purpose: Access shared data without passing props through multiple components.
Props drilling happens when you need to pass data through many intermediate components just to reach a child component that needs it. useContext lets you skip all those intermediate components.
How it works:
const ThemeContext = React.createContext();
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Header />
<Content />
<Footer />
</ThemeContext.Provider>
);
}
function Header() {
const theme = useContext(ThemeContext);
return <div className={theme}>Header</div>;
}
You create a context with createContext(), provide a value at a high level with Provider, and access it in any child component using useContext().
When to use: For global state like user authentication, app theme, language preferences, or any data needed by many components scattered throughout your app.
useReducer Hook
Purpose: Manage complex state logic with multiple related updates.
If your component has multiple state variables that update together, or if the next state depends on the previous one, useReducer is a better choice than useState. It’s similar to Redux but built into React.
How it works:
const initialState = { count: 0 };
function reducer(state, action) {
switch(action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
}
The reducer function takes current state and an action, then returns the new state. You dispatch actions to trigger state changes.
When to use: When managing form state with multiple fields, managing a list of items with add/remove/update operations, or any complex state logic.
useRef Hook
Purpose: Access DOM elements directly or store mutable values that don’t cause re-renders.
useRef returns an object with a .current property that persists across renders. Unlike state, changing a ref doesn’t trigger a re-render.
How it works:
function TextInput() {
const inputRef = useRef();
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>Focus Input</button>
</>
);
}
Example with storing values:
function Timer() {
const countRef = useRef(0);
const increment = () => {
countRef.current += 1;
console.log('Clicked:', countRef.current);
};
return <button onClick={increment}>Click me</button>;
}
When to use: To manage focus on input elements, trigger animations, integrate with third-party DOM libraries, or store values that change but shouldn’t cause re-renders.
useMemo Hook
Purpose: Memoize (cache) expensive calculations to improve performance.
If a component performs a heavy calculation on every render, it can slow things down. useMemo caches the result and only recalculates when dependencies change.
How it works:
function ExpensiveComponent({ items }) {
const sortedItems = useMemo(() => {
console.log('Sorting...');
return items.sort((a, b) => a - b);
}, [items]); // Only re-sort when items change
return <div>{sortedItems.map(item => <p>{item}</p>)}</div>;
}
The function inside useMemo runs only when dependencies change. Without useMemo, the sorting would happen on every render even if items didn’t change.
When to use: Only use useMemo for genuinely expensive calculations. Don’t overuse it—measuring performance first to confirm it’s actually slow is important. Most of the time, React is fast enough without it.
useCallback Hook
Purpose: Memoize (cache) functions so they maintain the same reference across renders.
When you define a function inside a component, a new function is created on every render. If you pass this function to child components or use it as a dependency in other hooks, it can cause unnecessary re-renders.
How it works:
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Clicked!');
}, []); // Same function reference always
return <Child onClick={handleClick} />;
}
Without useCallback, a new function would be created on every render, causing Child to re-render unnecessarily.
When to use: When passing functions to optimized child components that rely on reference equality, or when using functions as dependencies in useEffect or other hooks.
useId Hook
Purpose: Generate unique IDs for accessible form elements and lists.
Generating IDs manually can lead to duplicates or conflicts. useId generates unique IDs automatically that are stable across renders.
How it works:
function LoginForm() {
const emailId = useId();
const passwordId = useId();
return (
<>
<label htmlFor={emailId}>Email:</label>
<input id={emailId} type="email" />
<label htmlFor={passwordId}>Password:</label>
<input id={passwordId} type="password" />
</>
);
}
When to use: For accessible forms, linking labels to inputs, or any situation where you need unique identifiers.
useLayoutEffect Hook
Purpose: Run effects synchronously before the browser paints the screen.
useLayoutEffect is similar to useEffect but runs before the browser updates the DOM visually. This is useful when you need to measure or modify the DOM and see the changes immediately without a visual flicker.
How it works:
useLayoutEffect(() => {
// This runs before browser paint
const width = domNode.offsetWidth;
console.log('Width:', width);
}, []);
When to use: Rarely. Only use when you need measurements or DOM changes to happen before the browser paints, like calculating positions for tooltips or animations.
Best Practices for Using Hooks
Only call hooks at the top level: Don’t call hooks inside loops, conditions, or nested functions. React relies on the order of hook calls to track which state belongs to which hook.
Only call hooks from React functions: Call hooks from functional components or custom hooks, not from regular JavaScript functions.
Use the ESLint plugin: Install eslint-plugin-react-hooks to catch hook mistakes automatically.
Create custom hooks: You can combine multiple hooks into a custom hook to reuse logic across components.
function useUserData(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
return user;
}
Conclusion
React Hooks revolutionized how developers write React components. They make functional components powerful, reduce code complexity, and improve code reusability. By understanding when and how to use each hook, you can write cleaner, more maintainable React applications. Start with useState and useEffect, then gradually incorporate other hooks as your component logic becomes more complex.