Feb 20, 2024Last modified May 16, 2025

Deep dive on react hooks

React hooks are basically functions that expose some reusable utility for other functional components. Hooks were introduced in React 16.8.

Why we needed hooks ?

  • Class components were verbose and used to become hard to understand as they grew in size.
  • To simplify common logic sharing between components and avoiding the need for higher order components or wrapper components.

How are hooks implemented by react internally ?

Hooks internally operate in 3 phases:

  • Mount: Creates hook objects
  • Update: Reuses existing hooks
  • Effect: Handles side-effects after render

Lets look at some of the patterns and fundamental concepts that react hooks use internally.

  1. Closures

As you are aware, closures are functions who have access to variables in their lexical scope even after the function has returned.

Hooks use closures to store state values and other data. When you call a hook inside a component, React internally creates a closure that captures the state variable and its updater function. This closure ensures that the state persists between renders and is scoped to the specific component instance.

  1. Linked list storage

React uses linked list to track the order in which the hooks were called during component render. This linked list is attached to the component's fiber nodes.

Each time a component renders, React iterates through this list, matching up hook calls in the same order. This design means that hooks must always be called in the same order on every render, which is why conditional or looped hook calls are disallowed.

Fiber Node
β”‚
β”œβ”€β”€ memoizedState (Hooks linked list)
β”‚ β”‚
β”‚ β”œβ”€β”€ Hook #1 (useState)
β”‚ β”‚ β”œβ”€β”€ memoizedState: value
β”‚ β”‚ β”œβ”€β”€ next: β†’ Hook #2
β”‚ β”‚
β”‚ β”œβ”€β”€ Hook #2 (useEffect)
β”‚ β”‚ β”œβ”€β”€ memoizedState: effect
β”‚ β”‚ β”œβ”€β”€ next: β†’ Hook #3
β”‚ β”‚
β”‚ β”œβ”€β”€ ...
Fiber Node
β”‚
β”œβ”€β”€ memoizedState (Hooks linked list)
β”‚ β”‚
β”‚ β”œβ”€β”€ Hook #1 (useState)
β”‚ β”‚ β”œβ”€β”€ memoizedState: value
β”‚ β”‚ β”œβ”€β”€ next: β†’ Hook #2
β”‚ β”‚
β”‚ β”œβ”€β”€ Hook #2 (useEffect)
β”‚ β”‚ β”œβ”€β”€ memoizedState: effect
β”‚ β”‚ β”œβ”€β”€ next: β†’ Hook #3
β”‚ β”‚
β”‚ β”œβ”€β”€ ...
  1. Function composition

Hooks are just functions, which enables the creation of custom hooks. This promotes code reuse and modularity. Custom hooks encapsulate reusable logic and can call other hooks internally, allowing developers to compose complex behaviors from simple, testable units

  1. Queue-based update

Hooks like useEffect are managed by storing effect descriptors in a queue (often called updateQueue) on the fiber. Each effect has properties such as create (the effect callback), destroy (cleanup function), inputs (dependency array), and next (pointer to the next effect). React schedules and executes these effects after rendering, based on changes in the dependencies

Building custom hooks

  1. Detect device type (mobile or desktop)
import { useState, useEffect } from 'react';

export function useDeviceDetect() {
const [isMobile, setIsMobile] = useState(false);

useEffect(() => {
// Basic mobile detection using user agent
const userAgent = typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
const mobile = Boolean(
userAgent.match(
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
)
);
setIsMobile(mobile);
}, []);

return { isMobile };
}

// Usage example:
function Component() {
const { isMobile } = useDeviceDetect();

return (
<div>
{isMobile ? 'Mobile View' : 'Desktop View'}
</div>
);
}
import { useState, useEffect } from 'react';

export function useDeviceDetect() {
const [isMobile, setIsMobile] = useState(false);

useEffect(() => {
// Basic mobile detection using user agent
const userAgent = typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
const mobile = Boolean(
userAgent.match(
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
)
);
setIsMobile(mobile);
}, []);

return { isMobile };
}

// Usage example:
function Component() {
const { isMobile } = useDeviceDetect();

return (
<div>
{isMobile ? 'Mobile View' : 'Desktop View'}
</div>
);
}
  1. Fetch OS theme
import { useState, useEffect } from 'react';

export function useSystemTheme() {
const [systemTheme, setSystemTheme] = useState(() => {
// Initialize with current preference
if (typeof window === 'undefined') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
});

useEffect(() => {
if (typeof window === 'undefined') return;

const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

const handleChange = (e) => {
setSystemTheme(e.matches ? 'dark' : 'light');
};

// Add listener
mediaQuery.addEventListener('change', handleChange);

// Cleanup
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);

return systemTheme;
}

// Usage example:
function ThemedComponent() {
const systemTheme = useSystemTheme();

return (
<div className={systemTheme === 'dark' ? 'dark-theme' : 'light-theme'}>
Current system theme: {systemTheme}
</div>
);
}
import { useState, useEffect } from 'react';

export function useSystemTheme() {
const [systemTheme, setSystemTheme] = useState(() => {
// Initialize with current preference
if (typeof window === 'undefined') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
});

useEffect(() => {
if (typeof window === 'undefined') return;

const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

const handleChange = (e) => {
setSystemTheme(e.matches ? 'dark' : 'light');
};

// Add listener
mediaQuery.addEventListener('change', handleChange);

// Cleanup
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);

return systemTheme;
}

// Usage example:
function ThemedComponent() {
const systemTheme = useSystemTheme();

return (
<div className={systemTheme === 'dark' ? 'dark-theme' : 'light-theme'}>
Current system theme: {systemTheme}
</div>
);
}

Using hooks to improve performance

You can use the combination of useCallback hook and React.memo to improve rendering-performance of components by reducing unnecesary renders.

As you might know, a child component is re-rendered in two scenarios -

  1. When its state or props changes
  2. When its parents are re-rendered.

Now a lot times you might pass some state functions from the parent to the child components. Whenever the parent re renders the function reference in such scenarios gets recreated, requiring the child components to be re-rendered.

You can see this by using the react-devtools in browser.

Solution : The way to optimize these re-renders is through the usage of useCallback + React.memo().

  1. Memoize the function in the parent with useCallback to preserve its identity across renders.
  2. Memoize the child component with React.memo to prevent re-renders when props don’t change.

Lets look at an example -

// parent component
import { useState, useCallback } from 'react';
import Child from './Child';

function Parent() {
const [count, setCount] = useState(0);

// Memoize the increment function
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []); // No dependencies (stable function)

return (
<div>
<p>Parent Count: {count}</p>
<Child onIncrement={increment} />
</div>
);
}

// child component
import { memo } from 'react';

const Child = memo(({ onIncrement }) => {
console.log('Child rendered!'); // Only logs when props change
return <button onClick={onIncrement}>Increment from Child</button>;
});
// parent component
import { useState, useCallback } from 'react';
import Child from './Child';

function Parent() {
const [count, setCount] = useState(0);

// Memoize the increment function
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []); // No dependencies (stable function)

return (
<div>
<p>Parent Count: {count}</p>
<Child onIncrement={increment} />
</div>
);
}

// child component
import { memo } from 'react';

const Child = memo(({ onIncrement }) => {
console.log('Child rendered!'); // Only logs when props change
return <button onClick={onIncrement}>Increment from Child</button>;
});