Hooks Explained (ReactJS)
React hooks are some usable methods which help in building applications faster by managing state and controlling the lifecycle of components.
Hooks were introduced in React 16.8. Before that, developers mostly used class-based components to manage state and lifecycle events. That approach worked, but it made code harder to read, reuse, and maintain.
What are hooks?
Hooks are utility functions which help users to create state and manage component lifecycle without writing much code.
With hooks, you can:
- Store and update state
- Run logic when a component mounts or updates
- Clean up resources when a component unmounts
- Reuse logic across components
In simple words, hooks let you control the lifecycle of a component and its data using
small, readable functions.
Commonly Used Hooks
We are going to learn about the most commonly used hooks:
useStateuseRefuseEffectuseLayoutEffectuseMemouseCallbackuseContext
There are more hooks provided by React, but they are either used rarely or only in special scenarios.
useState
useState is used to manage dynamic data inside a component. When the state value changes, React automatically re-renders the component to reflect the UI changes.
const [show, setShow] = useState<boolean>(false);const [show, setShow] = useState<boolean>(false);
useState returns two values:
- The current state value
- A setter function to update the state value
Example: Toggle UI using useState
function ToggleButton() {const [show, setShow] = useState<boolean>(false);function toggleShow() {setShow((prev) => !prev);}return (<><button onClick={toggleShow}>Toggle Show</button>{show ? <div>Show is ON</div> : null}</>);}function ToggleButton() {const [show, setShow] = useState<boolean>(false);function toggleShow() {setShow((prev) => !prev);}return (<><button onClick={toggleShow}>Toggle Show</button>{show ? <div>Show is ON</div> : null}</>);}
Important Things to Understand About State
- State can store string, number, boolean, object, or array
- Updating state triggers a re-render
- State updates are asynchronous
- Never update state directly (always use the updater function)
❌ Wrong way
show = true;show = true;
This updates the variable but does not re-render the component.
✅ Correct way
setShow(true);setShow(true);
This notifies React to update the UI.
useRef
useRef is a hook which helps to hold the same values across all re-renders of a component. It also allows us to access the DOM for more manipulation.
Common use cases:
- Access DOM elements
- Store timers or intervals
- Keep previous values
- Hold mutable values across renders
const inputRef = useRef<HTMLInputElement>(null);const inputRef = useRef<HTMLInputElement>(null);
Accessing DOM Using useRef
function FocusInput() {const inputRef = useRef<HTMLInputElement>(null);function focusInput() {inputRef.current?.focus();}return (<><input ref={inputRef} /><button onClick={focusInput}>Focus Input</button></>);}function FocusInput() {const inputRef = useRef<HTMLInputElement>(null);function focusInput() {inputRef.current?.focus();}return (<><input ref={inputRef} /><button onClick={focusInput}>Focus Input</button></>);}
Why useRef Is Important
- Value stays the same across re-renders
- Updating
.currentdoes NOT cause re-render - Perfect for storing values that should not affect UI
Note:
useRefis not just for DOM access. Want to learn more about useRef hook in detail? Check it out here.
useEffect
useEffect is a hook that helps us run code based on the lifecycle of a component.
Using useEffect, we can perform actions when:
- A component is mounted (added to the UI)
- A component is unmounted (removed from the UI)
- A value changes and we want to react to that change
Common use cases include API calls, logging, timers, subscriptions, and validations.
Basic Example: Component Mount
useEffect(() => {console.log("My component mounted");}, []);useEffect(() => {console.log("My component mounted");}, []);
In this example:
- The callback function runs only once
- The empty array
[]means the effect has no dependencies - This runs after the component is rendered for the first time
Important: useEffect always runs after the UI is painted on the DOM.
That means the user sees the UI first, then this code runs.
Dependency Array Explained
useEffect accepts two parameters:
- A callback function
- A dependency array
The dependency array tells React when the effect should run.
const [username, setUsername] = useState("");useEffect(() => {checkUserNameAvailable(username);}, [username]);const [username, setUsername] = useState("");useEffect(() => {checkUserNameAvailable(username);}, [username]);
In this example:
- The effect runs when the component mounts
- The effect runs again every time
usernamechanges - If
usernamedoes not change, the effect does not run
This is useful when you want to perform checks or actions based on value changes.
Different Dependency Behaviours
useEffect(() => {// runs on every render});useEffect(() => {// runs on every render});
useEffect(() => {// runs only once (on mount)}, []);useEffect(() => {// runs only once (on mount)}, []);
useEffect(() => {// runs when value changes}, [value]);useEffect(() => {// runs when value changes}, [value]);
The above part is the most important thing to understand about useEffect. If
you don't understand these, your app may not be performant and optimized.
Cleanup in useEffect (Very Important)
Some effects create things that must be cleaned up later, such as:
setIntervalsetTimeout- Event listeners
- Subscriptions
If you don't clean them up, you will cause memory leaks.
Example: Using Cleanup with Interval
const timerRef = useRef<number | null>(null);useEffect(() => {// store interval id in ref so it stays the same across renderstimerRef.current = window.setInterval(() => {console.log("performing some action every second");}, 1000);// cleanup functionreturn () => {if (timerRef.current) {clearInterval(timerRef.current);}};}, []);const timerRef = useRef<number | null>(null);useEffect(() => {// store interval id in ref so it stays the same across renderstimerRef.current = window.setInterval(() => {console.log("performing some action every second");}, 1000);// cleanup functionreturn () => {if (timerRef.current) {clearInterval(timerRef.current);}};}, []);
What is happening here:
- The interval starts when the component mounts
- The interval runs every second
- When the component unmounts, the cleanup function runs
- The interval is cleared to avoid memory leaks
The function returned inside useEffect is called the cleanup function.
Why useRef Is Used Here
- We store the interval ID in
useRef useRefdoes not cause re-render- The value stays the same across renders
- This makes cleanup safe and predictable
Using
useStatehere would be wrong.
Common Mistakes with useEffect
- Running effects without dependency array
- Adding unnecessary dependencies
- Forgetting cleanup
- Using
useEffectfor things that don't need side effects
useLayoutEffect
It works almost the same way as useEffect, but there is one critical difference.
👉 useLayoutEffect runs before the browser paints the UI on the screen.
How useLayoutEffect Works
The flow looks like this:
- React renders the component
useLayoutEffectruns- Browser paints the UI
- User sees the UI
In comparison, useEffect runs after step 3.
This means useLayoutEffect can block the UI paint until it finishes.
Why Does This Matter?
Because sometimes you need to:
- Measure DOM size
- Read layout values
- Adjust UI before the user sees it
If you do this in useEffect, the user might see a flash, jump, or broken layout.
Example: Measuring DOM Size
function Box() {const boxRef = useRef<HTMLDivElement>(null);useLayoutEffect(() => {const height = boxRef.current?.getBoundingClientRect().height;console.log("box height:", height);}, []);return <div ref={boxRef}>Hello</div>;}function Box() {const boxRef = useRef<HTMLDivElement>(null);useLayoutEffect(() => {const height = boxRef.current?.getBoundingClientRect().height;console.log("box height:", height);}, []);return <div ref={boxRef}>Hello</div>;}
Here:
- The DOM element already exists
- Layout is measured before paint
- The user never sees incorrect UI
useLayoutEffect runs before the browser paints the UI, which makes it useful
for layout-related logic like measuring elements or fixing UI flicker. Since
it blocks rendering, it should be used carefully. If useEffect works, prefer
that.
useMemo
useMemo is used to cache computed values. It avoids running expensive calculations on every render.
const result = useMemo(() => {return heavyCalculation(value);}, [value]);const result = useMemo(() => {return heavyCalculation(value);}, [value]);
The calculation runs only when value changes.
- • Expensive calculations
- • Derived values
- • Performance-sensitive components
- • Simple calculations
- • Small components
- • Just because you saw it in a tutorial
useMemoexists to avoid expensive recalculations, not to make code look optimized. In React 19, the compiler often handles this for you, so manual memoization is less common. If a calculation is cheap or easy to read, caching it only adds noise.
useCallback
useCallback is used to cache functions. By default, functions are recreated on every render.
const handleClick = useCallback(() => {doSomething();}, []);const handleClick = useCallback(() => {doSomething();}, []);
Why useCallback Exists
When passing functions to child components, unnecessary re-renders can happen. useCallback helps avoid that.
Use It When
- Passing callbacks to memoized components
- Working with dependency-heavy effects
useCallbackis useful only when function identity actually matters, such as with memoized child components. With React 19 and the compiler, many cases no longer need manual wrapping. If addinguseCallbackdoesn't fix a real re-render problem, it's doing nothing.
Important Note (React 19)
In React 19, when you use the React Compiler, React can automatically optimize your code by memoizing functions and values for you.
This means in many cases you do not need to manually use useCallback or useMemo.
React analyzes your code and applies memoization at build time.
If you do not want React to memoize a specific function or value, you can disable it using a directive. Add the following line at the top of your function or file:
"use no memo";"use no memo";
useContext
useContext is used to share data between components without passing props manually at every level.
Normally in React, data flows from parent to child using props.
When your component tree grows deep, this leads to prop drilling. useContext solves this problem.
What Is Prop Drilling?
Prop drillingIn the diagram above:
- Component A owns the data (for example, user details)
- That data is passed to Component B
- Component B passes the same data again to Component C and Component D
Only Component C or Component D actually needs the data. Component B does not use it at all.
This is called prop drilling.
How useContext Helps
With useContext, you can:
- Store shared data in one place
- Access it from any component
- Avoid passing props through every layer
This is useful for:
- Authentication data
- Theme (dark / light)
- Language settings
- Global configuration
Step 1: Create a Context
import { createContext } from "react";const UserContext = createContext(null);import { createContext } from "react";const UserContext = createContext(null);
This creates a context object.
Step 2: Provide the Context
Wrap your component tree using a Provider.
function App() {const user = { name: "Yagyaraj" };return (<UserContext.Provider value={user}><Layout /></UserContext.Provider>);}function App() {const user = { name: "Yagyaraj" };return (<UserContext.Provider value={user}><Layout /></UserContext.Provider>);}
Now, every component inside Layout can access user.
Step 3: Consume the Context Using useContext
import { useContext } from "react";function UserProfile() {const user = useContext(UserContext);return <div>Hello {user.name}</div>;}import { useContext } from "react";function UserProfile() {const user = useContext(UserContext);return <div>Hello {user.name}</div>;}
No props. No drilling. Direct access.
Important Rule About useContext
Whenever the context value changes, all components using that context re-render. This is critical. If you put frequently changing data in context, you can hurt performance.
When useContext Is a Good Choice
Use useContext when:
- Data is truly global (like selected theme, logged-in user details)
- Many pages or components need the same data across the app
- Prop drilling is becoming painful
If only one or two components need the value, use props.
Combining useContext With useState
Most real apps use them together.
const ThemeContext = createContext(null);function ThemeProvider({ children }) {const [theme, setTheme] = useState("light");return (<ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>);}const ThemeContext = createContext(null);function ThemeProvider({ children }) {const [theme, setTheme] = useState("light");return (<ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>);}
Now you have global state with controlled updates.
Common Mistakes With useContext
- Using context for everything
- Putting large objects in context
- Updating context too often
- Forgetting that it causes re-renders
useContextremoves prop drilling for shared data, but it should be used only for truly global data. For complex or frequently changing state, tools like Redux or Zustand are usually a better choice.
Learn More
- React Hook Rules: https://react.dev/reference/rules/rules-of-hooks
Final Thoughts
React hooks are powerful, but misuse causes bugs and performance issues. Understand them first, trust the compiler, and keep your code simple.