Skip to main content
Lead Generation Websites, Google Maps Ranking, WhatsApp Funnels, Ecommerce, SEO, Web DesignSpeed Optimization · Conversion Optimization · Monthly Lead Systems · AI AutomationLead Generation Websites, Google Maps Ranking, WhatsApp Funnels, Ecommerce, SEO, Web Design

React Hooks Explained: useState and useEffect With Real Website Examples

Published: January 30, 2026
Written by Sumeet Shroff
React Hooks Explained: useState and useEffect With Real Website Examples
Table of Contents
  1. Introduction: Why React Hooks?
  2. useState Deep Dive
  3. useEffect Fundamentals
  4. Common Patterns: State Management with useState
  5. Side Effects: Data Fetching, Subscriptions, and Cleanup
  6. Dependency Array and Performance Pitfalls
  7. When to Use useReducer Instead of useState
  8. Case Study 1: Building a Blog List (fetch with useEffect)
  9. Case Study 2: Form State and Validation with useState
  10. Advanced Tips: useMemo, useCallback and Avoiding Re-renders
  11. Conclusion and Best Practices Checklist
  12. About Prateeksha Web Design

Introduction: Why React Hooks?

React Hooks are functions that let you "hook into" React state and lifecycle features from function components. Introduced to address longstanding pain points with class components, hooks enable you to write clearer, more reusable, and more testable UI logic without changing your component hierarchy.

Why were hooks added?

  • Classes made it harder to share stateful logic (higher-order components and render props were workarounds, but often verbose).
  • Lifecycle methods grew crowded: mounting, updating, and unmounting logic for a single concern often ended up split across multiple lifecycle methods (componentDidMount, componentDidUpdate, componentWillUnmount), which made reasoning and reuse difficult.
  • Function components were simpler, but lacked state and lifecycle features; hooks bring those capabilities back while keeping functions concise.

High-level trade-offs compared to class components

  • Simplicity and composition: Hooks encourage splitting logic by concern (e.g., a custom hook for form input), avoiding lifecycle method scattering.
  • Less boilerplate: No need for this, constructor binding, or large lifecycle method blocks.
  • Mental model shift: You reason about effects (useEffect) and state (useState) instead of lifecycle callbacks and instance methods.
  • Learning curve: The rules of hooks and dependency arrays require attention; misuse can cause subtle bugs.

What problems do hooks solve in real websites?

  • Shared logic across components becomes custom hooks (e.g., useAuth, useFetch) instead of HOCs.
  • Side effects and subscriptions are expressed declaratively and cleaned up automatically, reducing memory leaks on long-lived pages.
  • Local state for forms, UI toggles, and lists is easier to write and test in functional components.

Learning objectives for this part

  • Understand what React Hooks are and why they matter.
  • Recognize differences between functional components with hooks and class-based components.
  • Know the common hooks you'll use in everyday apps.

Prerequisites

  • Basic knowledge of JavaScript and React fundamentals
FactHooks are backward-compatible: you can adopt them gradually in an existing codebase. They don't replace classes immediately; they offer an alternative API for components.
TipStart by converting small, stateful class components to function components with hooks. This helps you learn the new mental model without touching the whole app.
WarningDon't call hooks conditionally or inside loops—always call them at the top level of your component or custom hook. Violating the rules of hooks breaks React's internal tracking.

Further Reading


useState Deep Dive

useState is the most basic hook for adding local, reactive state to function components. It returns a state value and a setter function, and re-renders the component when you call the setter with a new value.

Basic usage example

import React, { useState } from 'react';

function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }

Key concepts

  • Initial state: useState accepts an initial value. This value is used only during the first render.
  • Setter function: The updater returned by useState replaces the state value for that hook. For primitive values you pass the new value; for objects or arrays, you typically create a new object/array (immutably) and pass it to the setter.
  • Multiple state variables: You can call useState multiple times to manage independent pieces of state instead of keeping one big object.

Lazy initialization

If computing the initial state is expensive, pass a function to useState. React will call it only on the initial render:

const [data, setData] = useState(() => expensiveInit());

Functional updates for correctness

When a state update depends on the previous state, use the functional form of the setter to avoid stale closures (especially inside event handlers or timeouts):

setCount(prev => prev + 1);

This is important when multiple updates may be batched; the functional update always receives the freshest previous value.

Managing complex state shapes

  • Prefer multiple useState calls for independent values. This keeps updates local and reduces accidental overwrites:
const [title, setTitle] = useState('');
const [tags, setTags] = useState([]);
  • If state is naturally grouped (like a form), you can store an object, but remember to merge immutably:
const [form, setForm] = useState({ name: '', email: '' });
setForm(prev => ({ ...prev, email: 'new@example.com' }));

When to consider useReducer

If state transitions are complex, involve many sub-values, or require predictable update logic (e.g., undo/redo), useReducer provides a centralized reducer-based approach that scales better than many interdependent useState calls.

Common pitfalls

  • Mutating state directly (e.g., pushing into an array) prevents React from detecting changes. Always return new objects/arrays.
  • Reading stale state in async callbacks — prefer functional updates or refs when necessary.
TipFavor small, focused pieces of state. Splitting state into multiple useState hooks often leads to clearer updates and fewer bugs.
FactCalling useState triggers a re-render when the setter is used with a value that is !== (by reference) the previous value. For objects and arrays, create new instances to trigger updates.
WarningAvoid deep nested state objects if you frequently update inner fields—this causes more boilerplate and increases the chance of accidental mutation. Consider useReducer or normalizing state instead.

Further Reading


useEffect Fundamentals

useEffect is the hook for running side effects in functional components. Side effects include data fetching, subscriptions, manually mutating the DOM, timers, and logging. Unlike render logic, effects run after React has updated the DOM.

Basic example — fetch on mount

import React, { useEffect, useState } from 'react';

function UserList() { const [users, setUsers] = useState([]);

useEffect(() => { fetch('/api/users') .then(res => res.json()) .then(setUsers) .catch(console.error); }, []); // empty dependency array -> run once on mount

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

When effects run

  • By default (no dependency array), the effect runs after every completed render.
  • With an empty dependency array ([]), the effect runs once after the initial mount.
  • With dependencies ([dep1, dep2]), the effect runs after the first render and whenever any dependency changes (by reference inequality).

Cleanup functions

Effects can return a cleanup function. React runs the cleanup before the effect runs again, and on unmount. Use cleanup to unsubscribe, clear timers, or cancel network requests:

useEffect(() => {
  const id = setInterval(() => setTick(t => t + 1), 1000);
  return () => clearInterval(id);
}, []);

Dependency array rules and common gotchas

  • Always include all external values referenced inside an effect (props, state, functions) in the dependency array unless you intentionally want stale values. ESLint's "rules of hooks" plugin can help enforce this.
  • Functions defined inside the component are re-created each render. If you pass such a function as a dependency it will change each render unless memoized (useCallback). Often it's simpler to move effect logic into the effect itself or use refs.
  • Over-including dependencies can cause extra runs; under-including can cause stale data or missed updates.

Stale closures and how to avoid them

A common source of bugs is using state values inside an effect closure that become stale. For example, an interval capturing an old state value will repeatedly use that old value. Solutions:

  • Use functional updates on state setters inside the effect (e.g., setCount(c => c + 1)).
  • Store mutable values in refs (useRef) that effects can read/write without causing re-renders.

Example — cleanup on unmount (subscription)

useEffect(() => {
  const handle = subscribeToService(data => setData(data));
  return () => handle.unsubscribe();
}, [subscribeToService]);

Best practices for real websites

  • Keep effect bodies focused: an effect should do one job (fetching, subscribing, syncing). If you need multiple behaviors, use multiple useEffect calls.
  • Cancel or ignore stale async results. If a component unmounts before a fetch completes, guard setState calls or use AbortController.
  • Prefer declarative subscriptions and use cleanup to avoid leaks, especially when components mount/unmount often (modal dialogs, routes).
TipSplit complex side-effect logic into multiple effects: one effect for fetching, one for subscriptions, and one for DOM sync. This makes dependency arrays smaller and easier to reason about.
FactReact runs the cleanup for the previous effect before running the next effect. This ordering helps you avoid race conditions when updating subscriptions or timers.
WarningNever mutate state inside an effect without using the setter; direct mutation won't trigger a re-render and can cause inconsistent UI.

Further Reading


Common Patterns: State Management with useState

Practical UI state is the bread-and-butter of React apps. useState is small but powerful — this section shows readable, immutable patterns for toggles, counters, lists, and forms, plus guidance on when to lift state up.

Why idiomatic patterns matter: keeping updates immutable and predictable makes components easier to test, reason about, and avoid subtle bugs from stale closures or mutated state.

Simple toggle

A very common UI element is a boolean toggle (e.g., open/closed, on/off). Keep it local when no other component needs the value.

function FavoriteButton() {
  const [isFavorited, setIsFavorited] = useState(false);
  return (
    <button onClick={() => setIsFavorited(prev => !prev)}>
      {isFavorited ? 'Unfavorite' : 'Add to favorites'}
    </button>
  );
}

Notes:

  • Use the functional form setState(prev => next) when the new value depends on the previous one. This avoids race conditions when multiple updates happen quickly.
TipPrefer the functional updater (setState(prev => ...)) whenever you derive new state from previous state — it prevents bugs when React batches updates.

Counters and incremental state

For counters or numeric state, also prefer the functional update:

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(c => c - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

Lists and immutable updates

When updating arrays or objects, never mutate the existing state. Create new arrays/objects so React can detect changes.

Add an item:

function TodoList() {
  const [todos, setTodos] = useState([{id: 1, text: 'Buy milk'}]);

function addTodo(text) { setTodos(prev => [ ...prev, { id: Date.now(), text } ]); }

// ...rendering }

Toggle a todo's completed flag immutably:

setTodos(prev => prev.map(t => t.id === id ? { ...t, completed: !t.completed } : t));

Remove an item:

setTodos(prev => prev.filter(t => t.id !== id));
FactReact re-renders when a state setter receives a new reference. Returning a new array/object ensures React notices the change.

Forms and controlled inputs

Controlled components keep input state in React. For simple forms, use one useState per input or a single object for grouped inputs.

Single-input example:

function NameField() {
  const [name, setName] = useState('');
  return (
    <input value={name} onChange={e => setName(e.target.value)} placeholder="Your name" />
  );
}

Grouped inputs (one state object):

function SignupForm() {
  const [form, setForm] = useState({ email: '', password: '' });

function updateField(e) { const { name, value } = e.target; setForm(prev => ({ ...prev, [name]: value })); }

return ( <form> <input name="email" value={form.email} onChange={updateField} /> <input name="password" value={form.password} onChange={updateField} /> </form> ); }

When to lift state up vs keep local

  • Keep state local when only one component needs it (e.g., a toggle inside a modal). Local state is simpler and faster.
  • Lift state up when siblings need to coordinate or multiple components must reflect the same value (see React docs: Lifting State Up).
  • Consider composition or context for deeply nested sharing; don’t overuse context for frequently changing values (it causes many re-renders).
WarningDon’t prematurely lift everything to a parent. Lifting increases component coupling and can cause unnecessary renders if not managed carefully.

Quick performance tips

  • Use functional updates to avoid stale reads.
  • Avoid storing derived data in state; compute it from props/state during render.
  • For large lists, consider list virtualization instead of storing different representations in state.

Further Reading


Side Effects: Data Fetching, Subscriptions, and Cleanup

useEffect is where components interact with the outside world: network requests, timers, subscriptions, and DOM APIs. This section focuses on safe async fetching, cancellation, and reliable cleanup.

Fetching data with loading and error states

A reliable pattern contains three pieces of state: data, loading, and error. Use async functions inside effects and handle cancellation.

import axios from 'axios';

function UsersList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);

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

async function fetchUsers() {
  try {
    setLoading(true);
    const res = await axios.get('/api/users', { signal: controller.signal });
    setUsers(res.data);
  } catch (err) {
    if (!axios.isCancel(err)) setError(err);
  } finally {
    setLoading(false);
  }
}

fetchUsers();
return () =&gt; controller.abort();

}, []);

// render loading, error, users }

Notes:

  • axios supports AbortController (modern versions). For fetch, pass controller.signal and call controller.abort() in cleanup.
  • Always check whether the error is due to cancellation and avoid setting state after unmount.

Preventing race conditions

Race conditions happen when multiple requests overlap and out-of-order responses could set stale data. Abort the previous request or track a mounted flag.

Pattern (request-scoped token):

useEffect(() => {
  let isStale = false;
  async function load() {
    const data = await fetchData();
    if (!isStale) setState(data);
  }
  load();
  return () => { isStale = true; };
}, [someDependency]);

A better approach is AbortController — it actually cancels network activity and is cleaner.

Timers and subscriptions

useEffect is also for attaching and cleaning up subscriptions and timers.

useEffect(() => {
  const id = setInterval(() => setNow(Date.now()), 1000);
  return () => clearInterval(id);
}, []);

For external subscriptions (WebSocket, Firebase, PubSub), return a cleanup that unsubscribes. Always set up and clean up in the same effect so lifecycle is predictable.

Integrating third-party libraries

When a library needs DOM nodes (e.g., a charting library), create the node in JSX and initialize in an effect. Cleanup by destroying the instance to avoid memory leaks.

useEffect(() => {
  const chart = createChart(containerRef.current, options);
  return () => chart.destroy();
}, [options]);
TipPut setup and teardown in the same useEffect. That guarantees the cleanup sees the same closure as the setup and prevents leaks or duplicated subscriptions.

Common mistakes to avoid

  • Don’t make the effect callback itself async (i.e., use async in the top-level function). Instead, define and call an async function inside the effect.
  • Don’t ignore cleanup — unsubscribing and aborting prevents memory leaks and state updates on unmounted components.
WarningSetting state after a component has unmounted causes warnings and potential memory leaks. Use cleanup (AbortController or unsubscribe) to prevent it.
FactuseEffect cleanup runs before the next effect invocation and when the component unmounts — that's how you cancel or teardown side effects reliably.

Further Reading


Dependency Array and Performance Pitfalls

The dependency array is the most important part of useEffect. It tells React when to re-run an effect. Misunderstanding it causes infinite loops, stale closures, and unnecessary work.

How React compares dependencies

React compares each dependency by reference (===). For primitives this is value-equality; for objects and functions it's reference equality. If a dependency is a new reference on every render, the effect runs every render.

Common mistakes and patterns

  1. Missing dependencies -> stale closures

If an effect uses a prop or state but you omit it from the dependency array, the effect captures an old value. The ESLint rule (react-hooks/exhaustive-deps) flags missing dependencies.

  1. Over-specifying -> infinite loops

If you pass a dependency that changes on every render (e.g., an inline object or function), the effect runs again and again.

Example of a problematic effect:

useEffect(() => {
  doSomething(options); // options is created inline
}, [options]);

// but options = { foo: props.foo } is recreated each render -> infinite loop

Solutions: memoize or move values out of render.

useCallback and useMemo

  • useCallback(fn, deps) returns a memoized function reference. Use it when you need to pass a stable callback to dependencies or child components.
  • useMemo(valueFn, deps) memoizes computed values.

Example: stable callback used by effect

const handle = useCallback(() => {
  /* uses props.x */
}, [props.x]);

useEffect(() => { window.addEventListener('resize', handle); return () => window.removeEventListener('resize', handle); }, [handle]);

When to memoize?

  • Memoize when a stable reference prevents unnecessary effect runs or expensive re-renders of children.
  • Don’t overuse memoization — it adds cognitive overhead and some runtime cost. Measure before optimizing.

ESLint's role

Enable react-hooks/exhaustive-deps. It suggests the correct dependency list. If the suggestion adds a function you intentionally want omitted, either restructure the code or wrap the function with useCallback and include it.

Pattern for effects that only run once

To run an effect only once (mount/unmount) pass an empty array []. Ensure the effect doesn’t use props/state that should be captured; if you must use them, consider whether the effect should rerun when those values change.

Avoiding infinite loops: example

useEffect(() => {
  setCount(c => c + 1); // updating state inside effect
}, []); // fine

// But if the effect depends on count and updates it, wrong dependency array causes loops

Debugging tips

  • Add console logs in the effect and dependency values.
  • Use the React DevTools Profiler to see how often components re-render.
  • Temporarily disable the effect and re-enable with varied dependency lists.
TipWhen ESLint adds many deps and it feels noisy, first try to refactor the effect to accept stable inputs (useMemo/useCallback) instead of silencing the linter.
WarningSilencing or ignoring exhaustive-deps (e.g., by adding // eslint-disable) hides real bugs. Use it rarely and with clear justification.
FactThe dependency array controls when an effect runs; React relies on reference equality, so functions and objects should be stable or explicitly memoized.

Further Reading


When to Use useReducer Instead of useState

useReducer provides a predictable reducer-based pattern for complex state transitions. It's useful when state updates are interrelated, when the next state depends on the previous in many ways, or when actions are easier to reason about than ad-hoc setters.

When prefer useReducer

  • Multiple related state variables change together.
  • Complex update logic (branches, reset, undo/redo).
  • You want to centralize state transition logic for testing.
  • You have action-driven state transitions (form wizards, finite state machines).

Basic reducer example (todos)

function todosReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, action.payload];
    case 'toggle':
      return state.map(t => t.id === action.id ? { ...t, completed: !t.completed } : t);
    case 'remove':
      return state.filter(t => t.id !== action.id);
    default:
      throw new Error('Unknown action');
  }
}

function Todos() { const [todos, dispatch] = useReducer(todosReducer, []);

// dispatch({ type: 'add', payload: newTodo }) }

Form wizard example

For a multi-step wizard with complex next/previous/reset logic, a reducer documents transitions clearly and is easier to test:

function wizardReducer(state, action) {
  switch (action.type) {
    case 'next':
      return { ...state, step: state.step + 1 };
    case 'prev':
      return { ...state, step: Math.max(0, state.step - 1) };
    case 'updateField':
      return { ...state, data: { ...state.data, [action.field]: action.value } };
    case 'reset':
      return initialState;
    default:
      return state;
  }
}

Advantages

  • Centralized update logic (single switch or map) makes behavior explicit.
  • Reducers are easy to unit-test: call reducer(oldState, action) and assert new state.
  • Dispatching actions decouples event handlers from the update logic.

When not to use useReducer

  • For a handful of independent primitives, useState is simpler and clearer.
  • Reducers add ceremony; avoid them for trivial local toggles or single inputs.

Combining useReducer with context

If many components need to dispatch actions to the same store-like state, combine useReducer with React Context to provide state and dispatch. Be cautious: frequent updates propagate via context and can cause widespread re-renders if not optimized (split contexts or memoize selectors if necessary).

TipIf you find many setState calls that must be coordinated, consider refactoring to useReducer — it often simplifies logic and improves testability.
WarningUsing a single big reducer for unrelated concerns (a "god" reducer) makes code hard to maintain. Split reducers or keep unrelated state local.
FactuseReducer is identical in concept to Redux reducers but scoped to component state. It gives the same benefits (predictability, testability) without global complexity.

Further Reading


Case Study 1: Building a Blog List (fetch with useEffect)

In this case study you'll build a simple BlogList that fetches posts from an API, shows loading and error states, and safely cancels in-flight requests when the component unmounts or query parameters change. We'll use axios for requests and useEffect for lifecycle behavior.

Why this pattern matters: network responses can arrive after a component unmounts or after another request started. Handling loading, errors, and cancellation prevents memory leaks, flicker, and incorrect UI updates.

Example structure (small components, composition):

  • BlogList — container that fetches and coordinates state
  • BlogItem — presentational component for a single post
  • Loader / ErrorBanner — small UI pieces

Code (concise):

import React, { useState, useEffect } from "react";
import axios from "axios";

function BlogItem({ post }) { return ( <article> <h3>{post.title}</h3> <p>{post.excerpt}</p> </article> ); }

export default function BlogList({ query }) { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null);

useEffect(() => { const controller = new AbortController(); const source = controller.signal;

async function fetchPosts() {
  setLoading(true);
  setError(null);
  try {
    const res = await axios.get(&quot;/api/posts&quot;, {
      params: { q: query },
      signal: source,
    });
    setPosts(res.data);
  } catch (err) {
    // axios throws a special error for cancellations
    if (axios.isCancel &amp;&amp; axios.isCancel(err)) return;
    if (err?.name === &quot;CanceledError&quot; || err?.code === &quot;ERR_CANCELED&quot;) return;
    setError(err);
  } finally {
    setLoading(false);
  }
}

fetchPosts();

return () =&gt; controller.abort();

}, [query]);

if (loading) return <div>Loading posts...</div>; if (error) return ( <div> <p>Something went wrong: {error.message}</p> <button onClick={() => window.location.reload()}>Retry</button> </div> );

return ( <section> {posts.map((p) => ( <BlogItem key={p.id} post={p} /> ))} </section> ); }

Key points explained:

  • Use a loading boolean and an error state to represent UI status explicitly.
  • Pass an AbortController.signal to axios (modern axios supports it). On cleanup, call controller.abort() to cancel the request.
  • Check for canceled requests in the catch block and avoid setting state in that case.
  • Keep components small: BlogItem is purely presentational, making BlogList easier to test and reason about.
Tip When you need retries, exponential backoff, or caching consider adding a thin abstraction (useFetch hook) or a library like SWR/React Query instead of reimplementing every strategy in components.

Further Reading

Case Study 2: Form State and Validation with useState

This example shows handling a multi-field form with useState, adding inline validation, debounced checks, and when it’s time to refactor to useReducer or a library.

Patterns covered:

  • Single object state vs many independent useState calls
  • Controlled inputs and a generic handleChange
  • Error state shaped like the form
  • Debounced validation with useEffect

Example form pattern:

import React, { useState, useEffect } from "react";

function validate(values) { const errs = {}; if (!values.name) errs.name = "Name is required"; if (!values.email) errs.email = "Email is required"; else if (!/^[^@]+@[^@]+.[^@]+$/.test(values.email)) errs.email = "Invalid email"; if (values.password && values.password.length < 8) errs.password = "Too short"; return errs; }

export default function SignupForm() { const [form, setForm] = useState({ name: "", email: "", password: "" }); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({});

function handleChange(e) { const { name, value } = e.target; setForm((s) => ({ ...s, [name]: value })); }

function handleBlur(e) { setTouched((t) => ({ ...t, [e.target.name]: true })); }

// Debounced validation useEffect(() => { const id = setTimeout(() => { setErrors(validate(form)); }, 400); return () => clearTimeout(id); }, [form]);

function handleSubmit(e) { e.preventDefault(); const errs = validate(form); setErrors(errs); setTouched({ name: true, email: true, password: true }); if (Object.keys(errs).length === 0) { // submit } }

return ( <form onSubmit={handleSubmit} noValidate> <label> Name <input name="name" value={form.name} onChange={handleChange} onBlur={handleBlur} /> {touched.name && errors.name && <div className="error">{errors.name}</div>} </label> <label> Email <input name="email" value={form.email} onChange={handleChange} onBlur={handleBlur} /> {touched.email && errors.email && <div className="error">{errors.email}</div>} </label> <label> Password <input type="password" name="password" value={form.password} onChange={handleChange} onBlur={handleBlur} /> {touched.password && errors.password && <div className="error">{errors.password}</div>} </label> <button type="submit">Sign up</button> </form> ); }

When to use which state shape:

  • Small forms (3–5 fields) — an object with useState is usually fine.
  • Very dynamic forms or complex update rules — useReducer makes state transitions explicit and testable.
  • Large forms with lots of validation, async checks, or performance needs — consider a form library (React Hook Form, Formik).
Fact Using a single object state keeps related fields together and makes generic handlers simple, but updating nested values requires careful immutable updates.

Further Reading

Advanced Tips: useMemo, useCallback and Avoiding Re-renders

Optimizations with useMemo and useCallback can reduce unnecessary work, but they come with costs (memory, code complexity). Apply these tactics only after measuring.

When to use:

  • useMemo: memoize expensive calculations (derived data) that are expensive to compute on every render.
  • useCallback: stabilize function references passed to children so that memoized children don’t re-render unnecessarily.

Examples:

const derived = useMemo(() => expensiveCalculation(items), [items]);
const onSelect = useCallback((id) => setSelected(id), []);

Rules of thumb:

  • Measure before optimizing: use the React Profiler to spot expensive renders.
  • Prefer keeping components small and pure — often splitting a large component into smaller ones yields better results than heavy memoization.
  • Don’t wrap everything in useMemo/useCallback; each call has an overhead. Use when a clear render-cost win exists.
  • Remember reference equality: arrays and objects are new on every render unless memoized — this is a common source of extra renders.

Common misuse to avoid:

  • Memoizing very cheap values (strings, small arrays) where the overhead of memoization exceeds the savings.
  • Using stable callbacks without considering dependencies, which leads to stale closures.
Tip Start by memoizing the child component (React.memo) and only memoize values or callbacks if the child still re-renders too often.

Further Reading

Conclusion and Best Practices Checklist

This part ties together patterns you used in the case studies and offers a compact checklist for everyday development.

Core takeaways:

  • Keep effects focused and idempotent: each useEffect should do one job (fetch, subscribe, DOM sync).
  • Always declare effect dependencies; if you disable lints, document why carefully.
  • Clean up subscriptions, timers, and network calls in the effect cleanup to avoid leaks and stale updates.
  • Use useState for local UI state; refactor to useReducer when updates become complex or when actions are easier to reason about.
  • Optimize only after profiling. Prefer component decomposition and pure components before adding memoization.

Best Practices Checklist (copyable):

  • Use explicit loading and error states for async flows.
  • Cancel or ignore in-flight requests on unmount/change (AbortController or axios cancellation).
  • Keep effects' responsibilities small and well-scoped.
  • List all external dependencies in the dependency array; prefer functions stable via useCallback if required.
  • Avoid stale closures by referencing current values or using refs for mutable values.
  • Normalize complex state shapes and consider useReducer or external libraries for large forms.
  • Profile before applying useMemo/useCallback. Measure the impact.
  • Write small presentational components to reduce re-render surface area.

Next steps and resources:

  • Practice: convert a class-based fetch component to a hooks-based version and add cancellation.
  • Try: build a form with client-side and async validation; then refactor it to React Hook Form.
Warning Don’t silence exhaustive-deps lint warnings without understanding the implications — missing dependencies cause bugs that are often intermittent and hard to trace.

Further Reading

About Prateeksha Web Design

Prateeksha Web Design helps businesses turn tutorials like "React Hooks Explained: useState and useEffect With Real Website Examples" into real-world results with custom websites, performance optimization, and automation. From strategy to implementation, our team supports you at every stage of your digital journey.

Chat with us now Contact us today.

Sumeet Shroff
Sumeet Shroff
Sumeet Shroff is a renowned expert in web design and development, sharing insights on modern web technologies, design trends, and digital marketing.

Comments

Leave a Comment

Loading comments...