There are many ways to write a React component, and no clear standard has been established within the community. Ideally, we’d all follow a consistent format so components look and feel the same.
Until then, here’s what I think is key to writing readable and maintainable React components.
It’s now the consensus: the React community has overwhelmingly embraced functional components as the preferred approach, and the official documentation explicitly recommends writing components as functions.
Functional components are better because:
It is my preference to declare the components props before the component as a named and exported type. Always with the name [component-name]Props
.
export type CounterProps = { count: number };
export function Counter(props: CounterProps)
Rather than:
export function Counter(props: { count: number })
This is mostly my own personal preference but it has some benefits.
ComponentProps<typeof Component>
.When destructuring props inside the parameters, you often end up with messy looking code. Combine this with declaring props inside the parameters and you got yourself a cluttered mess.
Don’t do this 👎:
export function Counter({
count,
max,
min,
title,
}: { count: number; max: number; min: number; title: string }) {}
We often overuse destructuring. Simply reading the value from props.count
is often a good option, as it makes it clear you’re accessing a prop and where it comes from. However, in React we sometimes need to destructure to exclude props we don’t want to pass to a child. In those cases, I recommend destructuring inside the function instead.
Do this 👍:
export type CounterProps = {
count: number;
max: number;
min: number;
title: string;
};
export function Counter(props: CounterProps) {
const { count, max, min, title } = props;
}
This point is mainly about code aesthetics. I think arrow functions are overused in the JavaScript community. Historically, lambdas were meant for short, single-line functions—not 100-line React components. Personally, I also think it just looks cleaner to use a regular function. Since arrow functions don’t make the code any shorter in this case, I don’t see any reason to use them for React components.
Don’t do this 👎:
export const Counter = (props: CounterProps) => {
return <div />;
};
Do this 👍:
export function Counter(props: CounterProps) {
return <div />;
}
It is usually a good practice to include the first child’s props in your component’s props. Like this:
import type { ComponentProps } from "react";
export type CounterProps = ComponentProps<"div"> & {
count: number;
};
export function Counter(props: CounterProps) {
const { count, children, ...rest } = props;
return <div {...rest}>{children}</div>;
}
I think this is a good idea because you’re creating a more reusable component, and less need for wrapping it in useless divs.
Another good practice is to merge props wherever possible—especially for event handlers, style
, and className
.
Here’s an example of a component that merges all three.
import { useState, type ComponentProps, type MouseEventHandler } from "react";
import { cn } from "@/lib/utils";
export type CounterProps = ComponentProps<"button">;
export function Counter(props: CounterProps) {
const { children, className, style, onClick, ...rest } = props;
const [count, setCount] = useState(0);
const clickHandler: MouseEventHandler<HTMLButtonElement> = (e) => {
onClick?.(e);
if (e.isDefaultPrevented()) {
return;
}
setCount((prev) => prev + 1);
};
return (
<button
type="button"
className={cn("bg-red", className)}
style={{
cursor: "pointer",
...style,
}}
onClick={clickHandler}
{...rest}
>
{children}
</button>
);
}
Making your component composable is sometimes a good idea. However, often it makes more sense to keep it as a single component, especially for smaller components. Use this approach carefully and weigh the pros and cons. A composable component can be less obvious to someone unfamiliar with how it’s meant to be used.
Here’s an example of what a composable component looks like:
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils.ts";
export type ArticleProps = ComponentProps<"article">;
export function Article(props: ArticleProps) {
const { className, children, ...rest } = props;
return (
<article className={cn("bg-slate-300 grid gap-2", className)} {...rest}>
{children}
</article>
);
}
export type ArticleTitleProps = ComponentProps<"h2">;
export function ArticleTitle(props: ArticleTitleProps) {
const { className, children, ...rest } = props;
return (
<h2 className={cn("text-xl font-medium", className)} {...rest}>
{children}
</h2>
);
}
export type ArticleDescriptionProps = ComponentProps<"p">;
export function ArticleDescription(props: ArticleDescriptionProps) {
const { className, children, ...rest } = props;
return (
<p className={cn("tracking-tight text-slate-700", className)} {...rest}>
{children}
</p>
);
}
That can be used like this:
export function Page() {
return (
<Article>
<ArticleTitle>My first article</ArticleTitle>
<ArticleDescription>This is a description</ArticleDescription>
</Article>
);
}
This one is tricky—probably the hardest to get right. On one hand, I think it’s unhelpful to blindly follow a rule about keeping components under a certain number of lines. On the other hand, writing extremely long components can make your code hard to read. The key is to stay flexible rather than enforcing a strict limit. When it makes sense, break your component into smaller pieces. But be careful: splitting it the wrong way can actually make it even harder to follow.