Real Hook Examples
Real Hook examples combine useState, useEffect, useRef, useMemo, useCallback, useReducer, and custom Hooks into practical application patterns.
Introduction
React Hooks become much easier to understand when you see how they solve real application problems.
So far, you have learned individual Hooks such as:
useStateuseEffectuseRefuseMemouseCallbackuseReducer- Custom Hooks
The next step is to combine them correctly.
Real applications rarely use a Hook in isolation. A search box may use state, an Effect, a ref, and a Custom Hook. A form may use state or a reducer. A data table may use memoization. A modal may use refs and Effects.
This lesson brings those ideas together through practical examples.
The goal is not to memorize patterns. The goal is to recognize which Hook solves which problem and how Hooks work together while keeping components predictable.
1. A Practical Hook Decision Process
Before adding a Hook, ask:
What problem am I solving?|+-- UI data changes?| -> useState / useReducer|+-- External system synchronization?| -> useEffect|+-- Need a DOM node or mutable value?| -> useRef|+-- Expensive calculation?| -> useMemo|+-- Stable callback identity?| -> useCallback|+-- Reusable stateful logic?| -> Custom Hook
This prevents the common mistake of choosing a Hook first and then trying to find a reason to use it.
Example: Counter with Functional Updates
A counter is simple, but it demonstrates an important state pattern.
import { useState } from "react";function Counter() {const [count, setCount] = useState(0);function increment() {setCount((currentCount) => currentCount + 1);}return (<div><p>Count: {count}</p><button onClick={increment}>Increase</button></div>);}export default Counter;
Why use a functional update?
The next state depends on the previous state:
setCount((currentCount) => currentCount + 1);
This is preferable to manually depending on a potentially stale value when multiple updates are involved.
For example:
function increaseThreeTimes() {setCount((value) => value + 1);setCount((value) => value + 1);setCount((value) => value + 1);}
Each update receives the latest pending state.
Example: Form State with `useState`
For a small form, separate state variables can be clear and straightforward.
import { useState } from "react";function LoginForm() {const [email, setEmail] = useState("");const [password, setPassword] = useState("");function handleSubmit(event) {event.preventDefault();console.log({email,password,});}return (<form onSubmit={handleSubmit}><inputtype="email"value={email}onChange={(event) => setEmail(event.target.value)}placeholder="Email"/><inputtype="password"value={password}onChange={(event) => setPassword(event.target.value)}placeholder="Password"/><button type="submit">Login</button></form>);}export default LoginForm;
This is a controlled form.
The React state represents the current value of each input.
Important principle
Do not create an Effect just to react to a button click:
// Usually unnecessaryuseEffect(() => {if (submitted) {submitForm();}}, [submitted]);
The user action already happened in the event handler.
Prefer:
function handleSubmit(event) {event.preventDefault();submitForm();}
Example: Search Input with Derived Data
Suppose a list is already available in memory and you want to filter it based on a search term.
import { useState } from "react";const products = [{ id: 1, name: "Laptop" },{ id: 2, name: "Keyboard" },{ id: 3, name: "Mouse" },{ id: 4, name: "Monitor" },];function ProductSearch() {const [search, setSearch] = useState("");const filteredProducts = products.filter((product) =>product.name.toLowerCase().includes(search.toLowerCase()));return (<div><inputvalue={search}onChange={(event) => setSearch(event.target.value)}placeholder="Search products"/><ul>{filteredProducts.map((product) => (<li key={product.id}>{product.name}</li>))}</ul></div>);}export default ProductSearch;
Notice that filteredProducts is derived data.
We do not need:
const [filteredProducts, setFilteredProducts] = useState([]);
and we do not need an Effect to keep it synchronized.
The source of truth is:
search
The filtered list is calculated from that state.
Example: Fetching Data with `useEffect`
Fetching data is a classic example of synchronizing with an external system.
import { useEffect, useState } from "react";function Users() {const [users, setUsers] = useState([]);const [status, setStatus] = useState("loading");useEffect(() => {let ignore = false;async function loadUsers() {try {const response = await fetch("https://jsonplaceholder.typicode.com/users");if (!response.ok) {throw new Error("Failed to load users");}const data = await response.json();if (!ignore) {setUsers(data);setStatus("success");}} catch (error) {if (!ignore) {setStatus("error");}}}loadUsers();return () => {ignore = true;};}, []);if (status === "loading") {return <p>Loading...</p>;}if (status === "error") {return <p>Unable to load users.</p>;}return (<ul>{users.map((user) => (<li key={user.id}>{user.name}</li>))}</ul>);}export default Users;
The cleanup prevents an outdated request from updating the component after the Effect has been cleaned up.
For production applications, you may also use request cancellation with AbortController or a data-fetching library.
Example: Search with an External API
Now combine state and Effects.
import { useEffect, useState } from "react";function UserSearch() {const [query, setQuery] = useState("");const [users, setUsers] = useState([]);const [status, setStatus] = useState("idle");useEffect(() => {if (!query.trim()) {setUsers([]);setStatus("idle");return;}const controller = new AbortController();async function searchUsers() {setStatus("loading");try {const response = await fetch(`https://jsonplaceholder.typicode.com/users?name_like=${encodeURIComponent(query)}`,{signal: controller.signal,});if (!response.ok) {throw new Error("Search failed");}const data = await response.json();setUsers(data);setStatus("success");} catch (error) {if (error.name !== "AbortError") {setStatus("error");}}}searchUsers();return () => {controller.abort();};}, [query]);return (<div><inputvalue={query}onChange={(event) => setQuery(event.target.value)}placeholder="Search users"/>{status === "loading" && <p>Searching...</p>}{status === "error" && <p>Search failed.</p>}<ul>{users.map((user) => (<li key={user.id}>{user.name}</li>))}</ul></div>);}export default UserSearch;
What is happening?
User types|vquery changes|vComponent renders|vEffect sees new query|vAPI request starts|+--> cleanup cancels previous request|vresponse arrives|vstate updates|vcomponent renders again
This is a useful real-world state/effect relationship.
Example: Debounced Search with `useEffect`
Searching on every keystroke can generate too many requests.
A simple debounce can delay the API request.
import { useEffect, useState } from "react";function SearchBox() {const [query, setQuery] = useState("");const [debouncedQuery, setDebouncedQuery] = useState("");useEffect(() => {const timer = setTimeout(() => {setDebouncedQuery(query);}, 400);return () => {clearTimeout(timer);};}, [query]);return (<div><inputvalue={query}onChange={(event) => setQuery(event.target.value)}placeholder="Search..."/><p>Searching for: {debouncedQuery}</p></div>);}export default SearchBox;
The cleanup clears the previous timer whenever the query changes.
This demonstrates an important Effect pattern:
setup();return cleanup;
The cleanup should undo or cancel the work established by the Effect.
Example: Focus an Input with `useRef`
Refs are useful when you need access to a DOM node.
import { useRef } from "react";function SearchForm() {const inputRef = useRef(null);function focusInput() {inputRef.current?.focus();}return (<div><input ref={inputRef} placeholder="Search..." /><button onClick={focusInput}>Focus Search</button></div>);}export default SearchForm;
The ref stores the DOM element:
inputRef|v<input />
Changing inputRef.current does not trigger a render.
That is why a ref is appropriate for imperative DOM operations.
Example: Focus After a Condition Changes
Sometimes a state change should result in an interaction with the DOM.
import { useEffect, useRef, useState } from "react";function EditableTitle() {const [editing, setEditing] = useState(false);const inputRef = useRef(null);useEffect(() => {if (editing) {inputRef.current?.focus();}}, [editing]);return (<div>{editing ? (<input ref={inputRef} defaultValue="My title" />) : (<button onClick={() => setEditing(true)}>Edit title</button>)}</div>);}export default EditableTitle;
Here the Effect has a legitimate synchronization role:
React state: editing|vDOM should be focused|vEffect performs focus()
Example: Expensive Calculation with `useMemo`
Imagine a component displaying thousands of products.
import { useMemo, useState } from "react";function ProductList({ products }) {const [search, setSearch] = useState("");const [showAvailableOnly, setShowAvailableOnly] = useState(false);const filteredProducts = useMemo(() => {return products.filter((product) => {const matchesSearch = product.name.toLowerCase().includes(search.toLowerCase());const matchesAvailability =!showAvailableOnly || product.available;return matchesSearch && matchesAvailability;});}, [products, search, showAvailableOnly]);return (<div><inputvalue={search}onChange={(event) => setSearch(event.target.value)}/><label><inputtype="checkbox"checked={showAvailableOnly}onChange={(event) =>setShowAvailableOnly(event.target.checked)}/>Available only</label><ul>{filteredProducts.map((product) => (<li key={product.id}>{product.name}</li>))}</ul></div>);}
Important
Do not add useMemo automatically just because filtering exists.
For small lists, this is often perfectly fine:
const filteredProducts = products.filter(...);
Use memoization when the calculation is sufficiently expensive or when stable derived values are useful for another optimization.
Example: Stable Callback with `useCallback`
Consider a memoized child component.
import { memo, useCallback, useState } from "react";const TodoItem = memo(function TodoItem({ todo, onToggle }) {console.log("Rendering:", todo.text);return (<li><span>{todo.text}</span><button onClick={() => onToggle(todo.id)}>Toggle</button></li>);});function TodoList() {const [todos, setTodos] = useState([{ id: 1, text: "Learn React", completed: false },{ id: 2, text: "Build a project", completed: false },]);const [theme, setTheme] = useState("light");const toggleTodo = useCallback((id) => {setTodos((currentTodos) =>currentTodos.map((todo) =>todo.id === id? { ...todo, completed: !todo.completed }: todo));}, []);return (<div className={theme}><buttononClick={() =>setTheme((current) =>current === "light" ? "dark" : "light")}>Toggle Theme</button><ul>{todos.map((todo) => (<TodoItemkey={todo.id}todo={todo}onToggle={toggleTodo}/>))}</ul></div>);}export default TodoList;
The important relationship is:
useCallback|vstable function reference|vReact.memo child|vcan skip unnecessary rendering
useCallback is not useful merely because a function exists.
Example: Shopping Cart with Derived Totals
A shopping cart demonstrates the difference between source state and derived values.
import { useState } from "react";function Cart() {const [items, setItems] = useState([{ id: 1, name: "Keyboard", price: 2000, quantity: 2 },{ id: 2, name: "Mouse", price: 1000, quantity: 1 },]);const total = items.reduce((sum, item) => sum + item.price * item.quantity,0);return (<div>{items.map((item) => (<div key={item.id}>{item.name} × {item.quantity}</div>))}<strong>Total: ₹{total}</strong></div>);}export default Cart;
There is no need for:
const [total, setTotal] = useState(0);
because:
items|vcalculate total|vtotal
The cart items are state.
The total is derived data.
Example: Complex Form with `useReducer`
When many state transitions exist, a reducer can make the logic easier to reason about.
import { useReducer } from "react";const initialState = {name: "",email: "",status: "idle",error: null,};function formReducer(state, action) {switch (action.type) {case "fieldChanged":return {...state,[action.field]: action.value,};case "submitted":return {...state,status: "submitting",error: null,};case "success":return {...state,status: "success",};case "failure":return {...state,status: "error",error: action.message,};default:throw new Error(`Unknown action: ${action.type}`);}}function RegistrationForm() {const [state, dispatch] = useReducer(formReducer,initialState);function handleSubmit(event) {event.preventDefault();dispatch({ type: "submitted" });// API call would normally happen here or in a separate// async layer, not inside the reducer.}return (<form onSubmit={handleSubmit}><inputvalue={state.name}onChange={(event) =>dispatch({type: "fieldChanged",field: "name",value: event.target.value,})}/><inputvalue={state.email}onChange={(event) =>dispatch({type: "fieldChanged",field: "email",value: event.target.value,})}/><button type="submit">Register</button>{state.status === "submitting" && <p>Submitting...</p>}{state.error && <p>{state.error}</p>}</form>);}export default RegistrationForm;
The reducer remains pure:
action|vreducer|vnew state
It should not perform network requests, DOM operations, timers, or other side effects.
Example: Reusable Local Storage Hook
A Custom Hook can package repeated stateful behavior.
import { useEffect, useState } from "react";function useLocalStorage(key, initialValue) {const [value, setValue] = useState(() => {const storedValue = localStorage.getItem(key);if (storedValue === null) {return initialValue;}try {return JSON.parse(storedValue);} catch {return initialValue;}});useEffect(() => {localStorage.setItem(key, JSON.stringify(value));}, [key, value]);return [value, setValue];}export default useLocalStorage;
Now a component can use it:
import useLocalStorage from "./useLocalStorage";function ThemeSelector() {const [theme, setTheme] = useLocalStorage("theme", "light");return (<buttononClick={() =>setTheme((current) =>current === "light" ? "dark" : "light")}>Theme: {theme}</button>);}
The component does not need to know how persistence works.
It only knows the Hook's API:
const [theme, setTheme] = useLocalStorage("theme", "light");
Example: Window Size Custom Hook
Another useful example is tracking browser dimensions.
import { useEffect, useState } from "react";function useWindowSize() {const [size, setSize] = useState({width: window.innerWidth,height: window.innerHeight,});useEffect(() => {function handleResize() {setSize({width: window.innerWidth,height: window.innerHeight,});}window.addEventListener("resize", handleResize);return () => {window.removeEventListener("resize", handleResize);};}, []);return size;}export default useWindowSize;
Usage:
import useWindowSize from "./useWindowSize";function LayoutInfo() {const { width, height } = useWindowSize();return (<p>Window: {width} × {height}</p>);}
The Hook owns the browser subscription and cleanup.
The component only consumes the result.
Example: Authentication State
A common application pattern is separating authentication state from UI components.
A simplified Custom Hook might look like:
import { useEffect, useState } from "react";function useCurrentUser() {const [user, setUser] = useState(null);const [status, setStatus] = useState("loading");useEffect(() => {async function loadUser() {try {const response = await fetch("/api/me");if (!response.ok) {setUser(null);setStatus("unauthenticated");return;}const data = await response.json();setUser(data);setStatus("authenticated");} catch {setStatus("error");}}loadUser();}, []);return {user,status,isAuthenticated: status === "authenticated",};}export default useCurrentUser;
A component can then remain focused on presentation:
function Profile() {const {user,status,isAuthenticated,} = useCurrentUser();if (status === "loading") {return <p>Loading profile...</p>;}if (!isAuthenticated) {return <p>Please log in.</p>;}return <h1>Welcome, {user.name}</h1>;}
The UI does not need to contain all authentication implementation details.
Example: Modal with `useState` and `useEffect`
A modal can combine state with an external browser effect.
import { useEffect, useState } from "react";function Modal({ onClose }) {useEffect(() => {function handleKeyDown(event) {if (event.key === "Escape") {onClose();}}window.addEventListener("keydown", handleKeyDown);return () => {window.removeEventListener("keydown", handleKeyDown);};}, [onClose]);return (<div role="dialog" aria-modal="true"><h2>Settings</h2><button onClick={onClose}>Close</button></div>);}function App() {const [open, setOpen] = useState(false);return (<div><button onClick={() => setOpen(true)}>Open Settings</button>{open && <Modal onClose={() => setOpen(false)} />}</div>);}export default App;
The Effect subscribes to a browser event while the modal exists and removes the subscription when the modal is removed.
Example: Timer with Cleanup
Timers are another classic Effect use case.
import { useEffect, useState } from "react";function Timer() {const [seconds, setSeconds] = useState(0);useEffect(() => {const intervalId = setInterval(() => {setSeconds((current) => current + 1);}, 1000);return () => {clearInterval(intervalId);};}, []);return <p>Elapsed: {seconds}s</p>;}export default Timer;
The relationship is:
Component mounts|vsetInterval()|vstate updates every second|vComponent unmounts|vclearInterval()
Without cleanup, the timer could continue running after the component is no longer present.
Example: Previous Value with `useRef`
Refs can store information from a previous render without causing another render.
import { useEffect, useRef } from "react";function PreviousValue({ value }) {const previousValue = useRef(value);useEffect(() => {previousValue.current = value;}, [value]);return (<div><p>Current: {value}</p><p>Previous: {previousValue.current}</p></div>);}export default PreviousValue;
Conceptually:
Render|+--> read ref|vEffect|+--> store current value for future render
The ref is not being used as UI state.
It is simply retaining a mutable value between renders.
Example: Combining Multiple Hooks
A realistic component may use several Hooks together.
import {useCallback,useEffect,useMemo,useRef,useState,} from "react";function Dashboard({ products }) {const [search, setSearch] = useState("");const [darkMode, setDarkMode] = useState(false);const inputRef = useRef(null);const filteredProducts = useMemo(() => {return products.filter((product) =>product.name.toLowerCase().includes(search.toLowerCase()));}, [products, search]);const clearSearch = useCallback(() => {setSearch("");inputRef.current?.focus();}, []);useEffect(() => {document.title = search? `Searching: ${search}`: "Product Dashboard";return () => {document.title = "React App";};}, [search]);return (<div className={darkMode ? "dark" : "light"}><button onClick={() => setDarkMode((current) => !current)}>Toggle theme</button><inputref={inputRef}value={search}onChange={(event) => setSearch(event.target.value)}/><button onClick={clearSearch}>Clear</button><p>{filteredProducts.length} products found</p><ul>{filteredProducts.map((product) => (<li key={product.id}>{product.name}</li>))}</ul></div>);}export default Dashboard;
Each Hook has a distinct responsibility:
| Hook | Responsibility |
|---|---|
useState | UI state |
useRef | DOM reference |
useMemo | Expensive/intentional derived calculation |
useCallback | Stable callback identity |
useEffect | Synchronization with the document |
This is the mindset to develop: Hooks should have clear responsibilities.
Example: Avoiding an Unnecessary Effect
Consider this:
function UserGreeting({ firstName, lastName }) {const [fullName, setFullName] = useState("");useEffect(() => {setFullName(`${firstName} ${lastName}`);}, [firstName, lastName]);return <h1>Hello, {fullName}</h1>;}
This creates redundant state.
Instead:
function UserGreeting({ firstName, lastName }) {const fullName = `${firstName} ${lastName}`;return <h1>Hello, {fullName}</h1>;}
The second version is simpler.
General rule
If a value can be calculated directly from props or state during rendering, it usually does not need:
- another state variable
- an Effect
- synchronization logic
Example: Avoiding an Effect for an Event
This pattern is often unnecessarily complicated:
function SaveButton() {const [shouldSave, setShouldSave] = useState(false);useEffect(() => {if (shouldSave) {saveDocument();}}, [shouldSave]);return (<button onClick={() => setShouldSave(true)}>Save</button>);}
The event already tells you why the save should happen.
Prefer:
function SaveButton() {function handleSave() {saveDocument();}return (<button onClick={handleSave}>Save</button>);}
This makes the data flow easier to understand.
Example: State vs Ref
Suppose you want to track the number of button clicks and display it.
Use state:
const [clicks, setClicks] = useState(0);
If you want to store an interval ID:
const intervalRef = useRef(null);
The difference is:
State-> changes should update UI-> causes re-renderRef-> value is needed across renders-> changing it does not cause re-render
Choosing incorrectly can make a component harder to reason about.
Example: Loading, Success, and Error States
Real applications should explicitly model asynchronous states.
function UserList() {const [status, setStatus] = useState("idle");const [users, setUsers] = useState([]);const [error, setError] = useState(null);// ...}
A useful state model is:
idle|vloading|+--> success|+--> error
Then render based on the status:
if (status === "loading") {return <Spinner />;}if (status === "error") {return <ErrorMessage message={error} />;}return <UserList users={users} />;
Explicit state models are easier to maintain than several unrelated boolean flags such as:
isLoadingisLoadedhasErrorisEmptyisFetching
which can accidentally represent contradictory combinations.
Example: Hook Composition
Custom Hooks become particularly powerful when they compose other Hooks.
function useOnlineStatus() {const [isOnline, setIsOnline] = useState(navigator.onLine);useEffect(() => {function handleOnline() {setIsOnline(true);}function handleOffline() {setIsOnline(false);}window.addEventListener("online", handleOnline);window.addEventListener("offline", handleOffline);return () => {window.removeEventListener("online", handleOnline);window.removeEventListener("offline", handleOffline);};}, []);return isOnline;}
Usage:
function Checkout() {const isOnline = useOnlineStatus();if (!isOnline) {return <p>You are offline.</p>;}return <button>Place Order</button>;}
The component does not manage browser events directly.
The Custom Hook owns that responsibility.
Example: Hook-Based Data Fetching Abstraction
A reusable data Hook can expose a predictable API.
import { useEffect, useState } from "react";function useUsers() {const [state, setState] = useState({status: "loading",data: [],error: null,});useEffect(() => {const controller = new AbortController();async function loadUsers() {try {const response = await fetch("/api/users", {signal: controller.signal,});if (!response.ok) {throw new Error("Failed to fetch users");}const data = await response.json();setState({status: "success",data,error: null,});} catch (error) {if (error.name === "AbortError") {return;}setState({status: "error",data: [],error,});}}loadUsers();return () => controller.abort();}, []);return state;}
A consumer can simply write:
function UsersPage() {const { status, data, error } = useUsers();if (status === "loading") {return <p>Loading...</p>;}if (status === "error") {return <p>{error.message}</p>;}return (<ul>{data.map((user) => (<li key={user.id}>{user.name}</li>))}</ul>);}
In larger applications, dedicated libraries for server-state management are often preferable to building a complete data-fetching system from scratch.
A Realistic Hook Architecture
A larger feature might look like this:
Page|+-- UI state| || +-- useState| +-- useReducer|+-- reusable behavior| || +-- useAuth| +-- useForm| +-- useDebounce| +-- useOnlineStatus|+-- external synchronization| || +-- useEffect|+-- DOM interaction| || +-- useRef|+-- performance optimization|+-- useMemo+-- useCallback
The architecture is easier to maintain when each responsibility has a clear home.
Hook Example: Debounce as a Custom Hook
The debounce example can be extracted into reusable logic.
import { useEffect, useState } from "react";function useDebouncedValue(value, delay = 400) {const [debouncedValue, setDebouncedValue] = useState(value);useEffect(() => {const timer = setTimeout(() => {setDebouncedValue(value);}, delay);return () => {clearTimeout(timer);};}, [value, delay]);return debouncedValue;}export default useDebouncedValue;
Now:
function SearchPage() {const [query, setQuery] = useState("");const debouncedQuery = useDebouncedValue(query, 500);// Use debouncedQuery for API searching.return (<inputvalue={query}onChange={(event) => setQuery(event.target.value)}/>);}
This is a good example of abstraction because the implementation detail is reusable and the public API is simple.
Hook Example: Form Field Abstraction
A small form Hook might look like:
import { useState } from "react";function useFormField(initialValue = "") {const [value, setValue] = useState(initialValue);function onChange(event) {setValue(event.target.value);}function reset() {setValue(initialValue);}return {value,onChange,reset,setValue,};}
Usage:
function ProfileForm() {const name = useFormField("");const email = useFormField("");return (<form><inputvalue={name.value}onChange={name.onChange}placeholder="Name"/><inputvalue={email.value}onChange={email.onChange}placeholder="Email"/><button type="button" onClick={name.reset}>Reset Name</button></form>);}
A Custom Hook should make repeated logic easier to use, not hide important behavior unnecessarily.
Common Real-World Mistakes
Mistake 1: State for derived values
Avoid:
const [total, setTotal] = useState(0);
when total can be calculated from existing state.
Mistake 2: Effect for every calculation
Avoid:
useEffect(() => {setFullName(firstName + lastName);}, [firstName, lastName]);
when the value can be derived during rendering.
Mistake 3: useMemo everywhere
Avoid:
const name = useMemo(() => user.name, [user.name]);
There is normally no meaningful performance benefit here.
Mistake 4: useCallback everywhere
Avoid wrapping every function:
const handleClick = useCallback(() => {console.log("clicked");}, []);
Use it when function identity matters to an optimization or another dependency relationship.
Mistake 5: Ref instead of state
Avoid using:
const valueRef = useRef(0);
for information that must appear in the UI.
Use state when the UI should update.
Mistake 6: Side effects inside reducers
Never put network requests or DOM manipulation inside a reducer.
Reducers should calculate the next state from:
previous state + action
Mistake 7: Ignoring cleanup
Subscriptions, timers, event listeners, and other external resources often require cleanup.
Mistake 8: Race conditions in asynchronous work
If multiple requests can overlap, an older response can arrive after a newer response.
Use appropriate cancellation or ignore/staleness handling.
Real-World Hook Combination Checklist
When building a feature, ask:
State
- What information actually changes over time?
- Does the UI need to re-render when it changes?
- Can some values be derived instead?
Effects
- Am I synchronizing with something outside React?
- Does the Effect have the correct dependencies?
- Does it need cleanup?
- Can asynchronous work become stale?
Refs
- Do I need a DOM node?
- Do I need mutable information that should survive renders without causing renders?
Memoization
- Is there a measured performance reason?
- Is the calculation actually expensive?
- Does stable identity matter?
Custom Hooks
- Is this stateful logic repeated?
- Can the behavior have a clean reusable API?
- Is the abstraction hiding complexity rather than adding it?
Mini Project: Searchable Product Dashboard
Build a small product dashboard using multiple Hooks.
Requirements
The application should:
- Display a list of products.
- Provide a search field.
- Filter products by name.
- Provide an availability filter.
- Show a loading state for simulated asynchronous data.
- Provide a clear-search button.
- Focus the search input when cleared.
- Display the number of matching products.
- Persist the selected theme.
- Keep reusable logic inside Custom Hooks where appropriate.
Suggested Hook usage
ProductDashboard|+-- useState| +-- search| +-- products|+-- useEffect| +-- load products|+-- useMemo| +-- filtered products|+-- useRef| +-- search input|+-- useCallback| +-- clear search|+-- Custom Hook+-- useLocalStorage for theme
Challenge
Do not add a Hook simply because the project contains several Hooks.
Every Hook should have a clear responsibility.
Practice Exercises
Exercise 1: Counter
Create a counter with:
- increment
- decrement
- reset
- increment by 5
Use functional state updates where appropriate.
Exercise 2: Todo List
Create a todo application with:
- add todo
- toggle completed
- delete todo
- filter all/active/completed
Do not store the filtered list as separate state.
Exercise 3: Search
Create a search component that:
- accepts a query
- debounces the query
- fetches results
- cancels outdated requests
- displays loading and error states
Exercise 4: Custom Hook
Create:
useToggle(initialValue)
It should return:
const [value, toggle] = useToggle(false);
Exercise 5: Previous Value
Create a component that displays:
Current value: 10Previous value: 8
Use useRef.
Exercise 6: Timer
Create a timer with:
- start
- pause
- reset
Make sure intervals are cleaned up correctly.
Exercise 7: Theme
Create a theme selector that:
- switches between light and dark
- persists the theme
- restores the theme when the page reloads
Extract persistence into a Custom Hook.
Interview questions & answers
Interview preparation
20 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1When should you use useState?
BeginnerModel answer
UseuseStatewhen a component owns UI values that change over time and should trigger a re-render, such as form fields, toggles, or local list filters.2What is the difference between state and derived data?
BeginnerModel answer
State is stored information that changes independently. Derived data is calculated from existing props or state during render—for example filtering a product list from a search term—so it usually should not be duplicated in anotheruseState.3What problem does useEffect solve?
BeginnerModel answer
useEffectsynchronizes a component with an external system after render, such as fetching data, timers, subscriptions, or updating the document title—not ordinary calculations that belong in render.4What is useRef used for?
BeginnerModel answer
useRefholds a persistent mutable value across renders without causing updates. Common uses are DOM node references, previous values, timers IDs, and third-party library instances.5Why does changing a ref not cause a re-render?
BeginnerModel answer
Updatingref.currentmutates a box React keeps for you; it is not state. React only re-renders when state or props change, so ref mutations alone do not schedule a render.
Intermediate
6When should you use a functional state update?
IntermediateModel answer
Use the functional updater when the next state depends on the previous state—for example incrementing a counter from its prior value—especially with batched or repeated updates where reading the current variable can be stale.
7Why should Effects usually synchronize with external systems?
IntermediateModel answer
Effects exist to keep React in sync with something outside React.
Using them for derived values or event responses adds unnecessary timing complexity; calculate during render or handle events directly instead.
8Why is cleanup important?
IntermediateModel answer
Cleanup cancels or undoes work the Effect started—clearing timers, aborting fetches, or removing listeners—so stale updates and leaks do not happen when dependencies change or the component unmounts.
9When should you use useMemo?
IntermediateModel answer
UseuseMemowhen a calculation is expensive enough to skip on most renders, or when you need a stable derived reference for another optimization. Do not wrap every filter or map by default.10When should you use useCallback?
IntermediateModel answer
UseuseCallbackwhen a stable function identity matters—typically a callback passed to aReact.memochild or listed in another Hook's dependency array—not merely because a function exists.11What makes a Custom Hook useful?
IntermediateModel answer
A Custom Hook extracts reusable React-specific behavior (state, Effects, refs) behind a clearuse-prefixed API so multiple components share logic without sharing the same state instance.12Why should reducers remain pure?
IntermediateModel answer
Pure reducers receive state and an action and return the next state without side effects or mutation. That keeps transitions predictable, testable, and easy to reason about insideuseReducer.13How can asynchronous Effects create race conditions?
IntermediateModel answer
If a request finishes after a newer dependency change or unmount, it can still callsetStatewith stale data. Cleanup must ignore or abort outdated responses so only the latest relevant result updates state.
Advanced
14How would you design a reusable data-fetching Hook?
AdvancedModel answer
Expose status, data, error, and actions; keep the fetch inside an Effect with dependency-driven re-runs; own cleanup for cancellation; and hide service details so UI components only consume a small Hook API.
15How would you prevent stale network responses from updating state?
AdvancedModel answer
Track a request id or useAbortControllerin Effect cleanup. Ignore responses that no longer match the current query, or abort the previous request whenever dependencies change.16How do React.memo and useCallback work together?
AdvancedModel answer
React.memoskips re-rendering a child when props are referentially equal.useCallbackkeeps a parent callback stable so the memoized child is not forced to re-render by a new function identity every parent render.17When can memoization make performance worse?
AdvancedModel answer
Memoization adds dependency tracking and memory overhead. If the work is cheap or dependencies change almost every render,useMemo/useCallbackcan cost more than they save.18How would you decide whether logic belongs in a component or Custom Hook?
AdvancedModel answer
Keep one-off UI wiring in the component. Extract a Custom Hook when the behavior is reusable, substantial, or mixes several Hooks into a named capability such asuseDebouncedValueoruseLocalStorage.19How would you model complex asynchronous UI states?
AdvancedModel answer
Model explicit statuses such as idle, loading, success, and error—often withuseReduceror a small state machine—so the UI can render each path clearly instead of juggling many loosely related booleans.20How would you separate server state from local UI state?
AdvancedModel answer
Keep remote data (lists, details, cache) in a fetch/data layer or server-state library, and keep ephemeral UI concerns (modals, draft inputs, selected tabs) in localuseState/ feature Hooks.
Quick Reference
| Problem | Typical solution |
|---|---|
| UI value changes | useState |
| Complex state transitions | useReducer |
| Synchronize with external system | useEffect |
| DOM reference | useRef |
| Mutable value without render | useRef |
| Expensive calculation | useMemo |
| Stable callback identity | useCallback |
| Reusable stateful behavior | Custom Hook |
| Derived value | Calculate during render |
| Timer/subscription | useEffect + cleanup |
| Browser event subscription | useEffect + cleanup |
| Focus DOM element | useRef |
| Persistent browser storage | Custom Hook + Effect |