Hook Best Practices
Hook best practices cover keeping state minimal, deriving values, using Effects for synchronization, naming custom Hooks clearly, and optimizing intentionally.
React Hooks make it possible to build stateful, reusable, and maintainable function components.
But knowing how to use useState, useEffect, useRef, useMemo, useCallback, useReducer, and Custom Hooks is only the beginning.
Good React development also requires knowing how to use Hooks well.
Poor Hook usage can lead to:
- unnecessary re-renders
- stale values
- Effect loops
- duplicated state
- difficult-to-test components
- excessive memoization
- complicated dependency arrays
- hidden side effects
- tightly coupled components
- difficult-to-maintain Custom Hooks
This lesson brings together the most important practices for writing clean, predictable, maintainable React Hook code.
The Core Philosophy
A good React component should make it easy to answer:
What data does this component own?↓How does that data change?↓What should the UI display?↓What external systems must be synchronized?
A useful architecture is:
Props↓State↓Derived Values↓Event Handlers↓Effects↓UI
Not every component needs every layer.
The goal is to use the simplest design that correctly represents the behavior.
Best Practice 1: Keep State Minimal
One of the most important React principles is:
Store the minimum state necessary.
Avoid storing values that can be derived from existing state or props.
Bad:
const [firstName, setFirstName] =useState("");const [lastName, setLastName] =useState("");const [fullName, setFullName] =useState("");useEffect(() => {setFullName(`${firstName} ${lastName}`);}, [firstName, lastName]);
Better:
const [firstName, setFirstName] =useState("");const [lastName, setLastName] =useState("");const fullName =`${firstName} ${lastName}`;
The second version has fewer moving parts.
Derived State
A value is derived when it can be calculated from existing values.
Example:
const [price, setPrice] = useState(100);const [quantity, setQuantity] = useState(2);const total = price * quantity;
Do not create:
const [total, setTotal] = useState(200);
and synchronize it with an Effect.
Derived values should normally be calculated during rendering.
When Derived Data Is Expensive
If the calculation is genuinely expensive:
const filteredProducts = useMemo(() => {return expensiveFilter(products, query);}, [products, query]);
Use useMemo because there is an actual performance reason.
Do not turn every derived value into memoized state.
Best Practice 2: Keep State Close to Where It Is Used
State should generally live in the nearest practical component that needs to own it.
Suppose a modal is only used by:
ProductDetails
There may be no reason to store its state at the application root.
Prefer:
function ProductDetails() {const [isOpen, setIsOpen] =useState(false);// ...}
instead of unnecessarily lifting it to:
App
Local state can reduce unnecessary update propagation and makes ownership clearer.
When Should State Be Lifted?
Lift state when multiple components genuinely need the same state.
For example:
Parent├── SearchInput└── SearchResults
If both depend on the same search query:
function SearchPage() {const [query, setQuery] =useState("");return (<><SearchInputquery={query}setQuery={setQuery}/><SearchResultsquery={query}/></>);}
The parent becomes the appropriate state owner.
The principle is:
Only one component needs it↓Keep it localMultiple components need it↓Lift to their common owner
Best Practice 3: Prefer Functional State Updates When Appropriate
When the next state depends on the previous state:
setCount((current) => current + 1);
Prefer this over:
setCount(count + 1);
when the update is specifically based on previous state.
This is especially useful in:
- intervals
- event queues
- asynchronous callbacks
- multiple state updates
- reusable state logic
Example:
function incrementTwice() {setCount((current) => current + 1);setCount((current) => current + 1);}
Best Practice 4: Treat State as Immutable
Never mutate state directly.
Bad:
user.name = "Asha";
Bad:
items.push(newItem);
Good:
setUser((current) => ({...current,name: "Asha",}));
Good:
setItems((current) => [...current,newItem,]);
For nested objects:
setUser((current) => ({...current,profile: {...current.profile,city: "Bhubaneswar",},}));
Immutability makes state transitions easier to reason about and supports predictable React updates.
Best Practice 5: Keep Effects Focused
Avoid giant Effects:
useEffect(() => {fetchData();document.title = title;subscribe();localStorage.setItem("theme", theme);startTimer();}, [title,theme,]);
These operations represent different synchronization concerns.
Prefer separate Effects when the concerns have different dependencies or lifecycles:
useEffect(() => {fetchData();}, [query]);useEffect(() => {document.title = title;}, [title]);useEffect(() => {localStorage.setItem("theme",theme);}, [theme]);
Each Effect becomes easier to understand and debug.
One Effect, One Synchronization Concern
A useful guideline is:
One Effect↓One coherent synchronization concern
For example:
useEffect(() => {const connection =connectToRoom(roomId);return () => {connection.disconnect();};}, [roomId]);
This Effect has one clear responsibility:
Synchronize chat connection with roomId.
Best Practice 6: Do Not Use Effects for Derived Data
Avoid:
const [filtered, setFiltered] =useState([]);useEffect(() => {setFiltered(products.filter((product) =>product.category === category));}, [products, category]);
Prefer:
const filtered = products.filter((product) =>product.category === category);
If expensive:
const filtered = useMemo(() => {return products.filter((product) =>product.category === category);}, [products, category]);
This reduces unnecessary state and synchronization.
Best Practice 7: Use Event Handlers for User Actions
If something happens because the user clicked a button, the event handler is usually the clearest place to perform that action.
Good:
function handleSubmit() {saveForm();}
Avoid turning a user action into an indirect Effect when there is no synchronization requirement:
const [submitted, setSubmitted] =useState(false);useEffect(() => {if (submitted) {saveForm();}}, [submitted]);
The direct event-handler version is easier to understand.
Effects Are Not a Generic Event System
Avoid using state changes as signals merely to trigger Effects.
For example:
setShouldSave(true);
followed by:
useEffect(() => {if (shouldSave) {save();}}, [shouldSave]);
If the save is caused directly by a button click, prefer:
function handleSave() {save();}
Effects should synchronize state/props with external systems rather than act as a general-purpose event dispatcher.
Best Practice 8: Keep Custom Hooks Focused
A Custom Hook should represent a meaningful reusable behavior.
Good:
useOnlineStatus()useDebounce()useLocalStorage()usePrevious()useWindowSize()
Avoid giant Hooks such as:
useApplicationEverything()
that manage unrelated concerns.
Prefer composition:
useProductSearch()↓useDebounce()+useProducts()
Small Hooks can be combined into larger feature-level Hooks.
Best Practice 9: Give Custom Hooks a Clear API
A Hook should expose only what consumers need.
Good:
const {data,loading,error,refetch,} = useProducts();
The consumer should not need to know:
- how the request works
- how cancellation is implemented
- how loading is calculated
- how errors are normalized
The Hook acts as an abstraction boundary.
Best Practice 10: Do Not Use Custom Hooks for Pure Utilities
A normal helper function is better when no React Hook is required.
For example:
function formatCurrency(value) {return `₹${value.toFixed(2)}`;}
There is no reason to create:
function useFormatCurrency(value) {// unnecessary}
Use a Custom Hook only when the logic involves Hook-based or stateful behavior.
Best Practice 11: Follow the Rules of Hooks
Always:
Call Hooks↓at the top level↓of React components or Custom Hooks
Never:
if (condition) {useEffect(() => {});}
Never:
items.map(() => {useState();});
Never:
function handleClick() {useState();}
Correct conditional behavior should happen inside the Hook:
useEffect(() => {if (!enabled) {return;}// work}, [enabled]);
Best Practice 12: Keep Dependencies Accurate
Dependency arrays are part of the correctness model.
Example:
useEffect(() => {fetchUser(userId);}, [userId]);
If the Effect also uses:
organizationId
then the dependency relationship needs to account for it:
useEffect(() => {fetchUser(userId,organizationId);}, [userId, organizationId]);
Do not remove dependencies just to reduce Effect executions.
If the Effect runs too often, investigate why.
Best Practice 13: Prefer Restructuring Over Dependency Hacks
Suppose:
useEffect(() => {process(data);}, [data, process]);
and process changes on every render.
Do not immediately remove it:
useEffect(() => {process(data);}, [data]);
Instead, consider whether the function belongs inside the Effect:
useEffect(() => {function processData() {// ...}processData();}, [data]);
Or determine whether a stable callback is actually appropriate.
The goal is correct synchronization, not merely fewer Effect runs.
Best Practice 14: Use useCallback Intentionally
Good reason:
const handleDelete = useCallback((id) => {deleteItem(id);},[]);
when the stable function reference matters to a memoized child or another dependency relationship.
Bad reason:
const add = useCallback((a, b) => a + b,[]);
if the function is trivial and its identity does not matter.
useCallback adds complexity.
Use it when there is a real benefit.
Best Practice 15: Use useMemo Intentionally
Good:
const report = useMemo(() => {return generateLargeReport(transactions);}, [transactions]);
when report generation is genuinely expensive.
Usually unnecessary:
const name = useMemo(() => {return `${firstName} ${lastName}`;}, [firstName, lastName]);
Prefer:
const name =`${firstName} ${lastName}`;
Memoization should solve a performance problem, not become a coding habit.
Best Practice 16: Understand Referential Equality
JavaScript compares objects and functions by reference.
{} === {}
is:
false
Similarly:
(() => {}) === (() => {})
is:
false
This matters for:
- Effect dependencies
React.memo- Context values
useMemouseCallback
Understand reference identity before adding memoization.
Best Practice 17: Avoid Unnecessary Object Dependencies
Instead of:
const options = {roomId,theme,};useEffect(() => {connect(options);}, [options]);
consider:
useEffect(() => {connect({roomId,theme,});}, [roomId, theme]);
This makes the dependency relationship explicit.
Best Practice 18: Clean Up External Resources
If an Effect creates something external, determine how it should be cleaned up.
Examples:
addEventListener↓removeEventListener
setInterval↓clearInterval
subscribe↓unsubscribe
connect↓disconnect
createObserver↓disconnect
Example:
useEffect(() => {const timer = setInterval(refresh,5000);return () => {clearInterval(timer);};}, []);
Best Practice 19: Handle Async Effects Carefully
Asynchronous work can create:
- race conditions
- stale responses
- state updates after relevance changes
- cancellation problems
Use appropriate cancellation or cleanup strategies.
Example:
useEffect(() => {const controller =new AbortController();async function load() {try {const response = await fetch(url,{signal:controller.signal,});const data =await response.json();setData(data);} catch (error) {if (error.name !=="AbortError") {setError(error);}}}load();return () => {controller.abort();};}, [url]);
Best Practice 20: Avoid Race Conditions
Suppose:
Request A↓Request B↓B completes↓A completes
If both update the same state, older data can overwrite newer data.
Use cancellation or an appropriate stale-result strategy.
For APIs where request cancellation is supported:
const controller =new AbortController();fetch(url, {signal: controller.signal,});
Cleanup:
return () => {controller.abort();};
Best Practice 21: Use useRef for Non-Visual Mutable Values
A ref is useful when a value must persist between renders but changing it should not trigger a render.
Good examples:
const timerRef = useRef(null);
const socketRef = useRef(null);
const previousValueRef =useRef(value);
Do not use a ref simply to avoid a state update when the value actually affects the UI.
State vs Ref
Use state:
Value changes↓UI should change
Use ref:
Value changes↓UI does not need to change
Example:
const [isOpen, setIsOpen] =useState(false);
The UI depends on isOpen.
Compare:
const timerId = useRef(null);
The timer ID itself does not need to appear in the UI.
Best Practice 22: Keep Refs Out of the Rendering Model
Avoid relying on a ref as if it were state:
const countRef = useRef(0);countRef.current += 1;return <p>{countRef.current}</p>;
The UI will not automatically update when .current changes.
If the value belongs in the UI, use state.
Best Practice 23: Use useReducer for Complex Transitions
If state logic becomes difficult to understand:
setLoading(true);setError(null);setData(null);
and there are many combinations of transitions, consider:
dispatch({type: "requestStarted",});
with:
function reducer(state, action) {switch (action.type) {case "requestStarted":return {...state,loading: true,error: null,};// ...}}
useReducer is primarily about organizing complex state transitions, not automatically improving performance.
Best Practice 24: Design Meaningful Actions
Prefer:
{type: "formSubmitted"}
over:
{type: "setLoadingTrue"}
Meaningful actions describe events.
The reducer decides what those events mean.
This creates a cleaner state model.
Best Practice 25: Keep Reducers Pure
A reducer should:
- calculate next state
- avoid side effects
- avoid mutating state
- remain predictable
Good:
function reducer(state, action) {switch (action.type) {case "increment":return {...state,count: state.count + 1,};default:return state;}}
Avoid:
function reducer(state, action) {fetch("/api/data");return state;}
Best Practice 26: Use Context Carefully
Context is useful when many components need the same value.
But avoid putting every piece of application state into one giant Context.
Instead of:
AppContext├── auth├── cart├── products├── notifications├── filters├── settings└── everything else
consider feature-oriented boundaries.
For example:
AuthContextCartContextThemeContext
or other structures appropriate to the application.
Best Practice 27: Combine Context with useReducer for Feature State
A common pattern is:
const [state, dispatch] =useReducer(reducer,initialState);
Then provide:
<AppContext.Providervalue={{state,dispatch,}}>{children}</AppContext.Provider>
This can work well for feature-level state management.
Do not assume it is the right choice for every application.
Best Practice 28: Keep Component APIs Simple
A component should ideally expose meaningful props:
<ProductCardproduct={product}onAddToCart={handleAddToCart}/>
Avoid exposing internal state-management details unnecessarily.
Custom Hooks can help keep components clean:
const {products,loading,error,} = useProducts();
Best Practice 29: Keep Components Focused
If a component contains:
API requestsform managementWebSocketanalyticsmodal statetable filteringpaginationauthentication
it may be doing too much.
Consider separating concerns:
Component↓Custom Hooks├── useProducts├── useFilters├── usePagination└── useModal
The component focuses on composition and UI.
Best Practice 30: Use Component Boundaries for Repeated State
If every list item needs independent state, use a child component.
Good:
function TodoItem({ todo }) {const [editing, setEditing] =useState(false);return <div>{todo.text}</div>;}
Then:
todos.map((todo) => (<TodoItemkey={todo.id}todo={todo}/>))
This is much clearer than trying to create dynamic Hook calls inside the parent.
Best Practice 31: Use Stable Keys
Prefer:
key={item.id}
Avoid:
key={Math.random()}
Avoid index keys when items can be reordered or inserted:
key={index}
Keys are about component identity and correct reconciliation.
They can also affect whether state is preserved between list updates.
Best Practice 32: Do Not Optimize Before Measuring
A common mistake is adding:
useMemouseCallbackReact.memo
everywhere.
Instead:
Notice problem↓Measure↓Find bottleneck↓Optimize↓Measure again
Performance optimization should be evidence-driven.
Best Practice 33: Use React DevTools
React DevTools can help inspect:
- component hierarchy
- props
- state
- context
- component updates
- performance
For performance problems, use the Profiler to identify expensive components and interactions.
Best Practice 34: Keep Effects Correct Before Making Them Fast
A correct Effect that runs occasionally is better than an "optimized" Effect that uses stale data.
First ensure:
Correct dependencies+Correct cleanup+Correct synchronization
Then optimize if profiling shows a problem.
Best Practice 35: Make Effects Reversible
A useful principle is:
Setup should be paired with cleanup.
Example:
useEffect(() => {const connection =createConnection(roomId);connection.connect();return () => {connection.disconnect();};}, [roomId]);
This makes the Effect easier to reason about and robust under development lifecycle checks.
Best Practice 36: Design for Strict Mode
Do not write special code simply to prevent an Effect from being exercised more than once during development.
Instead, ensure setup and cleanup are correct.
Bad:
const hasRun =useRef(false);useEffect(() => {if (hasRun.current) {return;}hasRun.current = true;connect();}, []);
Better:
useEffect(() => {const connection =createConnection();connection.connect();return () => {connection.disconnect();};}, []);
The second version correctly models the lifecycle.
Best Practice 37: Avoid Hidden Side Effects
A function that looks like a pure calculation should not secretly modify external state.
Avoid:
function calculateTotal(items) {localStorage.setItem("items",JSON.stringify(items));return items.reduce(...);}
Prefer:
function calculateTotal(items) {return items.reduce(...);}
Then handle storage synchronization separately.
Best Practice 38: Keep Pure Logic Pure
Pure functions are easier to:
- test
- reuse
- debug
- reason about
Example:
function calculateTotal(items) {return items.reduce((total, item) =>total + item.price * item.quantity,0);}
Use it:
const total =calculateTotal(items);
No Hook is necessary.
Best Practice 39: Prefer Composition Over Giant Hooks
Instead of:
useDashboardEverything()
consider:
useAuth()useProducts()useFilters()usePagination()useNotifications()
Then compose them:
function Dashboard() {const auth = useAuth();const products = useProducts();const filters = useFilters();// ...}
This makes responsibilities clearer.
Best Practice 40: Keep Hook Return Values Predictable
If a Hook returns an object:
return {data,loading,error,};
keep the API stable.
If a Hook returns functions, make their identity stable only when there is a reason consumers benefit from it.
A predictable API makes Custom Hooks easier to adopt.
Best Practice 41: Avoid Premature Abstraction
Do not create a Custom Hook simply because two lines of code look similar.
Ask:
Is the logic stateful?Is it reusable?Does it represent a meaningful behavior?Would abstraction improve maintenance?
If not, keep the code simple.
Best Practice 42: Prefer Explicit Data Flow
React becomes easier to understand when data flow is obvious.
Prefer:
Props↓State↓Derived data↓Event handlers↓Effects↓UI
Avoid unnecessarily indirect flows:
Event↓State flag↓Effect↓Another state↓Another Effect↓Final action
Direct data flow is usually easier to debug.
Best Practice 43: Separate Server State from UI State
Not all application data is the same.
UI state might be:
isModalOpenselectedTabinputValue
Server state might be:
productsusersorders
Server-state problems often involve:
- caching
- refetching
- stale data
- retries
- synchronization
- request deduplication
For complex server-state requirements, consider an appropriate data-fetching/server-state solution rather than building increasingly complicated Effects manually.
Best Practice 44: Keep Loading and Error States Explicit
For asynchronous features, model the states clearly:
idleloadingsuccesserror
With useReducer:
const initialState = {status: "idle",data: null,error: null,};
This can be easier to reason about than several independent Boolean flags.
Best Practice 45: Avoid Boolean State Explosion
Avoid:
const [isLoading, setIsLoading] =useState(false);const [isError, setIsError] =useState(false);const [isSuccess, setIsSuccess] =useState(false);
This can allow contradictory combinations:
isLoading = trueisError = trueisSuccess = true
A status model can be clearer:
const [status, setStatus] =useState("idle");
Possible values:
idleloadingsuccesserror
For complex transitions, useReducer can make this even more explicit.
Best Practice 46: Avoid State That Represents Events
An event is not always state.
For example:
const [clicked, setClicked] =useState(false);
If clicked exists only to trigger an Effect:
useEffect(() => {if (clicked) {performAction();}}, [clicked]);
you may be modeling an event as state unnecessarily.
Use the event handler directly when appropriate.
Best Practice 47: Use Effects to Synchronize, Not Orchestrate Everything
Good:
useEffect(() => {document.title = title;}, [title]);
Less desirable:
useEffect(() => {setStep(2);}, [data]);useEffect(() => {setReady(true);}, [step]);useEffect(() => {submit();}, [ready]);
If a workflow is caused by one explicit event, model the workflow directly with event handlers and state transitions.
Best Practice 48: Test Reducers Independently
Because reducers are pure functions:
function reducer(state, action) {// ...}
they can be tested without rendering the component.
Example:
const state = {count: 0,};const nextState =reducer(state, {type: "increment",});
This makes complex state transitions easier to verify.
Best Practice 49: Test Custom Hooks by Behavior
For a Custom Hook such as:
useOnlineStatus()
test what consumers observe:
initial status↓online event↓status changes↓offline event↓status changes
Avoid tests that depend unnecessarily on internal implementation details.
Best Practice 50: Use a Consistent Hook Strategy
A mature React codebase should have conventions for:
- state ownership
- Effects
- Custom Hooks
- reducers
- Context
- memoization
- asynchronous operations
- testing
- folder organization
Consistency reduces cognitive overhead.
A Recommended Component Structure
A complex component can often follow a structure like:
function ProductDashboard({userId,}) {// 1. Stateconst [query, setQuery] =useState("");// 2. Custom Hooksconst {products,loading,error,} = useProducts(userId);// 3. Derived valuesconst filteredProducts =useMemo(() => {return products.filter((product) =>product.name.toLowerCase().includes(query.toLowerCase()));}, [products, query]);// 4. Event handlersconst handleSearch = (event) => {setQuery(event.target.value);};// 5. EffectsuseEffect(() => {document.title =`${filteredProducts.length} Products`;}, [filteredProducts.length]);// 6. Renderreturn (<div><inputvalue={query}onChange={handleSearch}/>{/* UI */}</div>);}
This is not a mandatory syntax template.
It is a useful organizational pattern.
A Practical Hook Decision Tree
When adding logic, ask:
Is it a simple value?
Use a normal variable:
const total = price * quantity;
Does the UI need to remember it between renders?
Use:
useState
Is the next value based on previous state?
Use a functional update:
setValue((current) => next);
Are state transitions complex?
Consider:
useReducer
Does the value need persistence without causing a render?
Consider:
useRef
Does the component need external synchronization?
Consider:
useEffect
Is a calculation genuinely expensive?
Consider:
useMemo
Does function identity matter?
Consider:
useCallback
Is stateful logic reusable?
Consider:
Custom Hook
Hook Anti-Patterns
The following patterns should trigger a review.
Anti-pattern 1
useEffect(() => {setDerivedValue(calculateValue(data));}, [data]);
Ask whether derivedValue can be calculated directly.
Anti-pattern 2
useCallback(() => 2 + 2,[]);
Ask whether memoization provides any value.
Anti-pattern 3
if (condition) {useEffect(...);}
This violates the Rules of Hooks.
Anti-pattern 4
useEffect(() => {setA(...);}, [a]);
Check for an Effect feedback loop.
Anti-pattern 5
const [isLoading, setIsLoading] =useState(false);const [isError, setIsError] =useState(false);const [isSuccess, setIsSuccess] =useState(false);
Consider a single status model.
Anti-pattern 6
const value = {state,dispatch,};
passed through a frequently rendering Context provider without considering whether context consumers actually need the resulting updates.
Hook Code Review Checklist
When reviewing a React component, ask:
State
- Is every state variable necessary?
- Can any state be derived?
- Is state owned by the right component?
- Are updates immutable?
- Should functional updates be used?
Effects
- Does each Effect synchronize with an external system?
- Are dependencies accurate?
- Is cleanup required?
- Can an Effect loop?
- Can async work race?
Refs
- Does the value actually need to trigger a render?
- Is a ref being used to hide state that belongs in the UI?
Memoization
- Is
useMemosolving an expensive calculation? - Is
useCallbacksolving a reference-identity problem? - Is
React.memoactually useful?
Reducers
- Are transitions complex enough to justify a reducer?
- Are actions meaningful?
- Is the reducer pure?
Custom Hooks
- Does the Hook represent reusable behavior?
- Is its API clear?
- Is it focused?
Components
- Are responsibilities clear?
- Are repeated stateful items separate components?
- Are keys stable?
Async Work
- Are errors handled?
- Is cancellation needed?
- Can stale data overwrite newer data?
Practical Example: Refactoring a Poor Component
Suppose you start with:
function ProductPage({ products }) {const [filtered, setFiltered] =useState([]);const [total, setTotal] =useState(0);const [clicked, setClicked] =useState(false);useEffect(() => {setFiltered(products.filter((product) =>product.available));}, [products]);useEffect(() => {setTotal(filtered.reduce((sum, product) =>sum + product.price,0));}, [filtered]);useEffect(() => {if (clicked) {saveAnalytics();}}, [clicked]);return (<div><buttononClick={() =>setClicked(true)}>Track</button><p>Total: ₹{total}</p></div>);}
There are several unnecessary synchronization steps.
A cleaner version:
function ProductPage({ products }) {const filtered = products.filter((product) => product.available);const total = filtered.reduce((sum, product) =>sum + product.price,0);function handleTrack() {saveAnalytics();}return (<div><button onClick={handleTrack}>Track</button><p>Total: ₹{total}</p></div>);}
If filtering or totaling becomes genuinely expensive, introduce useMemo based on measurement.
The improved version has:
- less state
- fewer Effects
- fewer renders caused by state synchronization
- clearer event handling
- simpler data flow
Advanced Principle: Optimize for Correctness First
The priority order should usually be:
Correctness↓Clarity↓Maintainability↓Performance optimization
Do not sacrifice correctness for a speculative optimization.
For example:
useMemo(...)
is not worth using if it causes confusing dependency behavior or obscures a simple calculation.
Likewise:
useCallback(...)
should not be used to hide an incorrectly designed data flow.
Advanced Principle: Optimize Boundaries
When performance problems exist, look at boundaries:
State boundaryComponent boundaryContext boundaryMemoization boundaryExternal-system boundary
Ask:
Where is state changing?Where does rendering start?Which children receive the update?Which external systems are synchronized?
This often reveals the actual optimization opportunity.
Advanced Principle: Prefer Local Reasoning
A good Hook design lets you understand one piece of behavior without reading the entire application.
For example:
const {data,loading,error,} = useProducts();
The component can reason about product data without understanding every request implementation detail.
This is one of the major benefits of well-designed Custom Hooks.
Hook Best Practices Cheat Sheet
State
const [value, setValue] =useState(initialValue);
Keep state minimal.
Functional update
setValue((current) => nextValue);
Use when next state depends on previous state.
Derived value
const total =price * quantity;
Do not store unnecessarily.
Effect
useEffect(() => {synchronize(value);return () => {cleanup();};}, [value]);
Use for external synchronization.
Ref
const ref = useRef(null);
Use for mutable values that do not need to trigger rendering.
Memoization
const result = useMemo(() => expensiveCalculation(data),[data]);
Use for meaningful expensive calculations.
Callback
const handleAction =useCallback(() => doSomething(value),[value]);
Use when function identity matters.
Reducer
const [state, dispatch] =useReducer(reducer,initialState);
Use for complex state transitions.
Custom Hook
const value =useSomething();
Use for reusable stateful behavior.
Rules
Top level+React component / Custom Hook
Practice Exercises
Exercise 1: Remove Derived State
Refactor:
const [total, setTotal] =useState(0);useEffect(() => {setTotal(items.reduce((sum, item) =>sum + item.price,0));}, [items]);
Remove unnecessary state and Effect.
Exercise 2: Refactor an Effect Chain
Given:
query↓Effect↓filteredProducts↓Effect↓total
Refactor the design so derived values are calculated directly.
Exercise 3: Improve State Ownership
Create a dashboard with:
- search
- modal
- product list
Place each piece of state in the most appropriate component.
Exercise 4: Functional Updates
Rewrite:
setCount(count + 1);setCount(count + 1);
so one event applies both increments.
Exercise 5: Effect Cleanup
Build a timer and ensure that:
- the interval starts correctly
- it stops correctly
- no timers remain after unmount
Exercise 6: Custom Hook API
Design:
useSearch()
with a clean return API.
Do not expose internal implementation details.
Exercise 7: Memoization Review
Review a component containing five useMemo and three useCallback calls.
For each one, decide whether it has a real performance or identity requirement.
Remove unnecessary memoization.
Exercise 8: Reducer Design
Create a reducer for:
idleloadingsuccesserror
Use meaningful actions:
requestStartedrequestSucceededrequestFailedreset
Mini Project: Production-Ready React Feature
Build a Product Search feature using the best practices from this lesson.
Requirements:
State
Manage only necessary UI state.
queryselectedProduct
Derived Data
Calculate:
filteredProductsresultCount
without redundant state.
Data Fetching
Use a focused Custom Hook:
useProducts()
Handle:
loadingsuccesserror
Search
Create:
useDebounce()
if debouncing is appropriate.
Product Items
Use a separate component:
<ProductItem />
with stable keys.
Performance
Use:
useMemouseCallbackReact.memo
only when there is a demonstrated reason.
Side Effects
Ensure:
- correct dependencies
- cleanup
- request cancellation when appropriate
- no unnecessary Effect chains
Architecture
Aim for:
ProductPage│├── useProducts()│├── useDebounce()│├── SearchInput│├── ProductList│ └── ProductItem│└── ProductDetails
Review the completed project using the Hook code review checklist.
Interview questions & answers
Interview preparation
12 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is the most important Hook best practice?
BeginnerModel answer
Use the simplest correct state and synchronization model.
Avoid unnecessary state, Effects, and memoization.
2Why should derived data usually not be stored in state?
BeginnerModel answer
Because it duplicates information that can be calculated from existing state or props, increasing synchronization complexity and potentially causing unnecessary renders.
3What is a good reason to create a Custom Hook?
BeginnerModel answer
To encapsulate reusable stateful behavior or a meaningful synchronization pattern.
4What is the difference between useRef and useState?
BeginnerModel answer
useStatestores data that participates in rendering.useRefstores a persistent mutable value without causing a re-render when.currentchanges.
Intermediate
5Should every derived value use useMemo?
IntermediateModel answer
No. UseuseMemowhen the calculation is expensive or memoization provides a meaningful benefit.6What should useEffect be used for?
IntermediateModel answer
Primarily for synchronizing a component with external systems such as APIs, subscriptions, timers, browser APIs, and third-party libraries.
7What should a reducer do?
IntermediateModel answer
A reducer should calculate the next state from the current state and action.
It should be pure and avoid side effects.
8Why should Effects have cleanup?
IntermediateModel answer
Cleanup releases external resources created by the Effect and keeps synchronization aligned with the component lifecycle.
Advanced
9Should every callback use useCallback?
AdvancedModel answer
No. UseuseCallbackwhen stable function identity matters.10Should user actions be handled with useEffect?
AdvancedModel answer
Usually not when the action is directly caused by an event.
Use an event handler when appropriate.
11Why is state placement important?
AdvancedModel answer
State placement affects ownership, component coupling, and the scope of updates.
Keep state local when possible and lift it only when multiple components need it.
12Why should you measure before optimizing?
AdvancedModel answer
Because memoization and other optimizations add complexity and may not improve the actual bottleneck.
Final Hook Best Practices Checklist
Before considering a Hook-based feature complete, ask:
State
- [ ] Is all state necessary?
- [ ] Can any value be derived?
- [ ] Is state owned by the right component?
- [ ] Are state updates immutable?
- [ ] Are functional updates used where appropriate?
Effects
- [ ] Does each Effect synchronize with an external system?
- [ ] Are dependencies accurate?
- [ ] Is cleanup implemented where required?
- [ ] Can the Effect create a feedback loop?
- [ ] Can asynchronous work race?
- [ ] Is cancellation needed?
Refs
- [ ] Does this value actually need to avoid rendering?
- [ ] Would state be more appropriate?
Memoization
- [ ] Is
useMemojustified? - [ ] Is
useCallbackjustified? - [ ] Is
React.memojustified? - [ ] Have you measured the problem?
Reducers
- [ ] Are transitions complex enough for a reducer?
- [ ] Are actions meaningful?
- [ ] Is the reducer pure?
- [ ] Is state immutable?
Custom Hooks
- [ ] Does the Hook represent a meaningful behavior?
- [ ] Is the API clear?
- [ ] Is the Hook focused?
- [ ] Does it follow the Rules of Hooks?
Components
- [ ] Are responsibilities clear?
- [ ] Are repeated stateful items separate components?
- [ ] Are keys stable?
- [ ] Is state unnecessarily lifted?
Architecture
- [ ] Is data flow easy to follow?
- [ ] Are side effects isolated?
- [ ] Is server state handled appropriately?
- [ ] Are abstractions justified?