Tutorials Logic, IN info@tutorialslogic.com

React Custom Hooks useFetch, useLocalStorage: Tutorial, Examples, FAQs & Interview Tips

React Custom Hooks useFetch, useLocalStorage

React in React is best learned by connecting the rule to an interactive form or modal. Start with the smallest component or hook, observe the output, and then add one realistic constraint so the concept becomes practical.

The key habit for this lesson is to watch props, state, and rendered JSX as it changes. That makes the topic easier to debug, easier to explain in interviews, and easier to use in real code without memorizing isolated syntax.

What Are Custom Hooks in React?

A custom hook is a JavaScript function whose name starts with use and which can call other React hooks such as useState, useEffect, useRef, or useContext. The main purpose of a custom hook is to extract reusable stateful logic from components.

Many React applications eventually contain repeated patterns such as fetching data, tracking online status, reading from local storage, debouncing user input, or handling form fields. If the same logic appears in multiple components, a custom hook allows you to move that logic into one reusable function and use it wherever needed.

A custom hook does not share rendered UI. It shares logic. Two components can use the same custom hook and still render completely different layouts.

Why Custom Hooks Are Useful

Custom hooks are one of the cleanest ways to organize React code. Instead of placing every effect, event listener, timer, and utility state directly inside components, you can move related logic into one small reusable unit.

  • They reduce repeated code across components
  • They keep components shorter and easier to read
  • They separate business logic from UI markup
  • They make complicated behavior easier to test and maintain
  • They help you create a consistent pattern across the application

Custom Hook vs Normal Function

If a function does not call React hooks, it is usually just a normal helper function. If it uses hooks and follows React hook rules, it should be written as a custom hook.

Feature Normal function Custom hook
Name Any valid function name Must start with use
Can call React hooks? No Yes
Main purpose General JavaScript logic Reusable React stateful logic
Used inside components Yes Yes
Knows about React lifecycle No Yes, through hooks like useEffect

Important Rules for Custom Hooks

A custom hook follows the same rules as built-in hooks:

React depends on hooks being called in the same order on every render. That is why these rules matter. A custom hook is not a special exception; it must follow the same behavior as any built-in hook.

  • Call hooks only at the top level of the hook
  • Do not call hooks inside loops, conditions, or nested functions
  • Call custom hooks only from React components or other custom hooks
  • Always start the hook name with use

The Problem Before Using a Custom Hook

Suppose two components need to detect whether the browser is online or offline. Without a custom hook, both components might repeat the same state and effect code.

Both components contain the same state and side-effect logic. This is a perfect situation for a custom hook.

Repeated Logic Without a Custom Hook

Repeated Logic Without a Custom Hook
import { useEffect, useState } from 'react'

function StatusBanner() {
    const [isOnline, setIsOnline] = useState(navigator.onLine)

    useEffect(() => {
        const goOnline = () => setIsOnline(true)
        const goOffline = () => setIsOnline(false)

        window.addEventListener('online', goOnline)
        window.addEventListener('offline', goOffline)

        return () => {
            window.removeEventListener('online', goOnline)
            window.removeEventListener('offline', goOffline)
        }
    }, [])

    return <p>{isOnline ? 'You are online' : 'You are offline'}</p>
}

The Problem Before Using a Custom Hook

The Problem Before Using a Custom Hook
import { useEffect, useState } from 'react'

function CheckoutButton() {
    const [isOnline, setIsOnline] = useState(navigator.onLine)

    useEffect(() => {
        const goOnline = () => setIsOnline(true)
        const goOffline = () => setIsOnline(false)

        window.addEventListener('online', goOnline)
        window.addEventListener('offline', goOffline)

        return () => {
            window.removeEventListener('online', goOnline)
            window.removeEventListener('offline', goOffline)
        }
    }, [])

    return (
        <button disabled={!isOnline}>
            {isOnline ? 'Place Order' : 'Offline'}
        </button>
    )
}

Extracting the Logic Into a Custom Hook

We can move the repeated logic into useOnlineStatus(). Then any component can ask for the current online status without rewriting the event listener code.

Notice what happened here: the components became much smaller, and the logic became reusable. That is the core benefit of custom hooks.

First Custom Hook Example

First Custom Hook Example
import { useEffect, useState } from 'react'

export function useOnlineStatus() {
    const [isOnline, setIsOnline] = useState(navigator.onLine)

    useEffect(() => {
        const goOnline = () => setIsOnline(true)
        const goOffline = () => setIsOnline(false)

        window.addEventListener('online', goOnline)
        window.addEventListener('offline', goOffline)

        return () => {
            window.removeEventListener('online', goOnline)
            window.removeEventListener('offline', goOffline)
        }
    }, [])

    return isOnline
}

Extracting the Logic Into a Custom Hook

Extracting the Logic Into a Custom Hook
import { useOnlineStatus } from './hooks/useOnlineStatus'

function StatusBanner() {
    const isOnline = useOnlineStatus()
    return <p>{isOnline ? 'You are online' : 'You are offline'}</p>
}

function CheckoutButton() {
    const isOnline = useOnlineStatus()

    return (
        <button disabled={!isOnline}>
            {isOnline ? 'Place Order' : 'Offline'}
        </button>
    )
}

How to Think About a Good Custom Hook

A good custom hook usually does one small job well. It takes inputs as arguments, uses internal state or effects when needed, and returns the values or functions that components need.

This makes a custom hook feel similar to a small reusable API for component logic.

  • Input: the data the hook needs, such as a URL, key name, or delay time
  • Internal logic: state, effects, refs, subscriptions, or calculations
  • Output: the value, object, or helper functions returned to the component

Example 1: Data Fetching with useFetch

Fetching data is one of the most common custom hook use cases. The hook can manage loading state, errors, cleanup, and the fetched result in one place.

The biggest advantage here is that the fetch logic lives in one place. If you later want to improve error handling, add retry support, or include headers, you can update the hook rather than changing many components.

Reusable Fetch Hook

Reusable Fetch Hook
import { useEffect, useState } from 'react'

export function useFetch(url) {
    const [data, setData] = useState(null)
    const [loading, setLoading] = useState(false)
    const [error, setError] = useState(null)

    useEffect(() => {
        if (!url) return

        const controller = new AbortController()

        async function loadData() {
            try {
                setLoading(true)
                setError(null)

                const response = await fetch(url, { signal: controller.signal })

                if (!response.ok) {
                    throw new Error(`Request failed with status ${response.status}`)
                }

                const json = await response.json()
                setData(json)
            } catch (err) {
                if (err.name !== 'AbortError') {
                    setError(err.message)
                }
            } finally {
                setLoading(false)
            }
        }

        loadData()

        return () => controller.abort()
    }, [url])

    return { data, loading, error }
}

Example 1: Data Fetching with useFetch

Example 1: Data Fetching with useFetch
import { useFetch } from './hooks/useFetch'

function UserList() {
    const { data: users, loading, error } = useFetch('/api/users')

    if (loading) return <p>Loading users...</p>
    if (error) return <p>Error: {error}</p>

    return (
        <ul>
            {users?.map(user => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    )
}

Example 1: Data Fetching with useFetch

Example 1: Data Fetching with useFetch
import { useFetch } from './hooks/useFetch'

function PostList() {
    const { data: posts, loading, error } = useFetch('/api/posts')

    if (loading) return <p>Loading posts...</p>
    if (error) return <p>Error: {error}</p>

    return <p>Total posts: {posts?.length ?? 0}</p>
}

Example 2: Persisting State with useLocalStorage

A custom hook can also connect React state to browser APIs. One common example is keeping a value in localStorage so it survives page refreshes.

This hook hides the storage details. The component simply works with a state-like API.

useLocalStorage Hook

useLocalStorage Hook
import { useEffect, useState } from 'react'

export function useLocalStorage(key, initialValue) {
    const [value, setValue] = useState(() => {
        try {
            const savedValue = localStorage.getItem(key)
            return savedValue ? JSON.parse(savedValue) : initialValue
        } catch {
            return initialValue
        }
    })

    useEffect(() => {
        try {
            localStorage.setItem(key, JSON.stringify(value))
        } catch (err) {
            console.error('Unable to save value:', err)
        }
    }, [key, value])

    const removeValue = () => {
        localStorage.removeItem(key)
        setValue(initialValue)
    }

    return [value, setValue, removeValue]
}

Example 2: Persisting State with useLocalStorage

Example 2: Persisting State with useLocalStorage
import { useLocalStorage } from './hooks/useLocalStorage'

function SettingsPanel() {
    const [theme, setTheme] = useLocalStorage('theme', 'light')
    const [fontSize, setFontSize, clearFontSize] = useLocalStorage('fontSize', 16)

    return (
        <div>
            <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
                Theme: {theme}
            </button>

            <input
                type="range"
                min={12}
                max={24}
                value={fontSize}
                onChange={e => setFontSize(Number(e.target.value))}
            />

            <p style={{ fontSize }}>Preview text size: {fontSize}px</p>
            <button onClick={clearFontSize}>Reset Font Size</button>
        </div>
    )
}

Example 3: Debouncing Search Input

Debouncing means delaying an update until the user stops typing for a short time. This is useful for search boxes, filters, and API requests.

The component still owns the input field, but the hook gives it a delayed version of the value. That makes the UI simpler and prevents repeated timer code in different components.

useDebounce Hook

useDebounce Hook
import { useEffect, useState } from 'react'

export function useDebounce(value, delay = 400) {
    const [debouncedValue, setDebouncedValue] = useState(value)

    useEffect(() => {
        const timer = setTimeout(() => {
            setDebouncedValue(value)
        }, delay)

        return () => clearTimeout(timer)
    }, [value, delay])

    return debouncedValue
}

Example 3: Debouncing Search Input

Example 3: Debouncing Search Input
import { useEffect, useState } from 'react'
import { useDebounce } from './hooks/useDebounce'

function SearchBox() {
    const [query, setQuery] = useState('')
    const debouncedQuery = useDebounce(query, 500)

    useEffect(() => {
        if (!debouncedQuery) return
        console.log('Run API search for:', debouncedQuery)
    }, [debouncedQuery])

    return (
        <div>
            <input
                value={query}
                onChange={e => setQuery(e.target.value)}
                placeholder="Search products"
            />
            <p>Immediate value: {query}</p>
            <p>Debounced value: {debouncedQuery}</p>
        </div>
    )
}

Example 4: Small Utility Hook with useToggle

Not every custom hook needs to be large. Some of the most useful hooks are tiny. A boolean toggle hook is a simple example.

useToggle Hook

useToggle Hook
import { useState } from 'react'

export function useToggle(initialValue = false) {
    const [value, setValue] = useState(initialValue)

    const toggle = () => setValue(current => !current)
    const setTrue = () => setValue(true)
    const setFalse = () => setValue(false)

    return { value, toggle, setTrue, setFalse }
}

Example 4: Small Utility Hook with useToggle

Example 4: Small Utility Hook with useToggle
import { useToggle } from './hooks/useToggle'

function ModalExample() {
    const { value: isOpen, toggle, setFalse } = useToggle(false)

    return (
        <div>
            <button onClick={toggle}>
                {isOpen ? 'Hide' : 'Show'} Modal
            </button>

            {isOpen && (
                <div className="modal">
                    <p>This modal uses a custom hook for state management.</p>
                    <button onClick={setFalse}>Close</button>
                </div>
            )}
        </div>
    )
}

Custom Hooks Can Return Anything Useful

A custom hook is just a function, so it can return any structure that makes sense:

Choose the return style that makes the hook easiest to understand. Arrays work well when the order is obvious. Objects work well when many named values are returned.

  • A single value, such as isOnline
  • An array, such as [value, setValue]
  • An object, such as { data, loading, error }
  • Helper functions, such as toggle, reset, or removeValue

Using One Custom Hook Inside Another

Custom hooks can call other custom hooks. This makes it possible to build higher-level logic from smaller reusable pieces.

This style is powerful because it lets you create app-specific hooks from smaller generic hooks.

Composing Custom Hooks

Composing Custom Hooks
import { useDebounce } from './useDebounce'
import { useFetch } from './useFetch'

export function useSearchResults(query) {
    const debouncedQuery = useDebounce(query, 500)
    const url = debouncedQuery ? `/api/search?q=${encodeURIComponent(debouncedQuery)}` : null

    const { data, loading, error } = useFetch(url)

    return {
        results: data ?? [],
        loading,
        error,
        debouncedQuery
    }
}

When You Should Create a Custom Hook

You do not need a custom hook for every tiny line of code. It usually makes sense to create one when:

If the logic is used only once and is already easy to read inside the component, you may not need a custom hook yet.

  • The same hook-based logic appears in more than one component
  • A component is becoming too large because logic and UI are mixed together
  • You want a cleaner abstraction around browser APIs, subscriptions, or effects
  • You want a consistent way to use logic across the project

Common Mistakes to Avoid

Mistake Why it is a problem Better approach
Naming it without use React hook rules and linting will not recognize it correctly Use names like useFetch or useToggle
Calling hooks conditionally Breaks hook order between renders Call hooks at the top level every time
Putting too much unrelated logic into one hook Makes the hook hard to reuse and maintain Keep each hook focused on one responsibility
Returning confusing values Consumers will misuse the hook Return a clear array or object with meaningful names
Using a custom hook only to wrap one line with no benefit Adds extra abstraction without improving readability Create hooks where they clearly improve reuse or clarity

Best Practices for Writing Better Custom Hooks

  • Keep the hook focused on a single concern
  • Use clear argument names and sensible defaults
  • Return an object when many outputs are involved
  • Hide repetitive implementation details from the component
  • Document unusual behavior through names and simple structure
  • Handle cleanup properly when using events, timers, or subscriptions
  • Prefer composition of small hooks instead of one very large hook

A Simple Mental Model

You can think of a component as answering the question, "What should the UI look like?" A custom hook answers the question, "How should this behavior work?" When you separate those concerns well, React code becomes much easier to read.

Summary

Custom hooks are one of the most important patterns in modern React. They allow you to extract reusable stateful logic, keep components small, and organize effects and state in a much cleaner way. Whether you are fetching data, synchronizing with local storage, debouncing user input, listening to browser events, or managing reusable UI behavior, a good custom hook can remove duplication and make your code easier to understand.

The key idea is simple: if multiple components need the same hook-based logic, move that logic into a function that starts with use, follow the rules of hooks, and return the values the component needs. Over time, custom hooks help a React codebase become more modular, readable, and maintainable.

Applied guide for React

Use React when the program needs a clear answer to a specific problem, not because the keyword looks familiar. In a real React task, first name the input, then name the transformation, then name the output. This small discipline shows whether the topic is being used correctly or only copied from an example.

A reliable practice flow is: create the smallest working component or hook, add one normal case, add one edge case such as missing, repeated, empty, or boundary input, and then confirm the result with React DevTools and test output. If the result surprises you, reduce the code until the behavior is visible again.

The most common trap here is copying the syntax before understanding the behavior. Avoid it by writing one sentence before the code that explains why React is the right choice. After the code runs, verify the lesson by doing this: change one input and explain the changed output.

  • Identify the exact problem solved by React.
  • Trace props, state, and rendered JSX before and after the main operation.
  • Keep one intentionally broken version and explain the fix.
  • Connect the example to an interactive form or modal so the idea feels concrete.
Key Takeaways
  • I can explain where React fits inside an interactive form or modal.
  • I can point to the exact props, state, and rendered JSX affected by this topic.
  • I tested a normal case and an edge case involving missing, repeated, empty, or boundary input.
  • I verified the result with React DevTools and test output instead of assuming it worked.
  • I can describe the main mistake: copying the syntax before understanding the behavior.
Common Mistakes to Avoid
WRONG Copying the syntax before understanding the behavior.
RIGHT Write the expected behavior first, then make the example prove it.
A one-line expectation turns the code from copied syntax into a testable idea.
WRONG Practicing only the perfect input.
RIGHT Also test missing, repeated, empty, or boundary input before considering the lesson complete.
The edge case is where most interview follow-up questions begin.
WRONG Looking only at the final output.
RIGHT Trace props, state, and rendered JSX through each important step.
Tracing makes debugging faster because you can see the first incorrect state.

Practice Tasks

  • Build one small component or hook that demonstrates React in an interactive form or modal.
  • Change the example to include missing, repeated, empty, or boundary input and record the difference.
  • Break the example by deliberately copying the syntax before understanding the behavior, then write the corrected version.
  • Explain the finished example in five bullet points: input, operation, output, failure case, and verification.

Frequently Asked Questions

Use it when the problem matches the behavior shown in the example and when the result can be verified through React DevTools and test output.

Start with a tiny case, then test missing, repeated, empty, or boundary input. The main warning sign is copying the syntax before understanding the behavior.

Trace props, state, and rendered JSX, predict the result, run the example, and compare your prediction with the actual output.

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.