Advanced30 min read

React Best Practices

Learn the conventions, patterns, and anti-patterns that distinguish professional React code from amateur code, covering component organization, accessibility, performance, and maintainability.

Component Organization and File Structure

How you organize your files has a direct impact on how quickly you can navigate, understand, and maintain your codebase. There is no single "correct" structure, but two patterns have proven effective at scale:

1. Feature-based structure (recommended for apps):

Group files by feature, not by type. Each feature folder contains everything it needs:

html
src/
  features/
    auth/
      LoginForm.tsx
      SignupForm.tsx
      useAuth.ts
      auth.test.ts
      auth.types.ts
    dashboard/
      Dashboard.tsx
      DashboardChart.tsx
      useDashboardData.ts
    shared/
      components/
        Button.tsx
        Modal.tsx
      hooks/
        useDebounce.ts
        useLocalStorage.ts

2. Flat component structure (for smaller projects):

html
src/
  components/
    Button/
      Button.tsx
      Button.test.tsx
      Button.module.css
      index.ts
    Header/
      Header.tsx
      Header.test.tsx
      index.ts

Key conventions:

  • One component per file. The file name matches the component name: UserCard.tsx exports UserCard.
  • Use an index.ts barrel file to simplify imports: import { Button } from '@/components/Button'.
  • Co-locate tests, styles, and types with the component they belong to.
  • Separate presentational components (pure UI, receive data via props) from container components (data fetching, business logic).
  • Keep components small. If a component file exceeds 200-300 lines, it is a signal to extract sub-components or custom hooks.

Naming Conventions and Single Responsibility

Consistent naming makes your codebase readable without documentation:

Components: PascalCase. Name them after what they render, not what they do.

html
UserCard.tsx      (good — describes what it renders)
FetchUser.tsx     (bad — describes implementation)

Hooks: camelCase with use prefix. Name them after what they provide.

html
useAuth()         (good — provides auth state and actions)
useLocalStorage() (good — provides localStorage access)
useFetch()        (ok — generic, but clear)

Event handlers: handle + event name in the component, on + event name for props.

jsx
// Inside the component
function SearchBar({ onSearch }) {
  const handleSubmit = (e) => {
    e.preventDefault();
    onSearch(query);
  };
  // ...
}

Boolean props: Use is, has, or should prefixes.

jsx
<Modal isOpen={true} hasOverlay={true} shouldCloseOnEscape={true} />

The Single Responsibility Principle states that each component should have one reason to change. Signs your component does too much:

  • It has more than 3-4 state variables.
  • It renders different UI based on many conditions.
  • It mixes data fetching with rendering.
  • You cannot describe what it does in one sentence.

Extract logic into custom hooks. Extract UI into sub-components. The result is components that are easier to test, reuse, and reason about.

State Management Guidelines

One of the biggest sources of complexity in React apps is mismanaged state. Follow these guidelines to keep state predictable:

1. Start with local state. Always start with useState in the component that needs the data. Only lift state up when siblings need to share it.

jsx
// Good — state is local to the component that uses it
function SearchBar() {
  const [query, setQuery] = React.useState('');
  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

2. Lift state only when necessary. When two sibling components need the same data, move state to their closest common parent:

jsx
function App() {
  const [user, setUser] = React.useState(null);
  return (
    <>
      <Header user={user} />
      <Profile user={user} onUpdate={setUser} />
    </>
  );
}

3. Use context for deeply shared state. When data needs to flow through many levels (theme, locale, auth), use the Provider pattern:

jsx
<AuthProvider>
  <ThemeProvider>
    <App />
  </ThemeProvider>
</AuthProvider>

4. Use external stores for complex state. When state logic becomes complex (many actions, derived state, async flows), use a state management library like Zustand or Redux Toolkit.

5. Derive state, do not sync it. If you can compute a value from existing state, do not store it separately:

jsx
// Bad — syncing derived state
const [items, setItems] = React.useState([]);
const [count, setCount] = React.useState(0); // redundant!

// Good — derive it
const [items, setItems] = React.useState([]);
const count = items.length; // computed on every render

6. Avoid state for ref-like values. Values that should not trigger re-renders (timer IDs, previous values, DOM references) belong in useRef, not useState.

Accessibility in React

Accessibility (a11y) is not an afterthought — it is a core quality requirement. React makes it easy to build accessible UIs when you follow these practices:

1. Use semantic HTML elements:

jsx
// Bad
<div onClick={handleClick}>Click me</div>

// Good
<button onClick={handleClick}>Click me</button>

Semantic elements come with built-in keyboard handling, focus management, and screen reader announcements. A <button> is focusable, responds to Enter/Space, and is announced as a button. A <div> does none of that.

2. Always provide text alternatives:

jsx
<img src="avatar.jpg" alt="Jane's profile photo" />
<button aria-label="Close dialog"><XIcon /></button>

3. Structure pages with landmarks:

jsx
function App() {
  return (
    <>
      <header><h1>My App</h1></header>
      <nav><a href="/">Home</a></nav>
      <main><p>Content goes here</p></main>
      <footer><p>Copyright info</p></footer>
    </>
  );
}

Screen readers use these landmarks (<header>, <nav>, <main>, <footer>) to let users jump between sections of the page.

4. Manage focus for dynamic content: When a modal opens, move focus into it. When it closes, return focus to the trigger element:

jsx
const triggerRef = React.useRef();
const modalRef = React.useRef();

const openModal = () => {
  setIsOpen(true);
  // Focus the modal after render
  setTimeout(() => modalRef.current?.focus(), 0);
};

const closeModal = () => {
  setIsOpen(false);
  triggerRef.current?.focus(); // Return focus
};

5. Use ARIA attributes when native HTML is insufficient:

jsx
<div role="alert" aria-live="polite">
  {errorMessage}
</div>

<button aria-expanded={isOpen} aria-controls="menu-content">
  Menu
</button>
<ul id="menu-content" role="menu" hidden={!isOpen}>
  <li role="menuitem">Option 1</li>
</ul>

6. Test with a keyboard. Navigate your entire UI using only Tab, Shift+Tab, Enter, Space, Escape, and arrow keys. If something is not reachable or operable, fix it.

Common Anti-Patterns to Avoid

Recognizing anti-patterns is as important as knowing best practices. Here are the most common mistakes:

1. Prop drilling through many layers:

jsx
// Bad — passing user through 5 levels
<App user={user}>
  <Layout user={user}>
    <Sidebar user={user}>
      <UserMenu user={user} />
    </Sidebar>
  </Layout>
</App>

// Good — use context
<UserProvider>
  <App />
</UserProvider>

2. Huge monolithic components: If your component file is 500+ lines, it is doing too much. Extract sub-components and hooks.

3. Unnecessary useEffect:

jsx
// Bad — useEffect to transform data
useEffect(() => {
  setFilteredItems(items.filter(i => i.active));
}, [items]);

// Good — compute during render
const filteredItems = items.filter(i => i.active);

useEffect is for synchronizing with external systems (API calls, subscriptions, DOM manipulation). If you are using it to derive state, you are overcomplicating things.

4. Missing keys or using index as key:

jsx
// Bad — index keys cause bugs when list order changes
{items.map((item, index) => <Item key={index} {...item} />)}

// Good — use a stable, unique identifier
{items.map(item => <Item key={item.id} {...item} />)}

5. Direct DOM manipulation:

jsx
// Bad — bypassing React
document.getElementById('title').textContent = 'New Title';

// Good — let React manage the DOM
setTitle('New Title');

6. Not cleaning up side effects:

jsx
// Bad — memory leak
useEffect(() => {
  window.addEventListener('resize', handleResize);
}, []);

// Good — cleanup function
useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

Performance checklist:

  • Memoize expensive calculations with useMemo.
  • Memoize callbacks passed to children with useCallback.
  • Use React.memo for components that re-render with the same props.
  • Lazy load heavy components with React.lazy and Suspense.
  • Profile with React DevTools before optimizing — do not guess.

Well-Structured App Example

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  // Each component has a single responsibility
  // Semantic HTML elements for accessibility
  // Clear naming conventions

  function Header() {
    return (
      <header id="header">
        <h1>My App</h1>
      </header>
    );
  }

  function Main() {
    return (
      <main id="main">
        <p>Content</p>
      </main>
    );
  }

  function Footer() {
    return (
      <footer id="footer">
        <p>&copy; 2024</p>
      </footer>
    );
  }

  // App composes independent components
  function App() {
    return (
      <>
        <Header />
        <Main />
        <Footer />
      </>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

Which of the following is an anti-pattern in React?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.