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
UserCardabove the line where it's defined - Named in stack traces — errors show
UserCardby name - Has
thisbinding — 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— inheritsthisfrom the surrounding scope (irrelevant for function components but matters in class component callbacks) - Named in stack traces — V8 infers the name from the
constbinding - Requires a semicolon after the closing brace (it's a variable assignment)
- Popular in codebases that prefer
constfor 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 DevTools —
Page, notAnonymous - 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
Anonymousor_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
childrenin 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
| Form | Hoisted | Named in DevTools | Tree-Shakeable | Best For |
|---|---|---|---|---|
function Name() {} | Yes | Yes | Only if exported | General use, Next.js pages |
const Name = () => {} | No | Yes (inferred) | Only if exported | Teams preferring const |
const Name = () => () | No | Yes (inferred) | Only if exported | Tiny, JSX-only components |
export default function Name() | Yes | Yes | No (default) | Next.js pages/layouts |
export function Name() | Yes | Yes | Yes | Shared components, utilities |
export const Name = () => {} | No | Yes (inferred) | Yes | Consistent const style |
React.FC<Props> | No | Yes (inferred) | Only if exported | Legacy 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 neededforwardRef 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
-
Anonymous default exports lose their name in DevTools and stack traces.
export default function() {}shows asAnonymous. Always name the function, even in default exports. -
Arrow functions need
<T,>for generics in.tsxfiles. Without the trailing comma,<T>is parsed as a JSX element. Function declarations don't have this ambiguity. -
constcomponents 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. -
React.FCimplicitly includedchildrenbefore React 18. If you're migrating from React 17, removingReact.FCmay surface type errors wherechildrenwas used but not explicitly typed. -
Implicit return breaks when you add logic.
const Card = () => (<div>...</div>)must be rewritten toconst Card = () => { const x = ...; return <div>...</div>; }when you add any statement before the JSX. -
export defaultcombined withconstis a two-step process. You can't writeexport default const Name = ...— it's a syntax error. Either useexport default function Name()or declare the const first, thenexport default Name. -
ESLint rules can conflict.
react/function-component-definitionenforces one style.prefer-arrow-callbackmay conflict. Align your ESLint config with your team's chosen convention to avoid churn. -
Mixing styles in one file confuses contributors. If
UserCardis a function declaration andAvatarright below it is an arrow function, readers waste time wondering if the difference is intentional. Be consistent within a file.
Alternatives
| Alternative | Use When |
|---|---|
| Class components | Legacy 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 props | Dynamic composition of behavior — mostly replaced by hooks |
Related
- Components — composition patterns, props, children
- JSX and TSX — the syntax components return
- TypeScript Generics — generic component typing in depth
- React 19 ref as prop — forwardRef is no longer needed