React / JSX
Write clean, readable, and maintainable React code by following consistent JSX and component patterns.
These rules help ensure clarity in UI logic, improve component reusability, reduce unnecessary complexity and re-renders, and promote accessibility, testability, and long-term scalability in React applications.
One component per file
Each file should contain only one top-level component. This makes components easier to locate, reason about, test, and reuse. It also improves auto-imports and keeps file responsibilities clear.
Tip: If small helper components are tightly coupled and not reused, they may be defined inline but not exported.
1// Card.tsx
2export function Card() { ... }
3export function CardHeader() { ... }
4export function CardBody() { ... }
1// Card.tsx
2export function Card() { ... }
3
4// CardHeader.tsx
5export function CardHeader() { ... }
Avoid prop drilling
Passing props through many intermediate components (prop drilling) creates brittle code and unnecessary coupling. Prefer React Context, composition, or state management solutions for deeply shared state.
1// App → Layout → Page → Sidebar → Profile → UserAvatar
2<Layout user={user} />
3
4function Sidebar({ user }) {
5 return <Profile user={user} />;
6}
1<UserProvider value={user}>
2 <Layout />
3</UserProvider>
4
5// Inside deeply nested component
6const user = useUser();
Keep components small and focused
A component should do one thing well. Split large, complex components into smaller, reusable ones to improve clarity and testability.
1function Dashboard() {
2 return (
3 <div>
4 <Nav />
5 <Sidebar />
6 <Content />
7 <Footer />
8 <Modal />
9 <Toast />
10 </div>
11 );
12}
1function Dashboard() {
2 return (
3 <>
4 <Layout />
5 <OverlayElements />
6 </>
7 );
8}
Destructure props at the top level
Destructuring props improves readability and makes dependencies explicit. It also reduces repetitive access like props.name
.
1function Profile(props) {
2 return <h1>{props.name}</h1>;
3}
1function Profile({ name }) {
2 return <h1>{name}</h1>;
3}
Use explicit return in multi-line JSX
When returning multi-line JSX, always wrap the content in parentheses to improve readability and avoid automatic semicolon insertion issues.
1return
2 <div>
3 Hello
4 </div>;
1return (
2 <div>
3 Hello
4 </div>
5);
Use fragments instead of unnecessary divs
Avoid creating extra DOM nodes. Use React Fragments (<>...</>
) to group elements without affecting the DOM structure.
1return (
2 <div>
3 <Title />
4 <Text />
5 </div>
6);
1return (
2 <>
3 <Title />
4 <Text />
5 </>
6);
Or full Fragments
syntax:
1cards.map(curr => (
2 <Fragment key={curr.id}>
3 <Title>{curr.title}</Title>
4 <Description text={curr.text} />
5 </Fragment>
6));
Always use 'key' in list rendering
When rendering lists, always specify a key
prop with a unique and stable value to help React track and optimize DOM updates.
items.map((item) => <li key={item.id}>{item.label}</li>);
Use conditional rendering properly
Apply conditional rendering clearly and consistently. Avoid deeply nested ternaries and chained logic. Prefer short-circuiting (&&
), early returns, or abstraction via components when rendering based on state.
return user ? (user.isAdmin ? <AdminPanel /> : <UserPanel />) : <LoginPrompt />
Early return:
1if (!user) return <LoginPrompt />;
2if (user.isAdmin) return <AdminPanel />;
3return <UserPanel />;
Short-circuit:
{isModalOpen && <Modal />}
Abstraction:
1function AuthGate({ user, children }) {
2 if (!user) return <LoginPrompt />;
3 return children;
4}
5
6// Usage
7<AuthGate user={user}>
8 <Dashboard />
9</AuthGate>
Keep logic out of JSX
Move as much logic as possible out of the return block to avoid cluttering your JSX. JSX should ideally contain just simple interpolation and conditional rendering. This improves readability and separation of concerns.
1return (
2 <>
3 ...
4 {items.filter(i => i.active && i.count > 0).map(i =>
5 i.type === "premium" ? <PremiumItem key={i.id} {...i} /> : <Item key={i.id} {...i} />
6 )}
7 ...
8 </>
9);
1
2const renderItems = useCallback(
3 () => {
4 return (
5 items
6 .filter(i => i.active && i.count > 0)
7 .map(i => i.type === "premium"
8 ? <PremiumItem key={i.id} {...i} />
9 : <Item key={i.id} {...i} />
10 );
11 );
12 }, [items]
13}
14
15return (
16 <>
17 ...
18 {renderItems(items)};
19 ...
20 </>
21);
Use 'useMemo' and 'useCallback' wisely
Use useMemo
and useCallback
to memoize expensive calculations or stable functions — but only when needed. Overusing them can hurt performance.
const memoizedValue = useMemo(() => 2 + 2, []);
const filteredList = useMemo(() => list.filter(fn), [list]);
Keep side effects inside 'useEffect'
Any code that interacts with the outside world (e.g., API calls, timers, DOM) should be placed inside useEffect
, not directly in the render flow.
1useEffect(() => {
2 fetch('/api/data');
3}, []);
Avoid Unnecessary State
Only store in state what cannot be derived from props or other state. Derived values should be computed in-place or with useMemo
const [fullName, setFullName] = useState(`${first} ${last}`);
const fullName = `${first} ${last}`;
Define event handlers outside JSX
Declare event handlers as named functions outside JSX to improve readability, enable reuse, and avoid unnecessary re-renders due to anonymous function re-creation.
<button onClick={() => doSomething(id)}>Click</button>
1function handleClick() {
2 doSomething(id);
3}
4
5return <button onClick={handleClick}>Click</button>;
Always follow hook rules
Hooks must be called unconditionally and in the same order on every render. Never place hooks inside conditions, loops, or nested functions.
1if (isAdmin) {
2 useEffect(() => {
3 // Invalid!
4 }, []);
5}
1useEffect(() => {
2 if (isAdmin) {
3 // Valid
4 }
5}, [isAdmin]);
Use controlled components for forms
Prefer controlled inputs (tied to component state) over uncontrolled ones. This makes validation, resets, and dynamic behaviors easier to manage and debug.
<input defaultValue="John" />
1const [name, setName] = useState("John");
2
3function handleNameChange(e: React.ChangeEvent<HTMLInputElement>) {
4 setName(e.target.value);
5}
6
7return <input value={name} onChange={handleNameChange} />;
Memoize expensive components and values
Use React.memo, useMemo, and useCallback to avoid unnecessary recalculations or re-renders — especially for heavy computations or frequently re-rendering child components.
1const memoizedItems = useMemo(() => computeItems(items), [items]);
2<ExpensiveList items={memoizedItems} />
Or for components:
const ExpensiveList = React.memo(({ items }) => { ... });
Split components by responsibility
If a component handles multiple concerns (data fetching, rendering UI, handling interactions), split responsibilities into separate components or hooks. This promotes clarity and reusability.
1function Dashboard() {
2 const [data, setData] = useState(null);
3 useEffect(() => { fetchData().then(setData); }, []);
4
5 return (
6 <div>
7 {data ? <List data={data} /> : <Spinner />}
8 </div>
9 );
10}
1function Dashboard() {
2 const data = useDashboardData();
3 return <DashboardContent data={data} />;
4}
Use lazy loading for heavy components
Use React.lazy
with Suspense
to defer loading large components until needed. This improves initial load performance, especially for route-based or modal content.
1const BigChart = React.lazy(() => import('./BigChart'));
2
3<Suspense fallback={<Spinner />}>
4 <BigChart />
5</Suspense>
Use error boundaries for critical UI
Wrap risky parts of your UI (routes, dynamic widgets, third-party embeds) in error boundaries to prevent a full app crash and show fallback UI.
1//Note: Error boundaries must be class components or libraries like react-error-boundary
2<ErrorBoundary fallback={<FallbackUI />}>
3 <RiskyWidget />
4</ErrorBoundary>
Extract logic into reusable hooks
Encapsulate shared logic (e.g., data fetching, timers, form state) in custom hooks instead of repeating code in multiple components.
1function ComponentA() {
2 const [value, setValue] = useState(0);
3 useEffect(() => {
4 const timer = setInterval(() => setValue(v => v + 1), 1000);
5 return () => clearInterval(timer);
6 }, []);
7}
1function useTick(interval = 1000) {
2 const [tick, setTick] = useState(0);
3 useEffect(() => {
4 const t = setInterval(() => setTick(v => v + 1), interval);
5 return () => clearInterval(t);
6 }, [interval]);
7 return tick;
8}
Make components testable
Write components in a way that makes them easy to test: avoid tightly coupled logic, use dependency injection when needed, and ensure selectors (e.g., data-testid
) are available for queries.
<button onClick={() => doSomethingSecret()}>Go</button>
<button data-testid="submit-button" onClick={handleSubmit}>Go</button>
Use shorthand for boolean props
When a boolean prop is set to true
, use the JSX shorthand syntax for cleaner and more idiomatic code.
<Navbar showTitle={true} />
<Navbar showTitle />
Don’t wrap string props in braces
For static string values, omit to reduce visual clutter and improve consistency.
<Sidebar title={"My Special App"} />
<Sidebar title="My Special App" />
Avoid conflicting prop names
Avoid using prop names that conflict with reserved DOM attributes like className
, style
, or id
unless you intend to pass them directly to a DOM element.
1<MyComponent className="dark" />
2<MyComponent style="compact" />
1<MyComponent variant="dark" />
2<MyComponent layout="compact" />
Use consistent quotes in JSX
Use double quotes for JSX attributes (HTML or custom components), and use single quotes for strings inside JavaScript/TypeScript code. This avoids escaping and improves visual consistency.
1<div className='header'>
2 {title === "Main" ? 'Yes' : "No"}
3</div>
1<div className="header">
2 {title === 'Main' ? 'Yes' : 'No'}
3</div>
Use self-closing tags when possible
For elements that don’t have children, always use self-closing tags. It makes code cleaner and less error-prone.
1<Input></Input>
2<div className="spacer"></div>
1<Input />
2<div className="spacer" />
Order JSX props for readability
When a component has many props, order them consistently to improve scanability and reduce cognitive load. While there's no single "correct" order, a common convention is:
Essential props (e.g.
id
,to
,href
,type
)Structural/config props (e.g.
variant
,size
,align
,direction
)Booleans and modifiers (
disabled
,loading
,dense
)Event handlers (
onClick
,onChange
,onSubmit
)Styling props (
className
,style
)Data & children (
title
,label
,value
,children
)
1<Button
2 onClick={handleClick}
3 size="lg"
4 className="cta"
5 variant="primary"
6 disabled
7 id="main-action"
8>
9 Submit
10</Button>
1<Button
2 id="main-action"
3 variant="primary"
4 size="lg"
5 disabled
6 onClick={handleClick}
7 className="cta"
8>
9 Submit
10</Button>