DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

Mastering React Components — Named vs Default Exports, PropTypes vs TypeScript, and Production-Ready Patterns

Mastering React Components — Named vs Default Exports, PropTypes vs TypeScript, and Production-Ready Patterns

Mastering React Components — Named vs Default Exports, PropTypes vs TypeScript, and Production-Ready Patterns

You’ve probably seen the same component written three different ways:

// 1) Named export
export const RepoStats = () => {
  return <div>RepoStats</div>
}

// 2) Default export
const RepoStats = () => {
  return <div>RepoStats</div>
}
export default RepoStats

// 3) With PropTypes (runtime validation)
import PropTypes from 'prop-types'
const RepoStats = (props) => {
  return <div>RepoStats</div>
}
RepoStats.propTypes = {}
export default RepoStats
Enter fullscreen mode Exit fullscreen mode

They all render the same <div>RepoStats</div>, but their implications for API design, DX, testing, and scale are very different. In this post, we’ll treat this simple example as a springboard to expert-level React component patterns you can use in real projects.


1) Named vs Default Exports — Which should you prefer?

TL;DR: Prefer named exports for most components. Use default exports for framework‑constrained files (e.g. Next.js pages, Remix routes) or when you truly want “one canonical export per file”.

Why many teams prefer named exports

  • Refactors are safer: Rename a component and your tooling updates imports automatically (import { RepoStats } from ...).
  • Auto‑imports are better: Editors can reliably suggest the right symbol.
  • Avoids “mystery names”: import Widget from './RepoStats' compiles, even if the symbol is actually RepoStats. With named exports, consumers must ask for the real name.
  • Tree‑shaking is great either way with ESM, but named exports make dead‑code elimination more predictable in some bundlers.

When default exports still shine

  • You intentionally want import RepoStats from '...' for ergonomic DX.
  • Framework conventions require default (e.g. Next.js pages/components in specific folders).
  • You are exporting a single object that feels like “the module”.

Team rule of thumb (a common ESLint setup):

// .eslintrc excerpt
{
  "rules": {
    "import/no-default-export": "warn"  // prefer named
  }
}
Enter fullscreen mode Exit fullscreen mode

Pragmatic exception: allow defaults in specific folders via overrides (e.g. /app/** in Next.js).


2) Function Declarations vs Arrow Functions

Both are valid for function components. Most codebases use arrow functions:

export const RepoStats = () => <div>RepoStats</div>
Enter fullscreen mode Exit fullscreen mode

Key differences:

  • Hoisting: function declarations are hoisted; arrows aren’t. For components, hoisting rarely matters because we import from other files.
  • Statics: you can attach statics to either, but arrows make React.memo and forwardRef composition more ergonomic:
const RepoStatsBase = ({ title = 'RepoStats' }) => <div>{title}</div>
export const RepoStats = Object.assign(React.memo(RepoStatsBase), {
  displayName: 'RepoStats'
})
Enter fullscreen mode Exit fullscreen mode

displayName helps DevTools, especially after HOCs.


3) Props: PropTypes (runtime) vs TypeScript (compile‑time)

PropTypes

  • Runtime validation: helpful when publishing JS packages, or when users don’t use TypeScript.
  • Catches incorrect usage in production (dev only by default, but can be bundled intentionally).
  • Limited expressiveness vs TypeScript.
import PropTypes from 'prop-types'

export const RepoStats = ({ owner, repo, showStars = true }) => (
  <section>
    <h3>{owner}/{repo}</h3>
    {showStars && <span>⭐ 1234</span>}
  </section>
)

RepoStats.propTypes = {
  owner: PropTypes.string.isRequired,
  repo: PropTypes.string.isRequired,
  showStars: PropTypes.bool
}
Enter fullscreen mode Exit fullscreen mode

TypeScript

  • Best DX: autocomplete, inline docs, strict unions, generics.
  • Prevents bugs before runtime.
  • You can still ship PropTypes for JS consumers (dual safety).
type RepoStatsProps = {
  owner: string
  repo: string
  showStars?: boolean
  children?: React.ReactNode
}

export const RepoStats: React.FC<RepoStatsProps> = ({
  owner,
  repo,
  showStars = true,
  children
}) => {
  return (
    <section aria-label="Repository statistics">
      <h3>{owner}/{repo}</h3>
      {showStars && <span>⭐ 1234</span>}
      {children}
    </section>
  )
}
Enter fullscreen mode Exit fullscreen mode

Avoid React.FC? It’s fine to use. If you prefer, just write ({ children }: Props) => … and type children explicitly.


4) Default Props — Use default parameters, not Component.defaultProps

For function components, prefer default parameters:

export const RepoStats = ({ showStars = true }) => { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

RepoStats.defaultProps still works, but default parameters are simpler and type‑safe.


5) Composition, children, and as props

Design components for composition:

type RepoStatsProps = {
  owner: string
  repo: string
  footer?: React.ReactNode
  children?: React.ReactNode
}

export function RepoStats({ owner, repo, children, footer }: RepoStatsProps) {
  return (
    <article>
      <header><strong>{owner}/{repo}</strong></header>
      <div>{children}</div>
      {footer && <footer>{footer}</footer>}
    </article>
  )
}
Enter fullscreen mode Exit fullscreen mode

For highly reusable UI primitives, consider a polymorphic as prop and forwarding refs:

import React, { forwardRef, ElementType, ComponentPropsWithoutRef } from 'react'

type BoxProps<T extends ElementType> = {
  as?: T
} & Omit<ComponentPropsWithoutRef<T>, 'as'>

export const Box = forwardRef(
  <T extends ElementType = 'div'>(
    { as, ...rest }: BoxProps<T>,
    ref: React.Ref<Element>
  ) => {
    const Tag = as || 'div'
    return <Tag ref={ref as any} {...rest} />
  }
)
Box.displayName = 'Box'
Enter fullscreen mode Exit fullscreen mode

6) Performance: React.memo, stable handlers, and when not to optimize

  • Wrap leaf components with React.memo when rerenders are frequent and props are stable.
  • Use useCallback/useMemo to stabilize expensive functions/values. Don’t sprinkle them everywhere—measure first.
  • Prefer keys that don’t change and avoid array index keys in dynamic lists.
  • With lists, memoize item rows; pass stable item props.
import { memo, useCallback } from 'react'

type RowProps = { id: string; onToggle(id: string): void }

const Row = memo(({ id, onToggle }: RowProps) => (
  <button onClick={() => onToggle(id)}>{id}</button>
))

export function List({ items }: { items: string[] }) {
  const onToggle = useCallback((id: string) => { /* ... */ }, [])
  return items.map((id) => <Row key={id} id={id} onToggle={onToggle} />)
}
Enter fullscreen mode Exit fullscreen mode

7) File Organization & Re‑exports

  • One component per file; colocate tests & stories: RepoStats/RepoStats.tsx, RepoStats/RepoStats.test.tsx, RepoStats/RepoStats.stories.tsx
  • Barrel export for ergonomics:
// src/components/index.ts
export { RepoStats } from './RepoStats/RepoStats'
export { Box } from './Box/Box'
Enter fullscreen mode Exit fullscreen mode

Consumers can now:

import { RepoStats, Box } from '@/components'
Enter fullscreen mode Exit fullscreen mode

8) Testing Considerations (named vs default)

  • Named export is easier to mock & import in isolation:
  import { RepoStats } from './RepoStats'
Enter fullscreen mode Exit fullscreen mode
  • With defaults, you often need:
  import RepoStats from './RepoStats'
Enter fullscreen mode Exit fullscreen mode
  • Both are fine—just stay consistent with your chosen convention.

9) A Production‑Ready RepoStats (two flavors)

A) TypeScript + React.memo + forwardRef

import React, { forwardRef, memo } from 'react'

type RepoStatsProps = {
  owner: string
  repo: string
  stars?: number
  watchers?: number
  forks?: number
  className?: string
}

const RepoStatsBase = forwardRef<HTMLDivElement, RepoStatsProps>(
  ({ owner, repo, stars, watchers, forks, className }, ref) => {
    return (
      <section ref={ref} className={className} aria-label="Repository statistics">
        <header><strong>{owner}/{repo}</strong></header>
        <ul>
          {typeof stars === 'number' && <li>{stars}</li>}
          {typeof watchers === 'number' && <li>👀 {watchers}</li>}
          {typeof forks === 'number' && <li>🍴 {forks}</li>}
        </ul>
      </section>
    )
  }
)
RepoStatsBase.displayName = 'RepoStats'

export const RepoStats = memo(RepoStatsBase)
Enter fullscreen mode Exit fullscreen mode

B) JavaScript + PropTypes (for libraries shipping JS)

import React from 'react'
import PropTypes from 'prop-types'

const RepoStats = ({ owner, repo, stars, watchers, forks, className }) => (
  <section className={className} aria-label="Repository statistics">
    <header><strong>{owner}/{repo}</strong></header>
    <ul>
      {Number.isFinite(stars) && <li>{stars}</li>}
      {Number.isFinite(watchers) && <li>👀 {watchers}</li>}
      {Number.isFinite(forks) && <li>🍴 {forks}</li>}
    </ul>
  </section>
)

RepoStats.propTypes = {
  owner: PropTypes.string.isRequired,
  repo: PropTypes.string.isRequired,
  stars: PropTypes.number,
  watchers: PropTypes.number,
  forks: PropTypes.number,
  className: PropTypes.string
}

export default RepoStats
Enter fullscreen mode Exit fullscreen mode

10) Accessibility & Semantics (don’t skip this)

  • Use meaningful landmarks: section, header, nav, main, footer.
  • Add aria‑labels only when semantics aren’t obvious.
  • Ensure color contrast; don’t rely solely on color to convey state.
  • Keyboard first: focus states, tabIndex, actionable components as real buttons/links.

11) Putting it all together — Decision Checklist

  • Export: named (prefer), default when framework demands it.
  • Typing: TypeScript in app code; PropTypes if shipping JS for others.
  • Defaults: parameter defaults, not defaultProps (for FCs).
  • Composition: accept children; consider as and forwardRef for primitives.
  • Performance: memo leaf nodes; measure before optimizing; stable handlers.
  • DX: set ESLint/prettier rules; add barrel exports for ergonomics.
  • A11y: semantics first; test with keyboard & screen reader.

Conclusion

All three versions of RepoStats render the same thing, but they signal very different architectural decisions. By choosing your export strategy, typing approach, and composition patterns deliberately, you’ll ship components that are easier to use, safer to change, and ready for scale.

Pick a convention, enforce it with lint rules, and document it in your project README. Your future self (and your teammates) will thank you.


✍️ Written by: Cristian Sifuentes — Full‑stack developer & AI/JS enthusiast. I design scalable React architectures and teach teams how to ship clean, accessible, and maintainable UI.

Tags: #react #frontend #programming #components #bestpractices

Top comments (0)