| |

React 35 ⚛️ Custom Hooks Basics

A custom hook is a function whose name starts with use and that calls other hooks. That is the entire definition. There is no special syntax, no registration, no framework API. A custom hook is a function that extracts component logic into a reusable unit. The logic inside it is the same hooks—useState, useEffect, useRef, useContext—that a component uses directly. The extraction is the mechanism that keeps components readable and the logic testable.

Key point: A custom hook is a function that starts with use and calls at least one hook. It can call other custom hooks. It returns whatever the caller needs: a value, an array, an object, a function, or a combination. The rules of hooks apply to custom hooks exactly as they apply to built-in hooks: call them at the top level of a component or another hook, and never inside loops, conditions, or nested functions.


Why custom hooks exist

The duplication problem. Two components that fetch data from different endpoints write the same loading, error, and data logic twice. The logic is identical except for the URL. A custom hook extracts the logic into one place, parameterized by the URL, and both components use it.

The readability problem. A component that manages five pieces of state, three effects, and two refs is hard to read. The component’s purpose—what it renders—is buried in the mechanics of how it gets the data. A custom hook moves the mechanics out, and the component becomes a short function that calls the hook and renders the result.

The testing problem. A custom hook can be tested independently of the component that uses it. The test calls the hook, exercises its logic, and asserts on the result. The component test focuses on the rendering. The separation makes both tests simpler.

The composition problem. A custom hook can call other custom hooks. A useUserProfile hook can call useFetch for the data and useAuth for the token. The composition is the same as components composing components: small pieces combined into larger ones.

The convention problem. The use prefix is not decoration. The React linter uses it to identify hooks and enforce the rules. A function that calls useState but is not named with the use prefix is not recognized as a hook, and the linter does not check it. A function named with the use prefix that does not call a hook is a false positive, and the linter warns. The prefix is the contract.

The sharing problem. The logic in a custom hook is shared by importing the hook, not by copying the code. A fix in the hook fixes every component that uses it. The sharing is the point.


a. Writing a custom hook

A custom hook is a function that starts with use and calls other hooks. It returns whatever the caller needs.

import { useState, useEffect } from 'react';

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

  const increment = () => setCount((c) => c + 1);
  const decrement = () => setCount((c) => c - 1);
  const reset = () => setCount(initial);

  return { count, increment, decrement, reset };
}

The hook returns an object with the state and the functions. The component uses it.

function Counter() {
  const { count, increment, decrement, reset } = useCounter(10);

  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

The component does not know how the counter is implemented. It receives the state and the functions and uses them. The logic is in the hook, and every component that needs a counter uses the same hook.

The return value can be an array if the caller benefits from positional destructuring.

function useToggle(initial = false) {
  const [value, setValue] = useState(initial);

  const toggle = () => setValue((v) => !v);

  return [value, toggle];
}

// Usage
const [isOpen, toggleOpen] = useToggle();

The array return mirrors the useState convention. The object return is more common for hooks with more than two values because the names are explicit.

The custom hook can accept arguments. The arguments are the parameters of the hook, and they can be any values.

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

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

The key and initialValue are the arguments. The hook reads from localStorage on mount and writes to it when the value changes.

The custom hook can call other custom hooks.

function useUserProfile(userId) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);
  const { token } = useAuth();

  return { user: data, loading, error, token };
}

The useFetch and useAuth are custom hooks. The useUserProfile composes them. The component that calls useUserProfile gets the combined result.

The custom hook can return functions that close over the hook’s state.

function useDebouncedValue(value, delay) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}

The useDebouncedValue returns the debounced value. The component uses it in a search.

function Search() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebouncedValue(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      search(debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

The component keeps the immediate query for the input and uses the debounced value for the search. The hook encapsulates the debouncing logic.


b. The fetch hook

The fetch hook is the canonical custom hook. It extracts the loading, error, and data logic.

import { useState, useEffect } from 'react';

function useFetch(url) {
  const [state, setState] = useState({
    status: 'idle',
    data: null,
    error: null,
  });

  useEffect(() => {
    const controller = new AbortController();

    setState({ status: 'loading', data: null, error: null });

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data) => setState({ status: 'success', data, error: null }))
      .catch((err) => {
        if (err.name === 'AbortError') return;
        setState({ status: 'error', data: null, error: err });
      });

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

  return state;
}

The hook returns the state object. The component renders the branches.

function UserProfile({ userId }) {
  const { status, data, error } = useFetch(`/api/users/${userId}`);

  if (status === 'loading') return <Spinner />;
  if (status === 'error') return <ErrorMessage error={error} />;
  if (status === 'success') return <p>{data.name}</p>;
  return null;
}

The component is short. The fetch logic is in the hook. Every component that fetches uses the same hook, and the loading, error, and abort logic is in one place.

The hook can be extended to include a retry.

function useFetch(url, options = {}) {
  const { retries = 0 } = options;
  const [attempt, setAttempt] = useState(0);
  const [state, setState] = useState({
    status: 'idle',
    data: null,
    error: null,
  });

  useEffect(() => {
    const controller = new AbortController();

    setState({ status: 'loading', data: null, error: null });

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data) => setState({ status: 'success', data, error: null }))
      .catch((err) => {
        if (err.name === 'AbortError') return;
        if (attempt < retries) {
          setTimeout(() => setAttempt((a) => a + 1), 2 ** attempt * 1000);
        } else {
          setState({ status: 'error', data: null, error: err });
        }
      });

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

  const retry = () => setAttempt((a) => a + 1);

  return { ...state, retry };
}

The hook now accepts a retries option and returns a retry function. The retry is automatic up to the limit, and manual via the returned function.

The hook can be parameterized with the fetch options.

function useFetch(url, options) {
  const [state, setState] = useState({ status: 'idle', data: null, error: null });

  useEffect(() => {
    const controller = new AbortController();

    setState({ status: 'loading', data: null, error: null });

    fetch(url, { ...options, signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data) => setState({ status: 'success', data, error: null }))
      .catch((err) => {
        if (err.name === 'AbortError') return;
        setState({ status: 'error', data: null, error: err });
      });

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

  return state;
}

The options is in the dependencies. The caller must memoize it, or the effect re-runs on every render.

const options = useMemo(() => ({ headers: { Auth: token } }), [token]);
const state = useFetch(url, options);

The useMemo stabilizes the options. The hook is the same; the caller is responsible for the stable dependency.


c. The rules of hooks and the linter

The rules of hooks apply to custom hooks exactly as they apply to built-in hooks.

Rule 1: Call hooks at the top level. Do not call hooks inside loops, conditions, or nested functions. The order of the hooks must be the same on every render.

// BAD: conditional hook
function Component({ enabled }) {
  if (enabled) {
    const [value, setValue] = useState(0); // error
  }
}

// GOOD: unconditional
function Component({ enabled }) {
  const [value, setValue] = useState(0);
  if (!enabled) return null;
}

Rule 2: Call hooks from React functions. A hook can be called from a component or from another hook. It cannot be called from a regular function.

// BAD: hook in a regular function
function calculate() {
  const [value, setValue] = useState(0); // error
}

// GOOD: hook in a custom hook
function useCalculate() {
  const [value, setValue] = useState(0);
  return value;
}

The use prefix is what the linter uses to identify a function as a hook. A function named useCalculate is recognized as a hook, and the linter checks the rules inside it. A function named calculate is not.

The eslint-plugin-react-hooks rule enforces both rules. The rules-of-hooks rule catches conditional hooks and hooks in non-React functions. The exhaustive-deps rule catches missing dependencies in useEffect, useMemo, and useCallback. Both rules apply to custom hooks.

The linter does not check the return value or the logic of a custom hook. It only checks that the hooks are called in the right places with the right dependencies. The correctness of the logic is the developer’s responsibility.

A custom hook can return anything. The convention is to return an object for multiple values and an array for two values, but the choice is the developer’s.

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handler = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);

  return size;
}

// Usage
const { width, height } = useWindowSize();

The hook returns an object with the window size. The component destructures it.

A custom hook can accept a ref and return a ref.

function useClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) return;
      handler(event);
    };
    document.addEventListener('mousedown', listener);
    return () => document.removeEventListener('mousedown', listener);
  }, [ref, handler]);
}

The hook attaches a listener to the document and calls the handler when the click is outside the ref. The hook takes the ref and the handler as arguments.

The handler is in the dependencies. If the caller passes a new function on every render, the effect re-runs on every render. The caller should memoize the handler with useCallback.

const handleClickOutside = useCallback(() => setOpen(false), []);
useClickOutside(ref, handleClickOutside);

The useCallback stabilizes the handler. The hook’s effect runs once.


Complete Example Session

// ============================================
// PART 1: BASIC CUSTOM HOOK
// ============================================
function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  const increment = () => setCount((c) => c + 1);
  const reset = () => setCount(initial);
  return { count, increment, reset };
}
// ============================================
// PART 2: USING THE HOOK
// ============================================
function Counter() {
  const { count, increment, reset } = useCounter(10);
  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}
// ============================================
// PART 3: ARRAY RETURN
// ============================================
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = () => setValue((v) => !v);
  return [value, toggle];
}

const [isOpen, toggleOpen] = useToggle();
// ============================================
// PART 4: LOCAL STORAGE HOOK
// ============================================
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}
// ============================================
// PART 5: DEBOUNCE HOOK
// ============================================
function useDebouncedValue(value, delay) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}
// ============================================
// PART 6: FETCH HOOK
// ============================================
function useFetch(url) {
  const [state, setState] = useState({
    status: 'idle',
    data: null,
    error: null,
  });

  useEffect(() => {
    const controller = new AbortController();
    setState({ status: 'loading', data: null, error: null });

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data) => setState({ status: 'success', data, error: null }))
      .catch((err) => {
        if (err.name === 'AbortError') return;
        setState({ status: 'error', data: null, error: err });
      });

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

  return state;
}
// ============================================
// PART 7: WINDOW SIZE HOOK
// ============================================
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handler = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);

  return size;
}
// ============================================
// PART 8: CLICK OUTSIDE HOOK
// ============================================
function useClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) return;
      handler(event);
    };
    document.addEventListener('mousedown', listener);
    return () => document.removeEventListener('mousedown', listener);
  }, [ref, handler]);
}
// ============================================
// PART 9: COMPOSING HOOKS
// ============================================
function useUserProfile(userId) {
  const { data, status, error } = useFetch(`/api/users/${userId}`);
  const { token } = useAuth();
  return { user: data, status, error, token };
}
// ============================================
// PART 10: CONDITIONAL HOOK BUG
// ============================================
// BAD: hook inside a condition
function Component({ enabled }) {
  if (enabled) {
    const [value, setValue] = useState(0); // error
  }
}

// GOOD: hook at the top
function Component({ enabled }) {
  const [value, setValue] = useState(0);
  if (!enabled) return null;
}

The ten parts covered a basic custom hook, using the hook, the array return, the local storage hook, the debounce hook, the fetch hook, the window size hook, the click outside hook, composing hooks, and the conditional hook bug.


Quick Reference

Custom Hook Rules

RuleRequirement
NameStarts with use
Calls hooksAt least one
Top levelNot in conditions or loops
React functionComponent or hook
ReturnAnything

Common Custom Hooks

HookPurpose
useCounterCounter state
useToggleBoolean toggle
useLocalStoragePersisted state
useDebouncedValueDebounced value
useFetchData fetching
useWindowSizeWindow dimensions
useClickOutsideDetect outside click
useIntervalInterval with cleanup
usePreviousPrevious value
useAuthAuthentication state

Return Conventions

ValuesConvention
OneSingle value
TwoArray [a, b]
ManyObject { a, b, c }
State + actionsObject

The Linter

RuleCatches
rules-of-hooksConditional hooks, hooks in non-React functions
exhaustive-depsMissing dependencies

Best Practices

✅ Do This:

// Name the hook with the use prefix
function useCounter() { }                                      // ✅

// Call hooks at the top level
function useCounter() {
  const [count, setCount] = useState(0);
  return count;
}                                                              // ✅

// Return an object for multiple values
return { count, increment, reset };                            // ✅

// Return an array for two values
return [value, setValue];                                      // ✅

// Compose custom hooks
function useUserProfile(id) {
  const { data } = useFetch(`/api/users/${id}`);
  return data;
}                                                              // ✅

// Memoize handlers passed to hooks
const handler = useCallback(() => {}, []);
useClickOutside(ref, handler);                                 // ✅

// Test the hook independently
// Use @testing-library/react-hooks or renderHook             // ✅

❌ Don’t Do This:

// Don't call hooks conditionally
if (enabled) {
  const [value, setValue] = useState(0);                       // ❌
}

// Don't call hooks in loops
for (let i = 0; i < 10; i++) {
  useEffect(() => {});                                         // ❌
}

// Don't call hooks from regular functions
function calculate() {
  const [value, setValue] = useState(0);                       // ❌
}

// Don't name a hook without the use prefix
function counter() {
  const [count, setCount] = useState(0);                       // ⚠️
}

// Don't return a new object on every render from a hook
return { count, increment: () => setCount(c => c + 1) };       // ⚠️

// Don't forget to memoize the handler dependency
useClickOutside(ref, () => setOpen(false));                    // ⚠️

// Don't test the hook only through the component
// Test the hook directly                                         // ⚠️

Common Pitfalls

PitfallWhy It HappensFix
Conditional hook errorHook inside a conditionMove to top level
Linter does not checkNo use prefixAdd the prefix
Re-renders on every callNew function returneduseCallback
Handler dependency loopsUnstable handleruseCallback
Hook logic duplicatedNo extractionExtract to hook
Test hard to writeLogic in the componentExtract to hook
Hook order changesConditional hooksTop-level only

Real-World Examples

1. Counter

function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  return { count, increment: () => setCount((c) => c + 1) };
}

2. Toggle

function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  return [value, () => setValue((v) => !v)];
}

3. Local Storage

function useLocalStorage(key, initial) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initial;
  });
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  return [value, setValue];
}

4. Debounced Value

function useDebouncedValue(value, delay) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

5. Fetch

function useFetch(url) {
  const [state, setState] = useState({ status: 'idle', data: null, error: null });
  useEffect(() => {
    const controller = new AbortController();
    setState({ status: 'loading', data: null, error: null });
    fetch(url, { signal: controller.signal })
      .then((r) => r.json())
      .then((data) => setState({ status: 'success', data, error: null }))
      .catch((err) => {
        if (err.name !== 'AbortError') setState({ status: 'error', data: null, error: err });
      });
    return () => controller.abort();
  }, [url]);
  return state;
}

6. Window Size

function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
  useEffect(() => {
    const handler = () => setSize({ width: window.innerWidth, height: window.innerHeight });
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  return size;
}

7. Click Outside

function useClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (e) => {
      if (!ref.current || ref.current.contains(e.target)) return;
      handler(e);
    };
    document.addEventListener('mousedown', listener);
    return () => document.removeEventListener('mousedown', listener);
  }, [ref, handler]);
}

8. Interval

function useInterval(callback, delay) {
  const savedCallback = useRef(callback);
  useEffect(() => { savedCallback.current = callback; }, [callback]);
  useEffect(() => {
    if (delay === null) return;
    const id = setInterval(() => savedCallback.current(), delay);
    return () => clearInterval(id);
  }, [delay]);
}

9. Previous Value

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => { ref.current = value; }, [value]);
  return ref.current;
}

10. Online Status

function useOnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);
  useEffect(() => {
    const on = () => setOnline(true);
    const off = () => setOnline(false);
    window.addEventListener('online', on);
    window.addEventListener('offline', off);
    return () => {
      window.removeEventListener('online', on);
      window.removeEventListener('offline', off);
    };
  }, []);
  return online;
}

Visual

Custom Hook Extraction

┌─────────────────────────────────────────────────────────────┐
│  BEFORE                                                     │
│                                                             │
│  function UserProfile({ id }) {                             │
│    const [data, setData] = useState(null);                  │
│    const [loading, setLoading] = useState(true);            │
│    const [error, setError] = useState(null);                │
│                                                             │
│    useEffect(() => {                                        │
│      const controller = new AbortController();              │
│      setLoading(true);                                      │
│      fetch(`/api/users/${id}`, { signal: controller.signal })│
│        .then((r) => r.json())                               │
│        .then((d) => { setData(d); setLoading(false); })     │
│        .catch((e) => { setError(e); setLoading(false); });  │
│      return () => controller.abort();                       │
│    }, [id]);                                                │
│                                                             │
│    if (loading) return <Spinner />;                         │
│    if (error) return <Error />;                             │
│    return <p>{data.name}</p>;                               │
│  }                                                          │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  AFTER                                                      │
│                                                             │
│  function useFetch(url) {                                   │
│    // the same logic, extracted                             │
│    return { data, loading, error };                         │
│  }                                                          │
│                                                             │
│  function UserProfile({ id }) {                             │
│    const { data, loading, error } = useFetch(`/api/users/${id}`);│
│    if (loading) return <Spinner />;                         │
│    if (error) return <Error />;                             │
│    return <p>{data.name}</p>;                               │
│  }                                                          │
│                                                             │
│  The component is short. The logic is reusable.             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Rules of Hooks

┌─────────────────────────────────────────────────────────────┐
│  GOOD: TOP LEVEL                                            │
│                                                             │
│  function useCounter() {                                    │
│    const [count, setCount] = useState(0);  ← top level      │
│    useEffect(() => {}, []);                ← top level      │
│    return count;                                            │
│  }                                                          │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  BAD: CONDITIONAL                                           │
│                                                             │
│  function useCounter({ enabled }) {                         │
│    if (enabled) {                                           │
│      const [count, setCount] = useState(0);  ← conditional  │
│    }                                                        │
│  }                                                          │
│                                                             │
│  The hook order changes between renders.                    │
│  React cannot match the state to the hook.                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Composition

┌─────────────────────────────────────────────────────────────┐
│  useAuth          useFetch                                  │
│    │                 │                                      │
│    └────────┬────────┘                                      │
│             ▼                                               │
│      useUserProfile                                         │
│             │                                               │
│             ▼                                               │
│    function UserProfile() {                                 │
│      const { user, token } = useUserProfile(id);            │
│      return <p>{user.name}</p>;                             │
│    }                                                        │
│                                                             │
│  Hooks compose like components.                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Return Values

┌─────────────────────────────────────────────────────────────┐
│  ONE VALUE                                                  │
│  return count;                                              │
│                                                             │
│  TWO VALUES                                                 │
│  return [value, setValue];                                  │
│  const [value, setValue] = useToggle();                     │
│                                                             │
│  MANY VALUES                                                │
│  return { count, increment, decrement, reset };             │
│  const { count, increment } = useCounter();                 │
│                                                             │
│  STATE + ACTION                                             │
│  return { ...state, retry };                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
DefinitionFunction starting with use calling hooks
Prefixuse is required
PlacementTop level of component or hook
ReturnAnything
CompositionHooks can call hooks
Linterrules-of-hooks, exhaustive-deps
Common hooksuseFetch, useLocalStorage, useDebouncedValue
Array return[value, setter]
Object return{ value, actions }
TestingrenderHook from testing library

Key takeaways:

  • A custom hook is a function that starts with use and calls at least one hook. There is no special syntax. The prefix is the contract, and the linter uses it to enforce the rules of hooks.
  • The rules of hooks apply to custom hooks. Call hooks at the top level, never inside conditions, loops, or nested functions. Call hooks from components or other hooks, never from regular functions.
  • The return value is whatever the caller needs. An array for two values, an object for more. The convention mirrors useState for two values and uses an object for many.
  • Custom hooks compose. A hook can call other custom hooks. The composition is the same as components composing components. Small hooks combine into larger ones.
  • The fetch hook is the canonical example. It extracts the loading, error, data, and abort logic into one reusable function. Every component that fetches uses the same hook, and the logic lives in one place.
  • The linter enforces the rules. The rules-of-hooks rule catches conditional hooks and hooks in non-React functions. The exhaustive-deps rule catches missing dependencies. Both apply to custom hooks.
  • Custom hooks are testable. The hook can be tested independently of the component. The test calls the hook and asserts on the result. The component test focuses on rendering.

Remember: Custom hooks are the mechanism for extracting and sharing component logic. They are not a new API; they are a naming convention that the linter recognizes. The logic inside a custom hook is the same useState, useEffect, and useRef that a component uses directly. The extraction makes the component readable and the logic reusable. Write a hook when the same logic appears in two components, or when a component’s logic is too complex to read. Name it with use, follow the rules, and the linter will keep you honest.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!