React Best Practices
React Best Practices covers focused components, state ownership, pure rendering, effects, data flow, accessibility, security, testing, and evidence-based performance optimization.
Learning Objectives
By the end of this lesson, you will be able to:
- Understand what makes React code maintainable.
- Structure components effectively.
- Keep components small and focused.
- Follow proper state-management practices.
- Understand state ownership and colocation.
- Avoid unnecessary state.
- Keep rendering logic pure.
- Use props effectively.
- Avoid prop drilling where it becomes problematic.
- Handle lists and keys correctly.
- Write clean JSX.
- Handle events correctly.
- Use effects only when necessary.
- Avoid common
useEffectmistakes. - Design reusable components without over-engineering.
- Optimize rendering based on evidence.
- Handle loading, error, empty, and success states.
- Organize React applications by feature.
- Improve accessibility and maintainability.
- Apply senior-level React engineering practices.
Introduction
We have now learned the core React flow:
React↓Vite↓Project Structure↓SPA↓Developer Tools↓JSX↓Rendering UI
At this point, you can write React code.
But there's a much bigger question:
How do we write React code that remains clean, scalable, performant, testable, and maintainable as the application grows?
A beginner can write:
function App() {// everything here}
A senior engineer thinks about:
ArchitectureState ownershipComponent boundariesData flowPerformanceAccessibilityTestingMaintainabilitySecurity
That's where React Best Practices become important.
What Are React Best Practices?
React best practices are engineering principles that help us build applications that are:
ReadableMaintainablePredictableReusableTestableAccessiblePerformantScalable
There isn't one magical React architecture that works for every application.
Instead, good React engineering means making appropriate decisions based on:
Application sizeTeam sizeBusiness requirementsPerformance requirementsData complexityDeployment model
The Golden Rule
One of the most useful principles is:
Keep things simple until complexity is justified.
Don't create:
15 abstractions10 custom hooks8 context providers6 state-management libraries
for a simple page.
Instead:
Simple problem↓Simple solution
When complexity genuinely appears:
Growing problem↓Measure↓Identify complexity↓Introduce appropriate abstraction
This prevents over-engineering.
Component Responsibility
A component should ideally have a clear responsibility.
For example:
function CourseCard() {// display one course}
is easier to understand than:
function CoursePage() {// fetch courses// authenticate user// process payments// manage filters// render charts// handle notifications// manage modals// format invoices}
The second component has too many responsibilities.
Single Responsibility
A useful principle is:
A component should have a focused reason to change.
For example:
CoursePage│├── CourseFilters├── CourseList├── CourseCard└── Pagination
Each component has a clearer responsibility.
Avoid Giant Components
This is a common enterprise problem.
You may eventually see:
CourseDashboard.tsx--------------------1,500 lines
containing:
API callsStateBusiness logicFormsTablesModalsChartsValidationJSX
This becomes difficult to:
UnderstandTestDebugReviewModifyReuse
Instead, decompose the feature.
Good Component Decomposition
Instead of:
CourseDashboard
consider:
CourseDashboard├── CourseHeader├── CourseFilters├── CourseSummary├── CourseList│ └── CourseCard├── CoursePagination└── CourseDetailsModal
The goal isn't to create the maximum number of components.
The goal is:
Create meaningful boundaries.
Don't Over-Componentize
The opposite problem also exists.
You don't need:
function CourseTitle() {return <h2>React</h2>;}
if the component provides no meaningful reuse, behavior, or boundary.
Over-componentization can make code harder to follow:
Page↓Section↓Container↓Wrapper↓Content↓Title
without any real architectural benefit.
Use components where they improve:
ReuseReadabilityResponsibilityTestingState isolation
Keep Rendering Pure
A React component should generally behave like:
UI = f(props, state)
For the same inputs, it should produce the same UI.
Good:
function Greeting({name}: {name: string;}) {return <h1>Hello {name}</h1>;}
Avoid side effects directly in render:
function Greeting() {localStorage.setItem("visited", "true");return <h1>Hello</h1>;}
Rendering should describe UI, not perform unrelated external operations.
Don't Mutate State Directly
This is one of the most important React practices.
Avoid:
const [user, setUser] = useState({name: "Jagannath"});user.name = "Rahul";setUser(user);
Instead:
setUser({...user,name: "Rahul"});
You're creating a new object.
The mental model:
Old State↓Create New State↓State Update↓Render
Why Immutability Matters
Consider:
const [courses, setCourses] = useState<Course[]>([]);
Avoid:
courses.push(newCourse);setCourses(courses);
Prefer:
setCourses([...courses,newCourse]);
This makes state transitions explicit.
It also works better with React's rendering model and common optimization techniques.
Keep State Minimal
A common mistake is storing values that can be calculated.
Bad:
const [firstName, setFirstName] = useState("John");const [lastName, setLastName] = useState("Doe");const [fullName, setFullName] = useState("John Doe");
fullName is derived from:
firstName+lastName
You can calculate:
const fullName =`${firstName} ${lastName}`;
Now there is only one source of truth for the actual data.
Avoid Redundant State
Another example:
const [courses, setCourses] = useState([]);const [courseCount, setCourseCount] = useState(0);
If:
courseCount = courses.length
you probably don't need separate state.
Instead:
const courseCount = courses.length;
This avoids synchronization problems.
One Source of Truth
Prefer:
Single source of truth
instead of:
Same information↓Stored in multiple places
For example:
const [cartItems, setCartItems] =useState<CartItem[]>([]);
Then derive:
const totalItems = cartItems.length;
and:
const totalPrice =cartItems.reduce((sum, item) =>sum + item.price * item.quantity,0);
This is safer than maintaining:
cartItemstotalItemstotalPrice
as separate state values.
State Colocation
Keep state as close as possible to where it is used.
Suppose only CourseFilters needs:
searchsortcategory
Don't automatically put them in global state.
Prefer:
CoursePage│└── CourseFilters├── search├── sort└── category
This is called state colocation.
Why State Colocation Matters
If state is unnecessarily high:
App└── Global State└── search↓CoursePage↓CourseFilters
many components may become connected to state they don't actually need.
Keeping state closer to its consumers can reduce:
CouplingComplexityUnnecessary renders
Lift State When Necessary
State shouldn't always stay local.
Suppose two sibling components need the same state:
CourseFiltersCourseList
Then move the state to their closest common parent:
CoursePage├── CourseFilters└── CourseList
Now:
CoursePage│└── search state│├── CourseFilters└── CourseList
This is the classic:
Lift state up
pattern.
Don't Make Everything Global
Global state is useful for genuinely shared concerns.
Examples might include:
Authenticated userThemeLocaleApplication-wide preferencesShared workflow state
But don't put every piece of state into global state.
For example:
Modal open/closedInput textDropdown stateLocal tab selection
usually doesn't need global state.
Props Are the Default Data Flow
React's primary data flow is:
Parent↓Props↓Child
Example:
<CourseCardcourse={course}/>
This is explicit and easy to reason about.
Prefer props when the data is naturally local to the component hierarchy.
Avoid Unnecessary Prop Drilling
Suppose:
App↓Dashboard↓CoursePage↓CourseList↓CourseCard
and you pass:
user
through every component only so CourseCard can access it.
That's prop drilling.
Sometimes it is perfectly acceptable.
But when it becomes excessive, consider:
ContextFeature-level stateState managementBetter component boundaries
Don't introduce Context just because two components share one value.
Context Is Not Automatically Global State
Context is useful for making a value available to descendants without explicitly passing props through every intermediate component.
Examples:
ThemeLocaleAuthentication contextFeature configuration
But Context has trade-offs.
When a context value changes, consumers of that context may need to re-render.
Therefore:
Use Context intentionally, not as a universal state-management solution.
Avoid Excessive useEffect
One of the most important modern React practices is:
Don't use an Effect for something you can calculate during rendering.
Bad:
const [fullName, setFullName] =useState("");useEffect(() => {setFullName(`${firstName} ${lastName}`);}, [firstName, lastName]);
This introduces unnecessary state and an unnecessary effect.
Better:
const fullName =`${firstName} ${lastName}`;
When Should You Use Effects?
Effects are primarily for synchronizing React with something outside React.
Examples:
Browser APIsSubscriptionsTimersExternal widgetsNetwork synchronizationNon-React systems
Think:
React↕External System
rather than:
React state↓Another React state
The second case often doesn't need an Effect.
Avoid Effect Chains
Suppose:
State A↓Effect↓State B↓Effect↓State C
This can become difficult to reason about.
Prefer deriving values directly when possible:
State↓Derived Data↓UI
rather than:
State↓Effect↓State↓Effect↓State
Don't Fetch Data Blindly in Every Component
A large application can become messy if every component independently manages:
LoadingAPI callErrorRetryCachingRace conditions
For example:
CourseCard→ APICourseList→ APICoursePage→ APIDashboard→ API
You can end up with duplicated network requests.
Prefer a deliberate data-fetching architecture using:
Feature servicesCustom hooksServer-state librariesCachingRoute loadersFramework capabilities
depending on your application.
Separate UI From Business Logic When Appropriate
Consider:
function CoursePage() {// 200 lines of pricing logic// 100 lines of validation// 100 lines of API handling// JSX}
This becomes difficult to maintain.
Instead:
CoursePage│├── useCourses()├── courseService├── pricing utilities├── validation└── UI components
The exact architecture depends on complexity.
Feature-Based Organization
For large applications, organizing by feature is often easier to scale than putting everything into global type folders.
Instead of:
components/hooks/services/types/utils/
with hundreds of unrelated files, consider:
features/├── courses/│ ├── components/│ ├── hooks/│ ├── services/│ ├── types/│ └── utils/│├── authentication/│ ├── components/│ ├── hooks/│ ├── services/│ └── types/│└── payments/├── components/├── hooks/├── services/└── types/
This keeps related code together.
Example Feature Architecture
For a course platform:
src/└── features/└── courses/├── components/│ ├── CourseCard.tsx│ ├── CourseList.tsx│ ├── CourseFilters.tsx│ └── CourseDetails.tsx│├── hooks/│ └── useCourses.ts│├── services/│ └── courseService.ts│├── types/│ └── course.types.ts│└── utils/└── courseHelpers.ts
This scales much better for large teams.
Keep API Calls Out of Presentational Components
Instead of:
function CourseCard() {// fetch course// update state// render course}
prefer:
Data layer↓Container / hook↓CourseCard
For example:
function CoursePage() {const { courses } = useCourses();return (<CourseListcourses={courses}/>);}
Then:
function CourseCard({course}: Props) {return (<article>{course.title}</article>);}
The card becomes easier to reuse and test.
But Don't Over-Enforce "Container vs Presentational"
You don't need to split every component into:
ContainerPresentational
The architecture should follow actual complexity.
For small components:
CourseCard
can simply handle its own display.
For complex features:
CoursePage↓useCourses()↓CourseList↓CourseCard
can provide clearer separation.
Use judgment.
Use Semantic HTML
Don't use:
<div onClick={handleClick}>Save</div>
when you really need a button.
Prefer:
<button onClick={handleClick}>Save</button>
Semantic HTML provides:
AccessibilityKeyboard behaviorBrowser semanticsBetter maintainability
for free.
Accessibility Is a Best Practice
React doesn't automatically make an application accessible.
Think about:
Semantic HTMLKeyboard navigationLabelsFocusARIA where appropriateColor contrastScreen readersError messages
For example:
<label htmlFor="email"></label><inputid="email"type="email"/>
is better than:
<div>Email</div><input />
Forms Should Be Accessible
Good:
<label htmlFor="course-search">Search courses</label><inputid="course-search"type="search"value={search}onChange={e =>setSearch(e.target.value)}/>
This gives the input a meaningful accessible label.
Use Stable Keys
When rendering:
{courses.map(course => (<CourseCardkey={course.id}course={course}/>))}
prefer:
Stable unique identity
Avoid:
key={Math.random()}
because the key changes on every render.
Also avoid array indexes when items can be reordered or removed.
Don't Use Random Keys
Bad:
<CourseCardkey={Math.random()}course={course}/>
This can cause React to treat the component as a new instance repeatedly.
That can lead to:
Lost stateUnnecessary DOM workUnexpected behavior
Use stable identifiers.
Keep Keys Unique Among Siblings
Good:
{courses.map(course => (<CourseCardkey={course.id}course={course}/>))}
Keys only need to be unique within the relevant sibling set.
You don't need globally unique keys across the entire application.
Avoid Premature Optimization
A common mistake:
React component↓Add memo↓Add useMemo↓Add useCallback↓Add context↓Add state library
without measuring anything.
Instead:
Build↓Measure↓Identify bottleneck↓Optimize↓Measure again
This is a much stronger engineering approach.
React.memo
React.memo can prevent unnecessary rendering of a component when its props haven't changed according to its comparison.
Example:
const CourseCard = memo(function CourseCard({course}: Props) {return (<article>{course.title}</article>);});
But don't automatically wrap every component in memo.
Use it where it provides measurable value.
useMemo
useMemo can cache the result of an expensive calculation between renders when dependencies remain unchanged.
Example:
const filteredCourses = useMemo(() => {return courses.filter(course =>course.title.toLowerCase().includes(search.toLowerCase()));}, [courses, search]);
But if the calculation is trivial:
const fullName =`${firstName} ${lastName}`;
you don't need useMemo.
useCallback
useCallback can preserve function identity between renders.
For example:
const handleSelect = useCallback((id: string) => {selectCourse(id);},[selectCourse]);
It can be useful when function identity affects memoized children or hook dependencies.
But again:
Don't use useCallback everywhere by default.
Avoid Premature Memoization
Memoization adds complexity.
If:
Calculation = 0.01 ms
and you spend time optimizing it without evidence, you've potentially made the code harder to understand for no meaningful benefit.
Optimization should be driven by:
MeasurementsUser experienceApplication scaleActual bottlenecks
Keep State Local for Performance
Suppose:
App├── Header├── Sidebar├── Dashboard└── CourseFilter
If CourseFilter owns:
searchText
only its relevant subtree needs to respond to it.
If App owns it unnecessarily, changing the value may involve a much larger rendering path.
State colocation can therefore improve both architecture and performance.
Avoid Unnecessary Context Updates
Suppose:
<AuthContext.Providervalue={{user,logout}}>
If the provider creates a new object every render, consumers may receive a new context value.
Depending on the architecture, this can cause additional consumer updates.
This is one reason context design and value identity matter.
But don't blindly memoize context values without understanding the actual performance issue.
Handle All UI States
A production component shouldn't assume:
Data always existsAPI always succeedsUser always behaves correctly
Think:
Loading↓Error↓Empty↓Success
Example:
if (loading) {return <Loading />;}if (error) {return <ErrorMessage />;}if (courses.length === 0) {return <EmptyState />;}return <CourseList courses={courses} />;
This makes applications much more robust.
Don't Ignore Error Handling
Bad:
try {await saveCourse(course);} catch {// nothing}
The user may have no idea that the operation failed.
Instead:
API Failure↓Application Error Handling↓User-Friendly Message
For example:
"Unable to save the course.Please try again."
Avoid exposing internal implementation details to users.
Handle Loading States Carefully
Don't always show:
Loading...
for every tiny update.
For example, if a table is already displayed and a filter changes, completely replacing it with a blank loading screen may create a poor user experience.
Depending on the UX, consider:
Existing data+Loading indicator
instead.
The right approach depends on the interaction.
Avoid Unnecessary API Requests
Suppose the user types:
rrereareacreact
and each keystroke triggers an API request.
You may create:
5 requests
instead of one intentional request.
For search fields, consider:
DebouncingCachingServer-side searchRequest cancellation
where appropriate.
Race Conditions
Suppose:
Request A → "react"Request B → "java"
Request B finishes first.
Then Request A finishes later.
If you aren't careful:
Older response↓Overwrites newer data
This creates a stale UI.
Good data-fetching architecture should account for:
CancellationRequest identityCachingRace conditions
Don't Put Secrets in React
This is critical.
Never put:
Database passwordPrivate API keySecret tokenCloud credentials
directly into frontend code.
Anything bundled into a browser application can potentially be inspected.
Remember:
Browser↓User can inspect
Frontend configuration is not secret storage.
Environment Variables Are Not Automatically Secret
Developers sometimes think:
VITE_SECRET_KEY
means:
It's secret.
Not if it is included in the client bundle.
For example:
Frontend environment variable↓Build↓JavaScript bundle↓Browser
Users can potentially inspect it.
Use backend services for actual secrets.
Validate on the Backend
Frontend validation is useful for UX:
Email formatRequired fieldPassword length
But never rely solely on frontend validation for security.
Correct architecture:
Frontend validation↓Better user experienceBackend validation↓Actual enforcement
Keep Business Rules Centralized
Suppose a course discount is:
20%
Don't duplicate:
CoursePageCourseCardCheckoutInvoice
with slightly different logic.
Prefer a shared business rule/service when appropriate:
Pricing logic↓Single source of truth
This prevents inconsistent behavior.
Reusable Components Should Be Generic Enough
Suppose you create:
<CourseCard />
with 25 props:
titlesubtitlepricediscountimagebadgeratingauthorcategory...
You may have created a component that tries to solve everything.
Prefer meaningful boundaries.
Sometimes:
CourseCard
should simply receive:
<CourseCard course={course} />
and the component can decide how to display the course.
Don't Build a "God Component"
A God component knows:
Everything
Avoid:
App.tsx├── Authentication├── Routing├── API calls├── Business rules├── Analytics├── Payments├── Courses├── Reports└── 1000 lines JSX
Instead:
App├── Providers├── Router└── Application Features
Then features own their complexity.
Keep Business Logic Testable
Instead of putting everything inside JSX:
const finalPrice =price -price * discount / 100;
for simple calculations it's fine.
For complex pricing:
Discount rulesTaxCouponsMembershipCurrencyRegional pricing
extract the logic:
calculateFinalPrice(...)
Now you can unit-test it independently.
Custom Hooks
Custom hooks are useful when behavior needs to be reused.
For example:
function useCourses() {// fetch courses// manage loading// manage error// return data}
Then:
function CoursePage() {const {courses,loading,error} = useCourses();// UI}
This separates reusable behavior from UI structure.
Don't Create Hooks Just to Rename Code
Bad abstraction:
function useTitle() {return "React";}
if it's used once and provides no meaningful behavior.
Good custom hook:
useCourses()useAuth()useDebouncedValue()usePagination()
when the behavior is reusable or meaningfully encapsulated.
Keep Custom Hooks Focused
Avoid:
useEverything()
that manages:
AuthenticationCoursesPaymentsNotificationsThemeAnalytics
Instead:
useAuth()useCourses()usePayments()useNotifications()
Focused hooks are easier to understand and test.
Prefer Composition Over Complex Inheritance
React is designed around composition.
For example:
<Card><CourseDetails /></Card>
rather than trying to build a deep inheritance hierarchy.
Composition gives you flexibility:
Card├── CourseDetails├── UserDetails└── PaymentDetails
depending on the context.
Use Clear Naming
Prefer:
isLoadingisAuthenticatedhandleSubmithandleDeleteselectedCoursefilteredCourses
over:
flagdata1tempthinghandleStuff
Good naming reduces the amount of code you need to understand.
Boolean Naming
Boolean values should read naturally.
Good:
const isLoading = true;const isAdmin = false;const hasCourses = true;const canEdit = true;
Avoid:
const loading = true;const admin = false;const edit = true;
The latter isn't always wrong, but the is/has/can/should convention makes intent clearer.
Event Handler Naming
Prefer:
handleSubmithandleClickhandleDeletehandleSearchhandleCourseSelect
For props:
onSubmitonClickonDeleteonCourseSelect
This creates a useful convention:
Parent↓onCourseSelectChild↓handleCourseSelect
Keep JSX Readable
Avoid:
<div className="course-card"><h2>{course.title}</h2><p>{course.description}</p><button onClick={() => selectCourse(course.id)}>View</button></div>
Prefer:
<div className="course-card"><h2>{course.title}</h2><p>{course.description}</p><buttononClick={() =>selectCourse(course.id)}>View</button></div>
Readable code is easier to review and debug.
Avoid Deeply Nested JSX
If your JSX becomes:
<div><section><div><div><article><div>...
consider whether component boundaries or semantic elements can make the structure clearer.
Use Semantic Elements
Prefer:
<header /><nav /><main /><section /><article /><footer /><button /><form />
when appropriate.
This improves:
AccessibilitySEOReadabilityMaintainability
Keep Accessibility in the Design
Don't wait until the end of the project.
Think about:
KeyboardFocusLabelsSemanticsScreen readersError messagingLoading announcements
from the beginning.
Accessibility is much easier when it is built into component design.
Test Behavior, Not Implementation
When testing a React component, prefer testing:
User clicks button↓Expected UI appears
rather than testing internal implementation details such as:
Internal function X was called exactly twice
Behavior-oriented tests tend to survive refactoring better.
Testing Pyramid
A mature application may use:
E2E/ \Integration/ \Unit Tests
Examples:
Unit
calculatePrice()
Integration
CoursePage + API mocking
E2E
User logs in↓Searches course↓Opens course↓Enrolls
The right balance depends on the application.
Don't Ignore Error and Loading Tests
If a component has:
LoadingErrorEmptySuccess
test meaningful states.
For example:
API successAPI failureNo coursesSlow response
Production bugs often happen in states developers didn't test.
Performance Best Practice
Don't say:
React is slow.
Instead ask:
What is slow?
Possible causes:
React renderJavaScript computationAPINetworkLarge listDOM sizeBrowser layoutImage sizeThird-party library
Use the correct diagnostic tool.
Performance Investigation Model
User reports slowness↓Reproduce↓Measure↓Classify bottleneck│┌──────┼────────┐▼ ▼ ▼React API Browser│ │ │Profiler Network Performance│▼Root Cause↓Optimize↓Measure Again
This is a senior engineering approach.
Don't Optimize Without Evidence
Avoid statements like:
Let's add useMemo.
Instead:
The profiler shows this expensive calculation runs on every render even though its dependencies rarely change, so useMemo may be appropriate.
That's the difference between:
Keyword-driven development
and:
Evidence-driven engineering
Use Code Splitting for Large Applications
A large application shouldn't necessarily load everything immediately.
Conceptually:
Initial Bundle↓Dashboard↓Load only what's needed
Other features:
ReportsAdminAnalyticsSettings
can potentially be loaded later.
We'll study:
lazy()SuspenseRoute-level code splitting
later.
Optimize Images and Assets
Frontend performance isn't only React.
Large images can dominate loading time.
Prefer:
Optimized image formatsResponsive imagesLazy loadingProper dimensionsCDNCaching
React is only one part of the application's performance.
Avoid Unnecessary DOM
A simple:
<><h1>React</h1><p>Course</p></>
may be better than:
<div><div><div><h1>React</h1><p>Course</p></div></div></div>
Use the simplest structure that communicates the UI correctly.
Security Best Practices
React applications should also follow secure development practices.
Important principles include:
Don't trust client inputDon't expose secretsValidate on backendEscape/render untrusted content safelyUse secure authenticationProtect APIsAvoid dangerous HTML injection
Be especially careful with:
dangerouslySetInnerHTML
It should only be used with properly trusted/sanitized content.
Don't Put Authorization Only in the UI
This:
{isAdmin && <AdminButton />}
is useful for UX.
But it isn't security.
Correct architecture:
Frontend↓Hide unavailable actionsBackend↓Enforce authorization
A user can modify frontend behavior in their browser.
The backend must enforce permissions.
Keep Dependencies Under Control
Don't install a package for every small problem.
Before adding:
libraryframeworkstate managerutility
ask:
Do we really need it?Can React/JavaScript already solve it?Will this increase maintenance?Does the team know it?
Dependencies have long-term costs.
Keep React and Dependencies Updated
Modern React applications evolve.
Keep an eye on:
ReactTypeScriptViteTesting librariesBuild toolsSecurity patches
But don't blindly upgrade production dependencies without testing.
Use:
Release notesAutomated testsStagingIncremental upgrades
for major changes.
Code Review Checklist
When reviewing React code, ask:
Components
Is responsibility clear?Is it too large?Is it over-engineered?
State
Is state necessary?Can it be derived?Is it colocated?Is it mutated?
Effects
Is this Effect actually necessary?Could this be calculated during render?
Performance
Is there evidence of a bottleneck?Are large lists handled correctly?
Accessibility
Are semantic elements used?Are forms labeled?Is keyboard interaction supported?
Security
Are secrets exposed?Is backend validation present?
Maintainability
Are names clear?Is logic testable?Are abstractions justified?
A Practical React Best-Practices Checklist
Before committing React code:
□ Components have clear responsibilities□ State is minimal□ State is colocated where practical□ Derived values aren't unnecessarily stored□ State isn't mutated directly□ Effects are genuinely necessary□ JSX is readable□ List keys are stable□ Event handlers aren't accidentally executed during render□ Components use semantic HTML□ Forms have accessible labels□ Loading/error/empty states are handled□ Business logic is testable□ No secrets are exposed□ Backend authorization is enforced□ Performance optimizations are evidence-based□ Large lists are handled appropriately□ Naming is clear□ Tests focus on user-visible behavior□ Dependencies are justified
Real-World Example
Suppose we are building:
TechLearningPro
with:
CoursesStudentsPaymentsAuthenticationAdminReports
A scalable architecture might look like:
src/│├── app/│ ├── router/│ ├── providers/│ └── App.tsx│├── features/│ ├── auth/│ │ ├── components/│ │ ├── hooks/│ │ ├── services/│ │ └── types/│ ││ ├── courses/│ │ ├── components/│ │ ├── hooks/│ │ ├── services/│ │ ├── types/│ │ └── utils/│ ││ ├── payments/│ └── students/│├── shared/│ ├── components/│ ├── hooks/│ ├── utils/│ └── types/│└── main.tsx
The important thing isn't copying this exact structure.
The important principle is:
Organize code around responsibilities and features as complexity grows.
Senior-Level React Engineering Mindset
A junior developer may ask:
How can I make this component work?
A senior developer asks:
What is the simplest maintainable design for this behavior?
An experienced engineer also asks:
Who owns this state?Who owns this business rule?What happens when the API fails?What happens with 10,000 records?How will this be tested?Will this be accessible?What happens during concurrent user actions?What happens when requirements change?
That mindset is more valuable than memorizing React APIs.
Best-Practice Decision Framework
When you're unsure how to implement something:
Requirement│▼Simplest Design│▼Is complexity justified?│ │No Yes│ │▼ ▼Keep it Measure/simple Analyze│▼Add abstraction│▼Test it│▼Monitor it
This prevents unnecessary architecture.
Interview questions & answers
Interview preparation
13 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What are React best practices?
BeginnerModel answer
They are engineering practices that help make React applications maintainable, predictable, reusable, accessible, testable, and performant.
Examples include keeping components focused, avoiding unnecessary state, using stable keys, keeping rendering pure, and handling UI states properly.
2Why shouldn't we mutate React state directly?
BeginnerModel answer
Direct mutation can make state changes difficult for React and developers to reason about and can lead to stale or unexpected UI behavior.
Creating new state values makes the state transition explicit.
3What is state colocation?
BeginnerModel answer
State colocation means keeping state as close as practical to the components that actually use it instead of unnecessarily putting it into global state.
Intermediate
4When should you use Context?
IntermediateModel answer
I use Context when a value needs to be available to many components across a subtree and passing it through props would create unnecessary coupling or prop drilling.
Typical examples are theme, locale, authentication information, or feature-level configuration.
5When should you use useEffect?
IntermediateModel answer
I use Effects primarily to synchronize React with external systems.
I avoid using them for values that can be derived during rendering or for ordinary state-to-state transformations.
6When should you use useMemo?
IntermediateModel answer
I useuseMemowhen there is an expensive calculation and profiling shows that avoiding repeated calculation provides a meaningful benefit. I don't use it automatically for every derived value.7How do you prevent unnecessary re-renders?
IntermediateModel answer
First I identify the actual rendering problem using profiling. Then I look at state placement, prop identity, context updates, component boundaries, expensive calculations, and large lists. Depending on the cause, I may use state colocation,React.memo, memoization, virtualization, or architectural changes.
Advanced
8What is more important: reusable components or simple components?
AdvancedModel answer
Neither should be pursued blindly.
I prefer simple, focused components first and introduce reuse when there is a genuine repeated behavior or meaningful abstraction.
Over-abstraction can be just as harmful as duplication.
9How would you structure a large React application?
AdvancedModel answer
For a large application, I'd generally organize around business features, with each feature owning its components, hooks, services, types, and utilities where appropriate.
I'd keep shared infrastructure separate and establish clear boundaries between UI, state/data fetching, and business logic.
10How do you approach React performance?
AdvancedModel answer
I use a measure-first approach.
I'd reproduce the issue, profile the application, identify whether the bottleneck is rendering, computation, network, DOM size, or browser work, and then apply the smallest effective optimization.
I'd validate the result with another measurement.
11How do you prevent a React application from becoming difficult to maintain?
AdvancedModel answer
I focus on clear component boundaries, feature-based organization, minimal state, predictable data flow, limited global state, focused custom hooks, testable business logic, consistent conventions, and strong code review practices.
I also avoid introducing abstractions until the complexity justifies them.
12How do you decide whether state should be local, lifted, contextual, or global?
AdvancedModel answer
I start with ownership.
If only one component needs it, I keep it local.
If siblings need it, I lift it to their closest common owner.
If many descendants need the same cross-cutting value, Context may be appropriate.
For complex shared application state or server state, I'd evaluate an appropriate state or data-fetching solution based on the actual requirements.
13How do you balance performance and maintainability?
AdvancedModel answer
I avoid optimizing prematurely.
I start with a clean architecture and measure real performance problems.
If a bottleneck appears, I choose the least complex optimization that addresses it and verify the improvement.
This keeps performance work evidence-based without making the entire codebase harder to understand.
Real Interview Scenario
Interviewer:
You have a React application with 100,000 records. How would you design it?
Don't immediately answer:
I'll use useMemo.
A stronger answer:
100,000 records↓Do we really need all records?↓Server-side pagination/filtering↓Efficient API↓Client caching↓Virtualized rendering↓Render only visible rows
Then investigate:
NetworkMemoryRenderingDOMBrowser performance
This demonstrates architectural thinking.
Another Interview Scenario
Interviewer:
A component has 15 useEffect hooks. Is that bad?
Don't answer:
Yes.
Instead:
Not necessarily. The number alone isn't the problem. I'd review what each Effect is synchronizing with. If many Effects are being used to derive state from other state, I'd consider simplifying them. If they're legitimately synchronizing with external systems, they may be justified.
That is a much stronger senior-level answer.
Another Scenario
Interviewer:
Should every component use React.memo?
Answer:
No. React.memo is an optimization, not a default component requirement. I'd use profiling to determine whether a component is rendering unnecessarily and whether memoization can meaningfully reduce the work.
Another Scenario
Interviewer:
Should everything go into Redux or another global state manager?
Answer:
No. I prefer state ownership based on scope. Local state should remain local, shared state can be lifted or provided through Context when appropriate, and complex application or server state should use an appropriate solution based on the problem.
The 10 Golden React Rules
If you remember only ten rules from this lesson, remember these:
Rule 1
Keep components focused.
Rule 2
Keep state minimal.
Rule 3
Don't store what you can derive.
Rule 4
Don't mutate state directly.
Rule 5
Keep state close to where it's used.
Rule 6
Use Effects for external synchronization, not ordinary calculations.
Rule 7
Use stable keys for lists.
Rule 8
Optimize based on measurement, not assumptions.
Rule 9
Build accessibility and security into the architecture.
Rule 10
Prefer simple solutions until complexity is justified.
Final Mental Model
Think about React architecture like this:
APPLICATION│┌──────────┴──────────┐▼ ▼FEATURES SHARED│ │▼ ▼Components ComponentsHooks HooksServices UtilitiesTypes│▼STATE│┌───────┴────────┐▼ ▼Local Shared│ │└───────┬────────┘▼JSX│▼RENDER│▼RECONCILIATION│▼COMMIT│▼DOM
And around everything:
AccessibilitySecurityTestingPerformanceObservabilityMaintainability
Final Takeaway
The biggest React best practice isn't a specific API.
It is an engineering mindset:
Build the simplest architecture that correctly solves today's problem, keep state and responsibilities close to where they belong, make rendering predictable, measure before optimizing, and introduce complexity only when the application genuinely needs it.
A professional React developer doesn't just know:
useStateuseEffectuseMemouseCallback
They know when not to use them.
That distinction is what separates writing React code from engineering React applications.