React Hooks Explained: useState and useEffect With Real Website Examples

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
Further Reading
- React – Introducing Hooks (official): https://reactjs.org/docs/hooks-intro.html — Official motivation and introduction to hooks from the React team.
- Overreacted – A Complete Guide to useEffect: https://overreacted.io/a-complete-guide-to-useeffect/ — Deep conceptual article by Dan Abramov explaining useEffect semantics.
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.
Further Reading
- React – useState Hook: https://reactjs.org/docs/hooks-state.html — Official docs including examples and notes on functional updates.
- FreeCodeCamp – useState guide: https://www.freecodecamp.org/news/react-usestate-guide/ — Practical examples and common pitfalls.
- MDN Web Docs – JavaScript Closures: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures — Background on closures useful for updater functions and event handlers.
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).
Further Reading
- React – useEffect Hook: https://reactjs.org/docs/hooks-effect.html — Official documentation with examples and rules of hooks.
- Overreacted – A Complete Guide to useEffect: https://overreacted.io/a-complete-guide-to-useeffect/ — In-depth conceptual explanation of how effects work and common gotchas.
- MDN Web Docs – Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API — Background on fetching data from APIs for examples.
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.
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));
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).
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
- React – Lifting State Up — Guidance on sharing state between components.
- Kent C. Dodds – Patterns for managing state — Practical patterns and decision guides for state management.
- FreeCodeCamp – Practical useState examples — Real-world examples for form state and list updates.
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 () => 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]);
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.
Further Reading
- Axios – GitHub — Popular HTTP client used in real-world React data fetching examples.
- React – Using the Effect Hook (examples) — Examples combining fetching and cleanup.
- MDN Web Docs – AbortController — Useful for canceling fetch requests in cleanup logic.
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
- 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.
- 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.
Further Reading
- React – Hooks FAQ — Guidance on common hooks questions including dependencies.
- Overreacted – Why Effect Dependencies Matter — Explanations of stale closures and dependency reasoning.
- Kent C. Dodds – useEffect and dependencies — Practical tips for managing dependencies and effects.
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).
Further Reading
- React – useReducer Hook — Official reference and examples for useReducer.
- FreeCodeCamp – useReducer examples — Practical patterns and transition examples.
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("/api/posts", { params: { q: query }, signal: source, }); setPosts(res.data); } catch (err) { // axios throws a special error for cancellations if (axios.isCancel && axios.isCancel(err)) return; if (err?.name === "CanceledError" || err?.code === "ERR_CANCELED") return; setError(err); } finally { setLoading(false); } } fetchPosts(); return () => 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.
Further Reading
- Axios – GitHub: https://github.com/axios/axios — Used in the example for HTTP requests.
- React – Using the Effect Hook: https://reactjs.org/docs/hooks-effect.html — Reference for effect behavior and cleanup.
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).
Further Reading
- MDN Web Docs – Form Validation: https://developer.mozilla.org/en-US/docs/Learn/Forms — Background on HTML form patterns and validation.
- FreeCodeCamp – React form handling: https://www.freecodecamp.org/news/form-handling-in-react/ — Practical guide to managing form state.
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.
Further Reading
- React – Hooks FAQ / Performance: https://reactjs.org/docs/hooks-faq.html#how-do-i-avoid-passing-callbacks-down — Official guidance on avoiding unnecessary renders and memoization.
- Kent C. Dodds – When to useCallback/useMemo: https://kentcdodds.com/blog/usememo-and-usecallback — Practical advice and anti-patterns for memoization.
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.
Further Reading
- React – Hooks Rules and Best Practices: https://reactjs.org/docs/hooks-rules.html — Rules of hooks and recommended practices.
- Overreacted – useEffect in depth: https://overreacted.io/a-complete-guide-to-useeffect/ — Essential conceptual background to reinforce best practices.
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.