Best practices for building maintainable React functional components with proper structure and naming.
Use PascalCase for component files and follow a consistent file structure that makes components easy to find and maintain.
AppLayout.tsx // ✅ Good component naming and structure
// File: components/layout/AppLayout.tsx
export const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
return (
<div className="app-layout">
<Header />
<main>{children}</main>
<Footer />
</div>
);
};
// File: components/user/UserProfile.tsx
export const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};
// File: components/forms/ContactForm.tsx
export const ContactForm: React.FC = () => {
const [formData, setFormData] = useState<ContactFormData>({
name: '',
email: '',
message: ''
});
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
</form>
);
};
// ❌ Avoid - poor naming conventions
// File: components/layout.tsx (not descriptive)
export const Layout = () => { /* ... */ };
// File: components/form.js (not specific, wrong extension)
export const Form = () => { /* ... */ };
// File: components/userprofile.tsx (not PascalCase)
export const userprofile = () => { /* ... */ }; Always use functional components with hooks. Follow a consistent internal structure for better readability.
// ✅ Recommended component structure
interface UserDashboardProps {
userId: string;
onUserUpdate?: (user: User) => void;
}
interface User {
id: string;
name: string;
email: string;
lastLogin?: Date;
}
export const UserDashboard: React.FC<UserDashboardProps> = ({
userId,
onUserUpdate
}) => {
// 1. State hooks first
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 2. Custom hooks
const { updateUser } = useUserApi();
// 3. Effect hooks
useEffect(() => {
fetchUserData();
}, [userId]);
// 4. Event handlers and other functions
const fetchUserData = async () => {
try {
setIsLoading(true);
const userData = await getUserById(userId);
setUser(userData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsLoading(false);
}
};
const handleUserUpdate = async (updates: Partial<User>) => {
if (!user) return;
const updatedUser = await updateUser(user.id, updates);
setUser(updatedUser);
onUserUpdate?.(updatedUser);
};
// 5. Early returns for loading/error states
if (isLoading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!user) {
return <EmptyState message="User not found" />;
}
// 6. Main render
return (
<div className="user-dashboard">
<UserProfile user={user} onUpdate={handleUserUpdate} />
<UserActivity userId={user.id} />
<UserSettings user={user} onUpdate={handleUserUpdate} />
</div>
);
};
// ❌ Avoid - class components
class UserDashboard extends React.Component<UserDashboardProps> {
// Class components are deprecated in our codebase
}
// ❌ Avoid - unstructured functional component
export const UserDashboard = ({ userId }) => {
const handleClick = () => { /* ... */ };
const [user, setUser] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
// effect logic mixed with other code
});
return <div>{/* ... */}</div>;
}; Design clear, well-typed interfaces that make component usage obvious and prevent common errors.
// ✅ Well-designed props interfaces
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
loading?: boolean;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
className?: string;
}
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
onClick,
type = 'button',
className = ''
}) => {
return (
<button
type={type}
disabled={disabled || loading}
onClick={onClick}
className={`btn btn-${variant} btn-${size} ${className}`}
>
{loading ? <Spinner /> : children}
</button>
);
};
// ✅ Complex component with optional sections
interface ProductCardProps {
product: Product;
showPrice?: boolean;
showDescription?: boolean;
showActions?: boolean;
onAddToCart?: (product: Product) => void;
onViewDetails?: (productId: string) => void;
}
export const ProductCard: React.FC<ProductCardProps> = ({
product,
showPrice = true,
showDescription = true,
showActions = true,
onAddToCart,
onViewDetails
}) => {
return (
<div className="product-card">
<img src={product.imageUrl} alt={product.name} />
<h3>{product.name}</h3>
{showDescription && product.description && (
<p>{product.description}</p>
)}
{showPrice && (
<span className="price">${product.price}</span>
)}
{showActions && (
<div className="actions">
<Button onClick={() => onViewDetails?.(product.id)}>
View Details
</Button>
<Button
variant="primary"
onClick={() => onAddToCart?.(product)}
>
Add to Cart
</Button>
</div>
)}
</div>
);
};
// ✅ Generic component with flexible children
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
size?: 'small' | 'medium' | 'large';
children: React.ReactNode;
}
export const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
size = 'medium',
children
}) => {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div
className={`modal modal-${size}`}
onClick={(e) => e.stopPropagation()}
>
{title && (
<header className="modal-header">
<h2>{title}</h2>
<button onClick={onClose}>×</button>
</header>
)}
<div className="modal-body">
{children}
</div>
</div>
</div>
);
};
// ❌ Avoid - poorly typed props
interface BadProps {
data: any; // Too generic
callback: Function; // Untyped function
config: object; // Generic object
}
// ❌ Avoid - missing default values
export const BadButton = ({ variant, size, children }: any) => {
// No defaults, will cause undefined behavior
return <button className={`btn-${variant}-${size}`}>{children}</button>;
}; Build components that are easy to compose and reuse across your application.
// ✅ Composable layout components
interface CardProps {
children: React.ReactNode;
className?: string;
}
export const Card: React.FC<CardProps> = ({ children, className = '' }) => {
return (
<div className={`card ${className}`}>
{children}
</div>
);
};
export const CardHeader: React.FC<CardProps> = ({ children, className = '' }) => {
return (
<div className={`card-header ${className}`}>
{children}
</div>
);
};
export const CardBody: React.FC<CardProps> = ({ children, className = '' }) => {
return (
<div className={`card-body ${className}`}>
{children}
</div>
);
};
export const CardFooter: React.FC<CardProps> = ({ children, className = '' }) => {
return (
<div className={`card-footer ${className}`}>
{children}
</div>
);
};
// Usage - flexible composition
const UserProfileCard = ({ user }: { user: User }) => {
return (
<Card>
<CardHeader>
<h2>{user.name}</h2>
</CardHeader>
<CardBody>
<p>Email: {user.email}</p>
<p>Role: {user.role}</p>
</CardBody>
<CardFooter>
<Button onClick={handleEdit}>Edit Profile</Button>
</CardFooter>
</Card>
);
};
// ✅ Higher-order component for data fetching
interface WithLoadingProps<T> {
data: T | null;
isLoading: boolean;
error: string | null;
children: (data: T) => React.ReactNode;
loadingComponent?: React.ReactNode;
errorComponent?: (error: string) => React.ReactNode;
}
export const WithLoading = <T,>({
data,
isLoading,
error,
children,
loadingComponent = <LoadingSpinner />,
errorComponent = (err: string) => <ErrorMessage message={err} />
}: WithLoadingProps<T>) => {
if (isLoading) return <>{loadingComponent}</>;
if (error) return <>{errorComponent(error)}</>;
if (!data) return <EmptyState />;
return <>{children(data)}</>;
};
// Usage with data fetching
const UserList = () => {
const { users, isLoading, error } = useUsers();
return (
<WithLoading
data={users}
isLoading={isLoading}
error={error}
loadingComponent={<div>Loading users...</div>}
>
{(users) => (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
)}
</WithLoading>
);
};
// ✅ Render prop pattern for flexible UI
interface DataListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
renderEmpty?: () => React.ReactNode;
className?: string;
}
export const DataList = <T,>({
items,
renderItem,
renderEmpty = () => <p>No items found</p>,
className = ''
}: DataListProps<T>) => {
if (items.length === 0) {
return <div className={className}>{renderEmpty()}</div>;
}
return (
<div className={`data-list ${className}`}>
{items.map((item, index) => (
<div key={index} className="data-list-item">
{renderItem(item, index)}
</div>
))}
</div>
);
};
// Usage - flexible rendering
const ProductList = ({ products }: { products: Product[] }) => {
return (
<DataList
items={products}
renderItem={(product) => (
<ProductCard
product={product}
onAddToCart={handleAddToCart}
/>
)}
renderEmpty={() => (
<EmptyState
title="No products found"
action={<Button onClick={handleRefresh}>Refresh</Button>}
/>
)}
/>
);
};