This guide covers best practices for creating React components in the Node.js website project.
- Component Architecture
- Component Guidelines
- Component Structure
- Component Implementation
- Styling Components
- TypeScript Best Practices
- Component Domain Organization
- Testing Components
The website uses a dual-package architecture for React components:
@node-core/ui-components: Reusable, framework-agnostic UI componentsapps/site/components: Website-specific components that use Next.js features
Generic UI components should be placed in @node-core/ui-components:
// @node-core/ui-components/src/Common/Button/index.tsx
import type { FC, ButtonHTMLAttributes } from 'react';
import styles from './index.module.css';
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
};
const Button: FC<ButtonProps> = ({
variant = 'primary',
size = 'medium',
children,
className,
...props
}) => (
<button
className={`${styles.button} ${styles[variant]} ${styles[size]} ${className || ''}`}
{...props}
>
{children}
</button>
);
export default Button;Components that need Next.js features should be in apps/site/components, such as components that require routing.
Each component should have its own folder with this structure:
ComponentName/
├── index.tsx # Component implementation
├── index.module.css # Component styles
├── index.stories.tsx # Storybook stories (ui-components only)
└── __tests__/
└── index.test.mjs # Unit tests
If a component has sub-components, organize them hierarchically:
ComponentName/
├── index.tsx
├── index.module.css
├── SubComponent/
│ ├── index.tsx
│ └── index.module.css
└── AnotherSubComponent/
├── index.tsx
└── index.module.css
import type { FC } from 'react';
import styles from './index.module.css';
type MyComponentProps = {
title: string;
description?: string;
isVisible?: boolean;
};
const MyComponent: FC<MyComponentProps> = ({
title,
description,
isVisible = true,
}) => {
if (!isVisible) return null;
return (
<div className={styles.container}>
<h2 className={styles.title}>{title}</h2>
{description && <p className={styles.description}>{description}</p>}
</div>
);
};
export default MyComponent;import type { FC, PropsWithChildren } from 'react';
import styles from './index.module.css';
type CardProps = PropsWithChildren<{
title: string;
variant?: 'default' | 'highlighted';
}>;
const Card: FC<CardProps> = ({ title, variant = 'default', children }) => (
<div className={`${styles.card} ${styles[variant]}`}>
<h3 className={styles.title}>{title}</h3>
<div className={styles.content}>{children}</div>
</div>
);
export default Card;Use CSS Modules for component styling:
/* index.module.css */
.container {
@apply flex
flex-col
gap-4
rounded-lg
bg-white
p-6
shadow-sm;
}
.title {
@apply text-xl
font-semibold
text-gray-900;
}
.description {
@apply leading-relaxed
text-gray-600;
}- Use camelCase for CSS class names
- Apply one Tailwind utility per line for readability
- Use
@applydirective for Tailwind utilities - Avoid plain CSS properties when Tailwind alternatives exist
- Keep styles scoped to the component
.container {
@apply grid
grid-cols-1
gap-4
md:grid-cols-2
lg:grid-cols-3;
}
.title {
@apply text-lg
md:text-xl
lg:text-2xl;
}// Prefix prop types with component name
type ButtonProps = {
variant: 'primary' | 'secondary' | 'danger';
size: 'small' | 'medium' | 'large';
isLoading?: boolean;
onClick?: () => void;
};
// Extend HTML attributes when appropriate
type InputProps = InputHTMLAttributes<HTMLInputElement> & {
label: string;
error?: string;
};
// Use PropsWithChildren for components that accept children
type ContainerProps = PropsWithChildren<{
maxWidth?: 'sm' | 'md' | 'lg' | 'xl';
className?: string;
}>;// Type-only imports
import type { FC, HTMLAttributes, MouseEvent } from 'react';
// Regular imports
import { useState, useEffect } from 'react';
import { Button } from '@node-core/ui-components';
// Avoid importing React itself
// ❌ import React from 'react';
// ✅ import type { FC } from 'react';Components should be organized by domain (where they're used):
- UI Components (
@node-core/ui-components)
When you need Next.js functionality in a UI component, use a wrapper, such as the one shown below:
// Base component (@node-core/ui-components)
const BaseNavLink: FC<BaseLinkProps> = ({ href, isActive, children }) => (
<a href={href} className={`${styles.link} ${isActive ? styles.active : ''}`}>
{children}
</a>
);
// Wrapper component (apps/site/components)
const NavLink: FC<NavLinkProps> = ({ href, children }) => {
const pathname = usePathname();
const isActive = pathname === href;
return (
<BaseNavLink href={href} isActive={isActive}>
{children}
</BaseNavLink>
);
};- Site Components (
apps/site/components)
// __tests__/index.test.mjs
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from '../index.js';
describe('Button Component', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
assert(isVisible(screen.getByRole('button', { name: /click me/i }));
});
it('applies variant styles correctly', () => {
render(<Button variant="secondary">Test</Button>);
const button = screen.getByRole('button');
assert(button.getAttribute('class').includes('secondary'))
});
});// index.stories.tsx
import type { Meta as MetaObj, StoryObj } from '@storybook/react';
import Button from '@node-core/ui-components/Common/Button';
type Story = StoryObj<typeof Button>;
type Meta = MetaObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary Button',
},
};
export const Loading: Story = {
args: {
variant: 'primary',
children: 'Loading...',
disabled: true,
},
};
export default {
component: Button,
title: 'Common/Button',
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
},
},
} as Meta;