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.
Search across all documentation pages
Every way to write a React function component - declaration vs. expression, arrow vs. function, default vs. named export, and when each form matters.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
function UserCard({ name, email }: { name: string; email: string }) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}UserCard above the line where it's definedUserCard by namethis binding - irrelevant for components but means it can be used with .bind() if neededconst UserCard = ({ name, email }: { name: string; email: string }) => {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
</div>
);
};this - inherits this from the surrounding scope (irrelevant for function components but matters in class component callbacks)const bindingconst for everythingconst UserCard = ({ name, email }: { name: string; email: string }) => (
<div>
<h2>{name}</h2>
<p>{email}</p>
</div>
);return - the entire body is a single expressionexport default function Page() {
return <main>Home</main>;
}page.tsx, layout.tsx, loading.tsx, error.tsxPage, not Anonymousconst Page = () => {
return <main>Home</main>;
};
export default Page;// Attaching static properties
const Tabs = ({ children }: { children: React.ReactNode }) => {
return <div>{children}</div>;
};
Tabs.Panel = TabPanel;
Tabs.List = TabList;
export default Tabs;export function UserCard({ name }: { name: string }) {
return <h2>{name}</h2>;
}import { UserCard } from "./user-card"// 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>;
}export const UserCard = ({ name }: { name: string }) => (
<h2>{name}</h2>
);export default function ({ name }: { name: string }) {
return <h2>{name}</h2>;
}Anonymous or _defaultReact.FC / React.FunctionComponent (Legacy)const UserCard: React.FC<{ name: string }> = ({ name }) => {
return <h2>{name}</h2>;
};children in older React types (pre-React 18)React.FC - now equivalent to plain typing| 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 |
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) => { ... };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.
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 DebuggingHigher-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>
);
}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.
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.
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.
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.
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.
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.
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.
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.
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.
| 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 |
Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥