React Hooks Simplified

Published on January 17, 2026

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:

  • useState
  • useRef
  • useEffect
  • useLayoutEffect
  • useMemo
  • useCallback
  • useContext

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.

ts
const [show, setShow] = useState<boolean>(false);

useState returns two values:

  1. The current state value
  2. A setter function to update the state value

Example: Toggle UI using useState

tsx
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

ts
show = true;

This updates the variable but does not re-render the component.

✅ Correct way

ts
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
ts
const inputRef = useRef<HTMLInputElement>(null);

Accessing DOM Using useRef

tsx
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 .current does NOT cause re-render
  • Perfect for storing values that should not affect UI

Note: useRef is 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

tsx
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:

  1. A callback function
  2. A dependency array

The dependency array tells React when the effect should run.

tsx
const [username, setUsername] = useState("");
useEffect(() => {
checkUserNameAvailable(username);
}, [username]);

In this example:

  • The effect runs when the component mounts
  • The effect runs again every time username changes
  • If username does 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

tsx
useEffect(() => {
// runs on every render
});
tsx
useEffect(() => {
// runs only once (on mount)
}, []);
tsx
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:

  • setInterval
  • setTimeout
  • Event listeners
  • Subscriptions

If you don't clean them up, you will cause memory leaks.

Example: Using Cleanup with Interval

tsx
const timerRef = useRef<number | null>(null);
useEffect(() => {
// store interval id in ref so it stays the same across renders
timerRef.current = window.setInterval(() => {
console.log("performing some action every second");
}, 1000);
// cleanup function
return () => {
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
  • useRef does not cause re-render
  • The value stays the same across renders
  • This makes cleanup safe and predictable

Using useState here would be wrong.

Common Mistakes with useEffect

  • Running effects without dependency array
  • Adding unnecessary dependencies
  • Forgetting cleanup
  • Using useEffect for 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:

  1. React renders the component
  2. useLayoutEffect runs
  3. Browser paints the UI
  4. 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

tsx
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.

tsx
const result = useMemo(() => {
return heavyCalculation(value);
}, [value]);

The calculation runs only when value changes.


✅ When to Use
  • • Expensive calculations
  • • Derived values
  • • Performance-sensitive components
❌ When NOT to Use
  • • Simple calculations
  • • Small components
  • • Just because you saw it in a tutorial

useMemo exists 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.

tsx
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

useCallback is 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 adding useCallback doesn'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:

ts
"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 drillingProp drilling

In 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

ts
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.

tsx
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

tsx
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.

tsx
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

useContext removes 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


Final Thoughts

React hooks are powerful, but misuse causes bugs and performance issues. Understand them first, trust the compiler, and keep your code simple.


linkedin | github | twitter