React SME Cookbook
All FAQs
componentsarrow-functionsfunction-declarationdefault-exportnamed-exportsyntax

Function Component Syntax Variations

Every way to write a React function component — declaration vs. expression, arrow vs. function, default vs. named export, and when each form matters.

Recipe

Quick-reference recipe card.

// 1. Function declaration (most common)
function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}</h1>;
}
 
// 2. Arrow function assigned to const
const Greeting = ({ name }: { name: string }) => {
  return <h1>Hello, {name}</h1>;
};
 
// 3. Arrow function with implicit return
const Greeting = ({ name }: { name: string }) => (
  <h1>Hello, {name}</h1>
);
 
// 4. Default export — function declaration
export default function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}</h1>;
}
 
// 5. Default export — arrow function (two-step)
const Greeting = ({ name }: { name: string }) => (
  <h1>Hello, {name}</h1>
);
export default Greeting;
 
// 6. Named export
export function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}</h1>;
}
 
// 7. Named export — arrow function
export const Greeting = ({ name }: { name: string }) => (
  <h1>Hello, {name}</h1>
);

When to reach for this: You don't need to choose one style exclusively — but you should understand what each form gives you and pick consistently within a project.

All Variations Explained

1. Function Declaration

function UserCard({ name, email }: { name: string; email: string }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>{email}</p>
    </div>
  );
}
  • Hoisted — you can call UserCard above the line where it's defined
  • Named in stack traces — errors show UserCard by name
  • Has this binding — irrelevant for components but means it can be used with .bind() if needed
  • Most common in React docs and Next.js conventions

2. Arrow Function Expression

const UserCard = ({ name, email }: { name: string; email: string }) => {
  return (
    <div>
      <h2>{name}</h2>
      <p>{email}</p>
    </div>
  );
};
  • Not hoisted — must be defined before use in the same file
  • Lexical this — inherits this from the surrounding scope (irrelevant for function components but matters in class component callbacks)
  • Named in stack traces — V8 infers the name from the const binding
  • Requires a semicolon after the closing brace (it's a variable assignment)
  • Popular in codebases that prefer const for everything

3. Arrow Function with Implicit Return

const UserCard = ({ name, email }: { name: string; email: string }) => (
  <div>
    <h2>{name}</h2>
    <p>{email}</p>
  </div>
);
  • Parentheses replace the braces and return — the entire body is a single expression
  • Good for simple, JSX-only components with no logic before the return
  • Breaks the moment you add a variable, hook, or conditional — you must convert to block body

4. Default Export — Function Declaration

export default function Page() {
  return <main>Home</main>;
}
  • Next.js convention for page.tsx, layout.tsx, loading.tsx, error.tsx
  • Shows the name in React DevToolsPage, not Anonymous
  • One per file — only one default export allowed

5. Default Export — Arrow Function (Two-Step)

const Page = () => {
  return <main>Home</main>;
};
export default Page;
  • Separates definition from export — useful when you need to attach static properties
  • Same DevTools behavior — React infers the name from the variable
// Attaching static properties
const Tabs = ({ children }: { children: React.ReactNode }) => {
  return <div>{children}</div>;
};
Tabs.Panel = TabPanel;
Tabs.List = TabList;
export default Tabs;

6. Named Export — Function Declaration

export function UserCard({ name }: { name: string }) {
  return <h2>{name}</h2>;
}
  • Import with braces: import { UserCard } from "./user-card"
  • Tree-shakeable — bundlers can drop unused named exports
  • Multiple per file — put related components together
// components/status.tsx — multiple related components
export function StatusBadge({ status }: { status: string }) {
  return <span className="badge">{status}</span>;
}
 
export function StatusIcon({ status }: { status: string }) {
  return <span className="icon">{status === "active" ? "🟢" : "🔴"}</span>;
}

7. Named Export — Arrow Function

export const UserCard = ({ name }: { name: string }) => (
  <h2>{name}</h2>
);
  • Same tree-shaking benefits as named function export
  • Some teams enforce this via ESLint for consistency with non-component exports

8. Inline Default Export (Anonymous)

export default function ({ name }: { name: string }) {
  return <h2>{name}</h2>;
}
  • No name in DevTools — shows as Anonymous or _default
  • Hard to debug — stack traces lack a meaningful name
  • Avoid this form — always name your components

9. React.FC / React.FunctionComponent (Legacy)

const UserCard: React.FC<{ name: string }> = ({ name }) => {
  return <h2>{name}</h2>;
};
  • Implicitly includes children in older React types (pre-React 18)
  • React 18+ removed implicit children from React.FC — now equivalent to plain typing
  • Adds no value over direct prop typing — most style guides recommend against it
  • Still seen in legacy codebases — recognize it but don't use it in new code

Comparison Table

FormHoistedNamed in DevToolsTree-ShakeableBest For
function Name() {}YesYesOnly if exportedGeneral use, Next.js pages
const Name = () => {}NoYes (inferred)Only if exportedTeams preferring const
const Name = () => ()NoYes (inferred)Only if exportedTiny, JSX-only components
export default function Name()YesYesNo (default)Next.js pages/layouts
export function Name()YesYesYesShared components, utilities
export const Name = () => {}NoYes (inferred)YesConsistent const style
React.FC<Props>NoYes (inferred)Only if exportedLegacy code only

Which Should You Pick?

There is no functional difference between function and arrow for components. React doesn't care. Pick based on your team's conventions:

Recommended defaults:

// Next.js pages, layouts, routes → default export + function declaration
export default function DashboardPage() { ... }
 
// Shared components → named export + function declaration
export function UserCard({ name }: UserCardProps) { ... }
 
// Internal helpers in the same file → plain function declaration
function formatStatus(status: string) { ... }

If your team uses arrow functions:

// Next.js pages → still use function declaration for readability
export default function DashboardPage() { ... }
 
// Everything else → arrow
export const UserCard = ({ name }: UserCardProps) => { ... };
const formatStatus = (status: string) => { ... };

Deep Dive

Hoisting in Practice

Function declarations are hoisted — they're available before their textual position in the file. Arrow functions assigned to const are not.

// This works — function declaration is hoisted
export default function Page() {
  return <Sidebar />;
}
 
function Sidebar() {
  return <nav>Links</nav>;
}
// This fails — const is NOT hoisted
export default function Page() {
  return <Sidebar />; // ReferenceError: Cannot access 'Sidebar' before initialization
}
 
const Sidebar = () => <nav>Links</nav>;

In practice, this rarely matters because most components are in separate files. It matters when you have helper components below the main export in the same file.

Generic Components

Function declarations and arrow functions handle generics differently in TSX:

// Function declaration — clean syntax
function List<T>({ items, render }: { items: T[]; render: (item: T) => React.ReactNode }) {
  return <ul>{items.map((item, i) => <li key={i}>{render(item)}</li>)}</ul>;
}
 
// Arrow function — needs trailing comma to disambiguate from JSX
const List = <T,>({ items, render }: { items: T[]; render: (item: T) => React.ReactNode }) => {
  return <ul>{items.map((item, i) => <li key={i}>{render(item)}</li>)}</ul>;
};

The <T,> with a trailing comma is necessary because <T> in a .tsx file is parsed as a JSX tag. The comma tells TypeScript it's a generic. This is one case where function declarations have a clear syntactic advantage.

displayName for Debugging

Higher-order components and React.memo can lose the component name. Set displayName explicitly:

const UserCard = React.memo(({ name }: { name: string }) => (
  <h2>{name}</h2>
));
UserCard.displayName = "UserCard";
 
// With a function declaration, the name is preserved automatically
const UserCard = React.memo(function UserCard({ name }: { name: string }) {
  return <h2>{name}</h2>;
});
// displayName is automatically "UserCard" — no manual assignment needed

forwardRef Variations

// Function expression inside forwardRef
const Input = React.forwardRef<HTMLInputElement, InputProps>(
  function Input({ label, ...props }, ref) {
    return (
      <label>
        {label}
        <input ref={ref} {...props} />
      </label>
    );
  }
);
 
// Arrow function inside forwardRef
const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ label, ...props }, ref) => (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  )
);
 
// React 19: ref is a regular prop — no forwardRef needed
function Input({ label, ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  );
}

Async Server Components

Server Components can be async — a syntax that only works with function declarations and arrow functions with block bodies:

// Function declaration — clean
export default async function PostsPage() {
  const posts = await getPosts();
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
 
// Arrow function — also works
const PostsPage = async () => {
  const posts = await getPosts();
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
};
export default PostsPage;

Implicit return (=> (...)) cannot be async because await is a statement, not an expression.

Gotchas

  1. Anonymous default exports lose their name in DevTools and stack traces. export default function() {} shows as Anonymous. Always name the function, even in default exports.

  2. Arrow functions need <T,> for generics in .tsx files. Without the trailing comma, <T> is parsed as a JSX element. Function declarations don't have this ambiguity.

  3. const components aren't hoisted. If you define helper components below the main export in the same file, use function declarations or move the helpers above the main component.

  4. React.FC implicitly included children before React 18. If you're migrating from React 17, removing React.FC may surface type errors where children was used but not explicitly typed.

  5. Implicit return breaks when you add logic. const Card = () => (<div>...</div>) must be rewritten to const Card = () => { const x = ...; return <div>...</div>; } when you add any statement before the JSX.

  6. export default combined with const is a two-step process. You can't write export default const Name = ... — it's a syntax error. Either use export default function Name() or declare the const first, then export default Name.

  7. ESLint rules can conflict. react/function-component-definition enforces one style. prefer-arrow-callback may conflict. Align your ESLint config with your team's chosen convention to avoid churn.

  8. Mixing styles in one file confuses contributors. If UserCard is a function declaration and Avatar right below it is an arrow function, readers waste time wondering if the difference is intentional. Be consistent within a file.

Alternatives

AlternativeUse When
Class componentsLegacy codebases, error boundaries (until React 19 useErrorBoundary)
Server Components (async function)Data fetching at render time with no client JS
Higher-order components (HOC)Cross-cutting concerns (auth, logging) — less common now with hooks
Render propsDynamic composition of behavior — mostly replaced by hooks