React Tutorial 0/120 lessons ~6 min read Lesson 43

    Hook Best Practices

    Hook best practices cover keeping state minimal, deriving values, using Effects for synchronization, naming custom Hooks clearly, and optimizing intentionally.

    Course progress0%
    Focus
    70 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    React Hooks make it possible to build stateful, reusable, and maintainable function components.

    But knowing how to use useState, useEffect, useRef, useMemo, useCallback, useReducer, and Custom Hooks is only the beginning.

    Good React development also requires knowing how to use Hooks well.

    Poor Hook usage can lead to:

    • unnecessary re-renders
    • stale values
    • Effect loops
    • duplicated state
    • difficult-to-test components
    • excessive memoization
    • complicated dependency arrays
    • hidden side effects
    • tightly coupled components
    • difficult-to-maintain Custom Hooks

    This lesson brings together the most important practices for writing clean, predictable, maintainable React Hook code.

    The Core Philosophy

    A good React component should make it easy to answer:

    text
    What data does this component own?
    ↓
    How does that data change?
    ↓
    What should the UI display?
    ↓
    What external systems must be synchronized?

    A useful architecture is:

    text
    Props
    ↓
    State
    ↓
    Derived Values
    ↓
    Event Handlers
    ↓
    Effects
    ↓
    UI

    Not every component needs every layer.

    The goal is to use the simplest design that correctly represents the behavior.

    Best Practice 1: Keep State Minimal

    One of the most important React principles is:

    Store the minimum state necessary.

    Avoid storing values that can be derived from existing state or props.

    Bad:

    javascript
    const [firstName, setFirstName] =
    useState("");
    const [lastName, setLastName] =
    useState("");
    const [fullName, setFullName] =
    useState("");
    useEffect(() => {
    setFullName(
    `${firstName} ${lastName}`
    );
    }, [firstName, lastName]);

    Better:

    javascript
    const [firstName, setFirstName] =
    useState("");
    const [lastName, setLastName] =
    useState("");
    const fullName =
    `${firstName} ${lastName}`;

    The second version has fewer moving parts.

    Derived State

    A value is derived when it can be calculated from existing values.

    Example:

    javascript
    const [price, setPrice] = useState(100);
    const [quantity, setQuantity] = useState(2);
    const total = price * quantity;

    Do not create:

    javascript
    const [total, setTotal] = useState(200);

    and synchronize it with an Effect.

    Derived values should normally be calculated during rendering.

    When Derived Data Is Expensive

    If the calculation is genuinely expensive:

    javascript
    const filteredProducts = useMemo(() => {
    return expensiveFilter(products, query);
    }, [products, query]);

    Use useMemo because there is an actual performance reason.

    Do not turn every derived value into memoized state.

    Best Practice 2: Keep State Close to Where It Is Used

    State should generally live in the nearest practical component that needs to own it.

    Suppose a modal is only used by:

    text
    ProductDetails

    There may be no reason to store its state at the application root.

    Prefer:

    javascript
    function ProductDetails() {
    const [isOpen, setIsOpen] =
    useState(false);
    // ...
    }

    instead of unnecessarily lifting it to:

    javascript
    App

    Local state can reduce unnecessary update propagation and makes ownership clearer.

    When Should State Be Lifted?

    Lift state when multiple components genuinely need the same state.

    For example:

    text
    Parent
    ├── SearchInput
    └── SearchResults

    If both depend on the same search query:

    javascript
    function SearchPage() {
    const [query, setQuery] =
    useState("");
    return (
    <>
    <SearchInput
    query={query}
    setQuery={setQuery}
    />
    <SearchResults
    query={query}
    />
    </>
    );
    }

    The parent becomes the appropriate state owner.

    The principle is:

    text
    Only one component needs it
    ↓
    Keep it local
    Multiple components need it
    ↓
    Lift to their common owner

    Best Practice 3: Prefer Functional State Updates When Appropriate

    When the next state depends on the previous state:

    javascript
    setCount((current) => current + 1);

    Prefer this over:

    javascript
    setCount(count + 1);

    when the update is specifically based on previous state.

    This is especially useful in:

    • intervals
    • event queues
    • asynchronous callbacks
    • multiple state updates
    • reusable state logic

    Example:

    javascript
    function incrementTwice() {
    setCount((current) => current + 1);
    setCount((current) => current + 1);
    }

    Best Practice 4: Treat State as Immutable

    Never mutate state directly.

    Bad:

    javascript
    user.name = "Asha";

    Bad:

    javascript
    items.push(newItem);

    Good:

    javascript
    setUser((current) => ({
    ...current,
    name: "Asha",
    }));

    Good:

    javascript
    setItems((current) => [
    ...current,
    newItem,
    ]);

    For nested objects:

    javascript
    setUser((current) => ({
    ...current,
    profile: {
    ...current.profile,
    city: "Bhubaneswar",
    },
    }));

    Immutability makes state transitions easier to reason about and supports predictable React updates.

    Best Practice 5: Keep Effects Focused

    Avoid giant Effects:

    javascript
    useEffect(() => {
    fetchData();
    document.title = title;
    subscribe();
    localStorage.setItem("theme", theme);
    startTimer();
    }, [
    title,
    theme,
    ]);

    These operations represent different synchronization concerns.

    Prefer separate Effects when the concerns have different dependencies or lifecycles:

    javascript
    useEffect(() => {
    fetchData();
    }, [query]);
    useEffect(() => {
    document.title = title;
    }, [title]);
    useEffect(() => {
    localStorage.setItem(
    "theme",
    theme
    );
    }, [theme]);

    Each Effect becomes easier to understand and debug.

    One Effect, One Synchronization Concern

    A useful guideline is:

    text
    One Effect
    ↓
    One coherent synchronization concern

    For example:

    javascript
    useEffect(() => {
    const connection =
    connectToRoom(roomId);
    return () => {
    connection.disconnect();
    };
    }, [roomId]);

    This Effect has one clear responsibility:

    text
    Synchronize chat connection with roomId.

    Best Practice 6: Do Not Use Effects for Derived Data

    Avoid:

    javascript
    const [filtered, setFiltered] =
    useState([]);
    useEffect(() => {
    setFiltered(
    products.filter(
    (product) =>
    product.category === category
    )
    );
    }, [products, category]);

    Prefer:

    javascript
    const filtered = products.filter(
    (product) =>
    product.category === category
    );

    If expensive:

    javascript
    const filtered = useMemo(() => {
    return products.filter(
    (product) =>
    product.category === category
    );
    }, [products, category]);

    This reduces unnecessary state and synchronization.

    Best Practice 7: Use Event Handlers for User Actions

    If something happens because the user clicked a button, the event handler is usually the clearest place to perform that action.

    Good:

    javascript
    function handleSubmit() {
    saveForm();
    }

    Avoid turning a user action into an indirect Effect when there is no synchronization requirement:

    javascript
    const [submitted, setSubmitted] =
    useState(false);
    useEffect(() => {
    if (submitted) {
    saveForm();
    }
    }, [submitted]);

    The direct event-handler version is easier to understand.

    Effects Are Not a Generic Event System

    Avoid using state changes as signals merely to trigger Effects.

    For example:

    javascript
    setShouldSave(true);

    followed by:

    javascript
    useEffect(() => {
    if (shouldSave) {
    save();
    }
    }, [shouldSave]);

    If the save is caused directly by a button click, prefer:

    javascript
    function handleSave() {
    save();
    }

    Effects should synchronize state/props with external systems rather than act as a general-purpose event dispatcher.

    Best Practice 8: Keep Custom Hooks Focused

    A Custom Hook should represent a meaningful reusable behavior.

    Good:

    javascript
    useOnlineStatus()
    useDebounce()
    useLocalStorage()
    usePrevious()
    useWindowSize()

    Avoid giant Hooks such as:

    javascript
    useApplicationEverything()

    that manage unrelated concerns.

    Prefer composition:

    text
    useProductSearch()
    ↓
    useDebounce()
    +
    useProducts()

    Small Hooks can be combined into larger feature-level Hooks.

    Best Practice 9: Give Custom Hooks a Clear API

    A Hook should expose only what consumers need.

    Good:

    javascript
    const {
    data,
    loading,
    error,
    refetch,
    } = useProducts();

    The consumer should not need to know:

    • how the request works
    • how cancellation is implemented
    • how loading is calculated
    • how errors are normalized

    The Hook acts as an abstraction boundary.

    Best Practice 10: Do Not Use Custom Hooks for Pure Utilities

    A normal helper function is better when no React Hook is required.

    For example:

    javascript
    function formatCurrency(value) {
    return `₹${value.toFixed(2)}`;
    }

    There is no reason to create:

    javascript
    function useFormatCurrency(value) {
    // unnecessary
    }

    Use a Custom Hook only when the logic involves Hook-based or stateful behavior.

    Best Practice 11: Follow the Rules of Hooks

    Always:

    text
    Call Hooks
    ↓
    at the top level
    ↓
    of React components or Custom Hooks

    Never:

    javascript
    if (condition) {
    useEffect(() => {});
    }

    Never:

    javascript
    items.map(() => {
    useState();
    });

    Never:

    javascript
    function handleClick() {
    useState();
    }

    Correct conditional behavior should happen inside the Hook:

    javascript
    useEffect(() => {
    if (!enabled) {
    return;
    }
    // work
    }, [enabled]);

    Best Practice 12: Keep Dependencies Accurate

    Dependency arrays are part of the correctness model.

    Example:

    javascript
    useEffect(() => {
    fetchUser(userId);
    }, [userId]);

    If the Effect also uses:

    javascript
    organizationId

    then the dependency relationship needs to account for it:

    javascript
    useEffect(() => {
    fetchUser(
    userId,
    organizationId
    );
    }, [userId, organizationId]);

    Do not remove dependencies just to reduce Effect executions.

    If the Effect runs too often, investigate why.

    Best Practice 13: Prefer Restructuring Over Dependency Hacks

    Suppose:

    javascript
    useEffect(() => {
    process(data);
    }, [data, process]);

    and process changes on every render.

    Do not immediately remove it:

    javascript
    useEffect(() => {
    process(data);
    }, [data]);

    Instead, consider whether the function belongs inside the Effect:

    javascript
    useEffect(() => {
    function processData() {
    // ...
    }
    processData();
    }, [data]);

    Or determine whether a stable callback is actually appropriate.

    The goal is correct synchronization, not merely fewer Effect runs.

    Best Practice 14: Use useCallback Intentionally

    Good reason:

    javascript
    const handleDelete = useCallback(
    (id) => {
    deleteItem(id);
    },
    []
    );

    when the stable function reference matters to a memoized child or another dependency relationship.

    Bad reason:

    javascript
    const add = useCallback(
    (a, b) => a + b,
    []
    );

    if the function is trivial and its identity does not matter.

    useCallback adds complexity.

    Use it when there is a real benefit.

    Best Practice 15: Use useMemo Intentionally

    Good:

    javascript
    const report = useMemo(() => {
    return generateLargeReport(
    transactions
    );
    }, [transactions]);

    when report generation is genuinely expensive.

    Usually unnecessary:

    javascript
    const name = useMemo(() => {
    return `${firstName} ${lastName}`;
    }, [firstName, lastName]);

    Prefer:

    javascript
    const name =
    `${firstName} ${lastName}`;

    Memoization should solve a performance problem, not become a coding habit.

    Best Practice 16: Understand Referential Equality

    JavaScript compares objects and functions by reference.

    javascript
    {} === {}

    is:

    text
    false

    Similarly:

    javascript
    (() => {}) === (() => {})

    is:

    text
    false

    This matters for:

    • Effect dependencies
    • React.memo
    • Context values
    • useMemo
    • useCallback

    Understand reference identity before adding memoization.

    Best Practice 17: Avoid Unnecessary Object Dependencies

    Instead of:

    javascript
    const options = {
    roomId,
    theme,
    };
    useEffect(() => {
    connect(options);
    }, [options]);

    consider:

    javascript
    useEffect(() => {
    connect({
    roomId,
    theme,
    });
    }, [roomId, theme]);

    This makes the dependency relationship explicit.

    Best Practice 18: Clean Up External Resources

    If an Effect creates something external, determine how it should be cleaned up.

    Examples:

    text
    addEventListener
    ↓
    removeEventListener
    text
    setInterval
    ↓
    clearInterval
    text
    subscribe
    ↓
    unsubscribe
    text
    connect
    ↓
    disconnect
    text
    createObserver
    ↓
    disconnect

    Example:

    javascript
    useEffect(() => {
    const timer = setInterval(
    refresh,
    5000
    );
    return () => {
    clearInterval(timer);
    };
    }, []);

    Best Practice 19: Handle Async Effects Carefully

    Asynchronous work can create:

    • race conditions
    • stale responses
    • state updates after relevance changes
    • cancellation problems

    Use appropriate cancellation or cleanup strategies.

    Example:

    javascript
    useEffect(() => {
    const controller =
    new AbortController();
    async function load() {
    try {
    const response = await fetch(
    url,
    {
    signal:
    controller.signal,
    }
    );
    const data =
    await response.json();
    setData(data);
    } catch (error) {
    if (
    error.name !==
    "AbortError"
    ) {
    setError(error);
    }
    }
    }
    load();
    return () => {
    controller.abort();
    };
    }, [url]);

    Best Practice 20: Avoid Race Conditions

    Suppose:

    text
    Request A
    ↓
    Request B
    ↓
    B completes
    ↓
    A completes

    If both update the same state, older data can overwrite newer data.

    Use cancellation or an appropriate stale-result strategy.

    For APIs where request cancellation is supported:

    javascript
    const controller =
    new AbortController();
    fetch(url, {
    signal: controller.signal,
    });

    Cleanup:

    javascript
    return () => {
    controller.abort();
    };

    Best Practice 21: Use useRef for Non-Visual Mutable Values

    A ref is useful when a value must persist between renders but changing it should not trigger a render.

    Good examples:

    javascript
    const timerRef = useRef(null);
    javascript
    const socketRef = useRef(null);
    javascript
    const previousValueRef =
    useRef(value);

    Do not use a ref simply to avoid a state update when the value actually affects the UI.

    State vs Ref

    Use state:

    text
    Value changes
    ↓
    UI should change

    Use ref:

    text
    Value changes
    ↓
    UI does not need to change

    Example:

    javascript
    const [isOpen, setIsOpen] =
    useState(false);

    The UI depends on isOpen.

    Compare:

    javascript
    const timerId = useRef(null);

    The timer ID itself does not need to appear in the UI.

    Best Practice 22: Keep Refs Out of the Rendering Model

    Avoid relying on a ref as if it were state:

    javascript
    const countRef = useRef(0);
    countRef.current += 1;
    return <p>{countRef.current}</p>;

    The UI will not automatically update when .current changes.

    If the value belongs in the UI, use state.

    Best Practice 23: Use useReducer for Complex Transitions

    If state logic becomes difficult to understand:

    javascript
    setLoading(true);
    setError(null);
    setData(null);

    and there are many combinations of transitions, consider:

    javascript
    dispatch({
    type: "requestStarted",
    });

    with:

    javascript
    function reducer(state, action) {
    switch (action.type) {
    case "requestStarted":
    return {
    ...state,
    loading: true,
    error: null,
    };
    // ...
    }
    }

    useReducer is primarily about organizing complex state transitions, not automatically improving performance.

    Best Practice 24: Design Meaningful Actions

    Prefer:

    javascript
    {
    type: "formSubmitted"
    }

    over:

    javascript
    {
    type: "setLoadingTrue"
    }

    Meaningful actions describe events.

    The reducer decides what those events mean.

    This creates a cleaner state model.

    Best Practice 25: Keep Reducers Pure

    A reducer should:

    • calculate next state
    • avoid side effects
    • avoid mutating state
    • remain predictable

    Good:

    javascript
    function reducer(state, action) {
    switch (action.type) {
    case "increment":
    return {
    ...state,
    count: state.count + 1,
    };
    default:
    return state;
    }
    }

    Avoid:

    javascript
    function reducer(state, action) {
    fetch("/api/data");
    return state;
    }

    Best Practice 26: Use Context Carefully

    Context is useful when many components need the same value.

    But avoid putting every piece of application state into one giant Context.

    Instead of:

    text
    AppContext
    ├── auth
    ├── cart
    ├── products
    ├── notifications
    ├── filters
    ├── settings
    └── everything else

    consider feature-oriented boundaries.

    For example:

    text
    AuthContext
    CartContext
    ThemeContext

    or other structures appropriate to the application.

    Best Practice 27: Combine Context with useReducer for Feature State

    A common pattern is:

    javascript
    const [state, dispatch] =
    useReducer(
    reducer,
    initialState
    );

    Then provide:

    javascript
    <AppContext.Provider
    value={{
    state,
    dispatch,
    }}
    >
    {children}
    </AppContext.Provider>

    This can work well for feature-level state management.

    Do not assume it is the right choice for every application.

    Best Practice 28: Keep Component APIs Simple

    A component should ideally expose meaningful props:

    javascript
    <ProductCard
    product={product}
    onAddToCart={handleAddToCart}
    />

    Avoid exposing internal state-management details unnecessarily.

    Custom Hooks can help keep components clean:

    javascript
    const {
    products,
    loading,
    error,
    } = useProducts();

    Best Practice 29: Keep Components Focused

    If a component contains:

    text
    API requests
    form management
    WebSocket
    analytics
    modal state
    table filtering
    pagination
    authentication

    it may be doing too much.

    Consider separating concerns:

    text
    Component
    ↓
    Custom Hooks
    ├── useProducts
    ├── useFilters
    ├── usePagination
    └── useModal

    The component focuses on composition and UI.

    Best Practice 30: Use Component Boundaries for Repeated State

    If every list item needs independent state, use a child component.

    Good:

    javascript
    function TodoItem({ todo }) {
    const [editing, setEditing] =
    useState(false);
    return <div>{todo.text}</div>;
    }

    Then:

    javascript
    todos.map((todo) => (
    <TodoItem
    key={todo.id}
    todo={todo}
    />
    ))

    This is much clearer than trying to create dynamic Hook calls inside the parent.

    Best Practice 31: Use Stable Keys

    Prefer:

    javascript
    key={item.id}

    Avoid:

    javascript
    key={Math.random()}

    Avoid index keys when items can be reordered or inserted:

    javascript
    key={index}

    Keys are about component identity and correct reconciliation.

    They can also affect whether state is preserved between list updates.

    Best Practice 32: Do Not Optimize Before Measuring

    A common mistake is adding:

    javascript
    useMemo
    useCallback
    React.memo

    everywhere.

    Instead:

    text
    Notice problem
    ↓
    Measure
    ↓
    Find bottleneck
    ↓
    Optimize
    ↓
    Measure again

    Performance optimization should be evidence-driven.

    Best Practice 33: Use React DevTools

    React DevTools can help inspect:

    • component hierarchy
    • props
    • state
    • context
    • component updates
    • performance

    For performance problems, use the Profiler to identify expensive components and interactions.

    Best Practice 34: Keep Effects Correct Before Making Them Fast

    A correct Effect that runs occasionally is better than an "optimized" Effect that uses stale data.

    First ensure:

    text
    Correct dependencies
    +
    Correct cleanup
    +
    Correct synchronization

    Then optimize if profiling shows a problem.

    Best Practice 35: Make Effects Reversible

    A useful principle is:

    Setup should be paired with cleanup.

    Example:

    javascript
    useEffect(() => {
    const connection =
    createConnection(roomId);
    connection.connect();
    return () => {
    connection.disconnect();
    };
    }, [roomId]);

    This makes the Effect easier to reason about and robust under development lifecycle checks.

    Best Practice 36: Design for Strict Mode

    Do not write special code simply to prevent an Effect from being exercised more than once during development.

    Instead, ensure setup and cleanup are correct.

    Bad:

    javascript
    const hasRun =
    useRef(false);
    useEffect(() => {
    if (hasRun.current) {
    return;
    }
    hasRun.current = true;
    connect();
    }, []);

    Better:

    javascript
    useEffect(() => {
    const connection =
    createConnection();
    connection.connect();
    return () => {
    connection.disconnect();
    };
    }, []);

    The second version correctly models the lifecycle.

    Best Practice 37: Avoid Hidden Side Effects

    A function that looks like a pure calculation should not secretly modify external state.

    Avoid:

    javascript
    function calculateTotal(items) {
    localStorage.setItem(
    "items",
    JSON.stringify(items)
    );
    return items.reduce(...);
    }

    Prefer:

    javascript
    function calculateTotal(items) {
    return items.reduce(...);
    }

    Then handle storage synchronization separately.

    Best Practice 38: Keep Pure Logic Pure

    Pure functions are easier to:

    • test
    • reuse
    • debug
    • reason about

    Example:

    javascript
    function calculateTotal(items) {
    return items.reduce(
    (total, item) =>
    total + item.price * item.quantity,
    0
    );
    }

    Use it:

    javascript
    const total =
    calculateTotal(items);

    No Hook is necessary.

    Best Practice 39: Prefer Composition Over Giant Hooks

    Instead of:

    javascript
    useDashboardEverything()

    consider:

    javascript
    useAuth()
    useProducts()
    useFilters()
    usePagination()
    useNotifications()

    Then compose them:

    javascript
    function Dashboard() {
    const auth = useAuth();
    const products = useProducts();
    const filters = useFilters();
    // ...
    }

    This makes responsibilities clearer.

    Best Practice 40: Keep Hook Return Values Predictable

    If a Hook returns an object:

    javascript
    return {
    data,
    loading,
    error,
    };

    keep the API stable.

    If a Hook returns functions, make their identity stable only when there is a reason consumers benefit from it.

    A predictable API makes Custom Hooks easier to adopt.

    Best Practice 41: Avoid Premature Abstraction

    Do not create a Custom Hook simply because two lines of code look similar.

    Ask:

    text
    Is the logic stateful?
    Is it reusable?
    Does it represent a meaningful behavior?
    Would abstraction improve maintenance?

    If not, keep the code simple.

    Best Practice 42: Prefer Explicit Data Flow

    React becomes easier to understand when data flow is obvious.

    Prefer:

    text
    Props
    ↓
    State
    ↓
    Derived data
    ↓
    Event handlers
    ↓
    Effects
    ↓
    UI

    Avoid unnecessarily indirect flows:

    text
    Event
    ↓
    State flag
    ↓
    Effect
    ↓
    Another state
    ↓
    Another Effect
    ↓
    Final action

    Direct data flow is usually easier to debug.

    Best Practice 43: Separate Server State from UI State

    Not all application data is the same.

    UI state might be:

    text
    isModalOpen
    selectedTab
    inputValue

    Server state might be:

    text
    products
    users
    orders

    Server-state problems often involve:

    • caching
    • refetching
    • stale data
    • retries
    • synchronization
    • request deduplication

    For complex server-state requirements, consider an appropriate data-fetching/server-state solution rather than building increasingly complicated Effects manually.

    Best Practice 44: Keep Loading and Error States Explicit

    For asynchronous features, model the states clearly:

    text
    idle
    loading
    success
    error

    With useReducer:

    javascript
    const initialState = {
    status: "idle",
    data: null,
    error: null,
    };

    This can be easier to reason about than several independent Boolean flags.

    Best Practice 45: Avoid Boolean State Explosion

    Avoid:

    javascript
    const [isLoading, setIsLoading] =
    useState(false);
    const [isError, setIsError] =
    useState(false);
    const [isSuccess, setIsSuccess] =
    useState(false);

    This can allow contradictory combinations:

    text
    isLoading = true
    isError = true
    isSuccess = true

    A status model can be clearer:

    javascript
    const [status, setStatus] =
    useState("idle");

    Possible values:

    text
    idle
    loading
    success
    error

    For complex transitions, useReducer can make this even more explicit.

    Best Practice 46: Avoid State That Represents Events

    An event is not always state.

    For example:

    javascript
    const [clicked, setClicked] =
    useState(false);

    If clicked exists only to trigger an Effect:

    javascript
    useEffect(() => {
    if (clicked) {
    performAction();
    }
    }, [clicked]);

    you may be modeling an event as state unnecessarily.

    Use the event handler directly when appropriate.

    Best Practice 47: Use Effects to Synchronize, Not Orchestrate Everything

    Good:

    javascript
    useEffect(() => {
    document.title = title;
    }, [title]);

    Less desirable:

    javascript
    useEffect(() => {
    setStep(2);
    }, [data]);
    useEffect(() => {
    setReady(true);
    }, [step]);
    useEffect(() => {
    submit();
    }, [ready]);

    If a workflow is caused by one explicit event, model the workflow directly with event handlers and state transitions.

    Best Practice 48: Test Reducers Independently

    Because reducers are pure functions:

    javascript
    function reducer(state, action) {
    // ...
    }

    they can be tested without rendering the component.

    Example:

    javascript
    const state = {
    count: 0,
    };
    const nextState =
    reducer(state, {
    type: "increment",
    });

    This makes complex state transitions easier to verify.

    Best Practice 49: Test Custom Hooks by Behavior

    For a Custom Hook such as:

    javascript
    useOnlineStatus()

    test what consumers observe:

    text
    initial status
    ↓
    online event
    ↓
    status changes
    ↓
    offline event
    ↓
    status changes

    Avoid tests that depend unnecessarily on internal implementation details.

    Best Practice 50: Use a Consistent Hook Strategy

    A mature React codebase should have conventions for:

    • state ownership
    • Effects
    • Custom Hooks
    • reducers
    • Context
    • memoization
    • asynchronous operations
    • testing
    • folder organization

    Consistency reduces cognitive overhead.

    A complex component can often follow a structure like:

    javascript
    function ProductDashboard({
    userId,
    }) {
    // 1. State
    const [query, setQuery] =
    useState("");
    // 2. Custom Hooks
    const {
    products,
    loading,
    error,
    } = useProducts(userId);
    // 3. Derived values
    const filteredProducts =
    useMemo(() => {
    return products.filter(
    (product) =>
    product.name
    .toLowerCase()
    .includes(
    query.toLowerCase()
    )
    );
    }, [products, query]);
    // 4. Event handlers
    const handleSearch = (event) => {
    setQuery(event.target.value);
    };
    // 5. Effects
    useEffect(() => {
    document.title =
    `${filteredProducts.length} Products`;
    }, [filteredProducts.length]);
    // 6. Render
    return (
    <div>
    <input
    value={query}
    onChange={handleSearch}
    />
    {/* UI */}
    </div>
    );
    }

    This is not a mandatory syntax template.

    It is a useful organizational pattern.

    A Practical Hook Decision Tree

    When adding logic, ask:

    Is it a simple value?

    Use a normal variable:

    javascript
    const total = price * quantity;

    Does the UI need to remember it between renders?

    Use:

    javascript
    useState

    Is the next value based on previous state?

    Use a functional update:

    javascript
    setValue((current) => next);

    Are state transitions complex?

    Consider:

    javascript
    useReducer

    Does the value need persistence without causing a render?

    Consider:

    javascript
    useRef

    Does the component need external synchronization?

    Consider:

    javascript
    useEffect

    Is a calculation genuinely expensive?

    Consider:

    javascript
    useMemo

    Does function identity matter?

    Consider:

    javascript
    useCallback

    Is stateful logic reusable?

    Consider:

    javascript
    Custom Hook

    Hook Anti-Patterns

    The following patterns should trigger a review.

    Anti-pattern 1

    javascript
    useEffect(() => {
    setDerivedValue(
    calculateValue(data)
    );
    }, [data]);

    Ask whether derivedValue can be calculated directly.

    Anti-pattern 2

    javascript
    useCallback(
    () => 2 + 2,
    []
    );

    Ask whether memoization provides any value.

    Anti-pattern 3

    javascript
    if (condition) {
    useEffect(...);
    }

    This violates the Rules of Hooks.

    Anti-pattern 4

    javascript
    useEffect(() => {
    setA(...);
    }, [a]);

    Check for an Effect feedback loop.

    Anti-pattern 5

    javascript
    const [isLoading, setIsLoading] =
    useState(false);
    const [isError, setIsError] =
    useState(false);
    const [isSuccess, setIsSuccess] =
    useState(false);

    Consider a single status model.

    Anti-pattern 6

    javascript
    const value = {
    state,
    dispatch,
    };

    passed through a frequently rendering Context provider without considering whether context consumers actually need the resulting updates.

    Hook Code Review Checklist

    When reviewing a React component, ask:

    State

    • Is every state variable necessary?
    • Can any state be derived?
    • Is state owned by the right component?
    • Are updates immutable?
    • Should functional updates be used?

    Effects

    • Does each Effect synchronize with an external system?
    • Are dependencies accurate?
    • Is cleanup required?
    • Can an Effect loop?
    • Can async work race?

    Refs

    • Does the value actually need to trigger a render?
    • Is a ref being used to hide state that belongs in the UI?

    Memoization

    • Is useMemo solving an expensive calculation?
    • Is useCallback solving a reference-identity problem?
    • Is React.memo actually useful?

    Reducers

    • Are transitions complex enough to justify a reducer?
    • Are actions meaningful?
    • Is the reducer pure?

    Custom Hooks

    • Does the Hook represent reusable behavior?
    • Is its API clear?
    • Is it focused?

    Components

    • Are responsibilities clear?
    • Are repeated stateful items separate components?
    • Are keys stable?

    Async Work

    • Are errors handled?
    • Is cancellation needed?
    • Can stale data overwrite newer data?

    Practical Example: Refactoring a Poor Component

    Suppose you start with:

    javascript
    function ProductPage({ products }) {
    const [filtered, setFiltered] =
    useState([]);
    const [total, setTotal] =
    useState(0);
    const [clicked, setClicked] =
    useState(false);
    useEffect(() => {
    setFiltered(
    products.filter(
    (product) =>
    product.available
    )
    );
    }, [products]);
    useEffect(() => {
    setTotal(
    filtered.reduce(
    (sum, product) =>
    sum + product.price,
    0
    )
    );
    }, [filtered]);
    useEffect(() => {
    if (clicked) {
    saveAnalytics();
    }
    }, [clicked]);
    return (
    <div>
    <button
    onClick={() =>
    setClicked(true)
    }
    >
    Track
    </button>
    <p>Total: ₹{total}</p>
    </div>
    );
    }

    There are several unnecessary synchronization steps.

    A cleaner version:

    javascript
    function ProductPage({ products }) {
    const filtered = products.filter(
    (product) => product.available
    );
    const total = filtered.reduce(
    (sum, product) =>
    sum + product.price,
    0
    );
    function handleTrack() {
    saveAnalytics();
    }
    return (
    <div>
    <button onClick={handleTrack}>
    Track
    </button>
    <p>Total: ₹{total}</p>
    </div>
    );
    }

    If filtering or totaling becomes genuinely expensive, introduce useMemo based on measurement.

    The improved version has:

    • less state
    • fewer Effects
    • fewer renders caused by state synchronization
    • clearer event handling
    • simpler data flow

    Advanced Principle: Optimize for Correctness First

    The priority order should usually be:

    text
    Correctness
    ↓
    Clarity
    ↓
    Maintainability
    ↓
    Performance optimization

    Do not sacrifice correctness for a speculative optimization.

    For example:

    javascript
    useMemo(...)

    is not worth using if it causes confusing dependency behavior or obscures a simple calculation.

    Likewise:

    javascript
    useCallback(...)

    should not be used to hide an incorrectly designed data flow.

    Advanced Principle: Optimize Boundaries

    When performance problems exist, look at boundaries:

    text
    State boundary
    Component boundary
    Context boundary
    Memoization boundary
    External-system boundary

    Ask:

    text
    Where is state changing?
    Where does rendering start?
    Which children receive the update?
    Which external systems are synchronized?

    This often reveals the actual optimization opportunity.

    Advanced Principle: Prefer Local Reasoning

    A good Hook design lets you understand one piece of behavior without reading the entire application.

    For example:

    javascript
    const {
    data,
    loading,
    error,
    } = useProducts();

    The component can reason about product data without understanding every request implementation detail.

    This is one of the major benefits of well-designed Custom Hooks.

    Hook Best Practices Cheat Sheet

    State

    javascript
    const [value, setValue] =
    useState(initialValue);

    Keep state minimal.

    Functional update

    javascript
    setValue((current) => nextValue);

    Use when next state depends on previous state.

    Derived value

    javascript
    const total =
    price * quantity;

    Do not store unnecessarily.

    Effect

    javascript
    useEffect(() => {
    synchronize(value);
    return () => {
    cleanup();
    };
    }, [value]);

    Use for external synchronization.

    Ref

    javascript
    const ref = useRef(null);

    Use for mutable values that do not need to trigger rendering.

    Memoization

    javascript
    const result = useMemo(
    () => expensiveCalculation(data),
    [data]
    );

    Use for meaningful expensive calculations.

    Callback

    javascript
    const handleAction =
    useCallback(
    () => doSomething(value),
    [value]
    );

    Use when function identity matters.

    Reducer

    javascript
    const [state, dispatch] =
    useReducer(
    reducer,
    initialState
    );

    Use for complex state transitions.

    Custom Hook

    javascript
    const value =
    useSomething();

    Use for reusable stateful behavior.

    Rules

    text
    Top level
    +
    React component / Custom Hook

    Practice Exercises

    Exercise 1: Remove Derived State

    Refactor:

    javascript
    const [total, setTotal] =
    useState(0);
    useEffect(() => {
    setTotal(
    items.reduce(
    (sum, item) =>
    sum + item.price,
    0
    )
    );
    }, [items]);

    Remove unnecessary state and Effect.

    Exercise 2: Refactor an Effect Chain

    Given:

    text
    query
    ↓
    Effect
    ↓
    filteredProducts
    ↓
    Effect
    ↓
    total

    Refactor the design so derived values are calculated directly.

    Exercise 3: Improve State Ownership

    Create a dashboard with:

    • search
    • modal
    • product list

    Place each piece of state in the most appropriate component.

    Exercise 4: Functional Updates

    Rewrite:

    javascript
    setCount(count + 1);
    setCount(count + 1);

    so one event applies both increments.

    Exercise 5: Effect Cleanup

    Build a timer and ensure that:

    • the interval starts correctly
    • it stops correctly
    • no timers remain after unmount

    Exercise 6: Custom Hook API

    Design:

    javascript
    useSearch()

    with a clean return API.

    Do not expose internal implementation details.

    Exercise 7: Memoization Review

    Review a component containing five useMemo and three useCallback calls.

    For each one, decide whether it has a real performance or identity requirement.

    Remove unnecessary memoization.

    Exercise 8: Reducer Design

    Create a reducer for:

    text
    idle
    loading
    success
    error

    Use meaningful actions:

    text
    requestStarted
    requestSucceeded
    requestFailed
    reset

    Mini Project: Production-Ready React Feature

    Build a Product Search feature using the best practices from this lesson.

    Requirements:

    State

    Manage only necessary UI state.

    text
    query
    selectedProduct

    Derived Data

    Calculate:

    text
    filteredProducts
    resultCount

    without redundant state.

    Data Fetching

    Use a focused Custom Hook:

    javascript
    useProducts()

    Handle:

    text
    loading
    success
    error

    Search

    Create:

    javascript
    useDebounce()

    if debouncing is appropriate.

    Product Items

    Use a separate component:

    javascript
    <ProductItem />

    with stable keys.

    Performance

    Use:

    text
    useMemo
    useCallback
    React.memo

    only when there is a demonstrated reason.

    Side Effects

    Ensure:

    • correct dependencies
    • cleanup
    • request cancellation when appropriate
    • no unnecessary Effect chains

    Architecture

    Aim for:

    text
    ProductPage
    │
    ├── useProducts()
    │
    ├── useDebounce()
    │
    ├── SearchInput
    │
    ├── ProductList
    │ └── ProductItem
    │
    └── ProductDetails

    Review the completed project using the Hook code review checklist.

    Interview questions & answers

    Interview preparation

    12 questions grouped by difficulty — expand any card for a model answer and follow-up probe.

    Beginner

    4
    1. 1What is the most important Hook best practice?
      Beginner

      Model answer

      Use the simplest correct state and synchronization model.

      Avoid unnecessary state, Effects, and memoization.

    2. 2Why should derived data usually not be stored in state?
      Beginner

      Model answer

      Because it duplicates information that can be calculated from existing state or props, increasing synchronization complexity and potentially causing unnecessary renders.

    3. 3What is a good reason to create a Custom Hook?
      Beginner

      Model answer

      To encapsulate reusable stateful behavior or a meaningful synchronization pattern.

    4. 4What is the difference between useRef and useState?
      Beginner

      Model answer

      useState stores data that participates in rendering. useRef stores a persistent mutable value without causing a re-render when .current changes.

    Intermediate

    4
    1. 5Should every derived value use useMemo?
      Intermediate

      Model answer

      No. Use useMemo when the calculation is expensive or memoization provides a meaningful benefit.
    2. 6What should useEffect be used for?
      Intermediate

      Model answer

      Primarily for synchronizing a component with external systems such as APIs, subscriptions, timers, browser APIs, and third-party libraries.

    3. 7What should a reducer do?
      Intermediate

      Model answer

      A reducer should calculate the next state from the current state and action.

      It should be pure and avoid side effects.

    4. 8Why should Effects have cleanup?
      Intermediate

      Model answer

      Cleanup releases external resources created by the Effect and keeps synchronization aligned with the component lifecycle.

    Advanced

    4
    1. 9Should every callback use useCallback?
      Advanced

      Model answer

      No. Use useCallback when stable function identity matters.
    2. 10Should user actions be handled with useEffect?
      Advanced

      Model answer

      Usually not when the action is directly caused by an event.

      Use an event handler when appropriate.

    3. 11Why is state placement important?
      Advanced

      Model answer

      State placement affects ownership, component coupling, and the scope of updates.

      Keep state local when possible and lift it only when multiple components need it.

    4. 12Why should you measure before optimizing?
      Advanced

      Model answer

      Because memoization and other optimizations add complexity and may not improve the actual bottleneck.

    Final Hook Best Practices Checklist

    Before considering a Hook-based feature complete, ask:

    State

    • [ ] Is all state necessary?
    • [ ] Can any value be derived?
    • [ ] Is state owned by the right component?
    • [ ] Are state updates immutable?
    • [ ] Are functional updates used where appropriate?

    Effects

    • [ ] Does each Effect synchronize with an external system?
    • [ ] Are dependencies accurate?
    • [ ] Is cleanup implemented where required?
    • [ ] Can the Effect create a feedback loop?
    • [ ] Can asynchronous work race?
    • [ ] Is cancellation needed?

    Refs

    • [ ] Does this value actually need to avoid rendering?
    • [ ] Would state be more appropriate?

    Memoization

    • [ ] Is useMemo justified?
    • [ ] Is useCallback justified?
    • [ ] Is React.memo justified?
    • [ ] Have you measured the problem?

    Reducers

    • [ ] Are transitions complex enough for a reducer?
    • [ ] Are actions meaningful?
    • [ ] Is the reducer pure?
    • [ ] Is state immutable?

    Custom Hooks

    • [ ] Does the Hook represent a meaningful behavior?
    • [ ] Is the API clear?
    • [ ] Is the Hook focused?
    • [ ] Does it follow the Rules of Hooks?

    Components

    • [ ] Are responsibilities clear?
    • [ ] Are repeated stateful items separate components?
    • [ ] Are keys stable?
    • [ ] Is state unnecessarily lifted?

    Architecture

    • [ ] Is data flow easy to follow?
    • [ ] Are side effects isolated?
    • [ ] Is server state handled appropriately?
    • [ ] Are abstractions justified?

    Key Takeaways

    Ready to mark this lesson complete?Track your journey across the entire course.