# Component Design Standards for Shadcn
This document outlines the coding standards for component design when working with Shadcn. Following these guidelines will ensure that your components are reusable, maintainable, performant, and secure.
## 1. Component Structure and Organization
### 1.1. File and Folder Structure
**Do This:**
* Organize components into a clear, logical directory structure. A common pattern is "components//.tsx" for the main component file, and "components//index.ts" for exporting.
* Keep related files (styles, tests, stories) within the same component directory.
**Don't Do This:**
* Dump all components into a single "components" directory.
* Mix different types of files (components, styles, tests) within the same directory without clear organization.
**Why:** A well-structured file system improves discoverability, maintainability, and collaboration. It facilitates easier navigation and reduces the likelihood of naming conflicts.
**Example:**
"""
src/
└── components/
├── Button/
│ ├── Button.tsx
│ ├── Button.module.css
│ ├── Button.stories.tsx
│ ├── Button.test.tsx
│ └── index.ts
├── Card/
│ ├── Card.tsx
│ ├── CardHeader.tsx
│ ├── CardBody.tsx
│ ├── CardFooter.tsx
│ └── index.ts
└── index.ts // Exports from all components
"""
### 1.2. Single Responsibility Principle
**Do This:**
* Each component should have a single, well-defined purpose. If a component becomes too complex, refactor it into smaller, more focused components.
**Don't Do This:**
* Create "god components" that handle multiple unrelated responsibilities.
* Write components that are tightly coupled to specific use cases.
**Why:** The Single Responsibility Principle makes components easier to understand, test, and reuse. It reduces the impact of changes, as modifications to one component are less likely to affect others.
**Example:**
Instead of a single "UserProfile" component that fetches data, formats it, and displays it, create separate components for:
* "UserProfileData": Fetches the user data.
* "UserProfileFormatter": Formats the data (e.g., date formatting).
* "UserProfileDisplay": Displays the formatted data.
"""tsx
// UserProfileData.tsx
interface UserProfile {
name: string;
email: string;
createdAt: Date;
}
async function fetchUserProfile(userId: string): Promise {
// Simulate fetching data
return new Promise((resolve) => {
setTimeout(() => {
resolve({ name: 'John Doe', email: 'john.doe@example.com', createdAt: new Date() });
}, 500);
});
}
export function UserProfileData({ userId }: { userId: string }) {
const [profile, setProfile] = React.useState(null);
React.useEffect(() => {
fetchUserProfile(userId).then(setProfile);
}, [userId]);
if (!profile) {
return Loading...;
}
return ;
}
// UserProfileFormatter.tsx
import { format } from 'date-fns';
interface UserProfile {
name: string;
email: string;
createdAt: Date;
}
interface FormattedUserProfile {
name: string;
email: string;
createdAt: string;
}
function formatUserProfile(profile: UserProfile): FormattedUserProfile {
return {
name: profile.name,
email: profile.email,
createdAt: format(profile.createdAt, 'MMMM dd, yyyy'),
};
}
export function UserProfileFormatter({ profile }: { profile: UserProfile }) {
const formattedProfile = formatUserProfile(profile);
return ;
}
// UserProfileDisplay.tsx
interface UserProfile {
name: string;
email: string;
createdAt: string;
}
export function UserProfileDisplay({ profile }: { profile: UserProfile }) {
return (
{profile.name}
<p>Email: {profile.email}</p>
<p>Created At: {profile.createdAt}</p>
);
}
//Usage
"""
### 1.3. Controlled vs. Uncontrolled Components
**Do This:**
* Prefer controlled components when you need to manage the component's state from the parent.
* Use uncontrolled components when you want the component to manage its own state internally.
* Provide mechanisms for the parent to influence the uncontrolled component's state (e.g., through callbacks).
**Don't Do This:**
* Mix controlled and uncontrolled patterns within the same component without a clear reason.
* Overuse controlled components when uncontrolled components would be simpler and more appropriate.
**Why:** Properly choosing between controlled and uncontrolled components improves flexibility and maintainability. Controlled components provide fine-grained control, while uncontrolled components simplify common use cases.
**Example (Controlled Component):**
"""tsx
// ControlledInput.tsx
import React from 'react';
interface ControlledInputProps {
value: string;
onChange: (newValue: string) => void;
}
export function ControlledInput({ value, onChange }: ControlledInputProps) {
return (
onChange(e.target.value)}
/>
);
}
// Usage
function ParentComponent() {
const [inputValue, setInputValue] = React.useState('');
return (
setInputValue(newValue)}
/>
<p>Value: {inputValue}</p>
);
}
"""
**Example (Uncontrolled Component):**
"""tsx
// UncontrolledInput.tsx
import React from 'react';
interface UncontrolledInputProps {
defaultValue?: string;
onChange?: (newValue: string) => void;
}
export function UncontrolledInput({ defaultValue = "", onChange }: UncontrolledInputProps) {
const [value, setValue] = React.useState(defaultValue);
const handleChange = (e: React.ChangeEvent) => {
setValue(e.target.value);
onChange?.(e.target.value);
};
return ;
}
// Usage
function ParentComponent() {
const handleInputChange = (newValue: string) => {
console.log('Input changed:', newValue);
};
return (
);
}
"""
## 2. Styling and Theming with Shadcn
### 2.1. Utilizing "cn" Utility
**Do This:**
* Use the "cn" utility (from "tailwind-merge" and "clsx") to conditionally apply Tailwind CSS classes. "cn" has now become a part of most Shadcn UI packages so install with "npm install tailwind-merge clsx".
**Don't Do This:**
* Use string concatenation to build class names, as it's error-prone and less efficient.
* Manually handle class name conflicts.
**Why:** The "cn" utility simplifies class name management, ensures consistent styling, and improves performance by avoiding unnecessary re-renders. It also prevents class conflicts by merging Tailwind CSS classes intelligently.
**Example:**
"""tsx
import { cn } from "@/lib/utils" // or wherever you've placed your utils.ts
interface ButtonProps extends React.ButtonHTMLAttributes {
variant?: "primary" | "secondary";
size?: "small" | "medium" | "large";
}
export function Button({ variant = "primary", size = "medium", className, ...props }: ButtonProps) {
return (
{props.children}
);
}
"""
Here we're using the "cn" utlity to conditionally set multiple tailwind classes depending on which props the component is passed.
### 2.2. Theme Variables
**Do This:**
* Use CSS variables (custom properties) for theming. Define variables for colors, fonts, spacing, and other theme-related values.
* Organize theme variables in a dedicated file (e.g., "src/styles/theme.css" or "app/theme.ts")
* Shadcn provides a "theme.json" that can be customized: "npx shadcn-ui@latest init"
**Don't Do This:**
* Hardcode theme values directly in component styles.
* Use magic numbers for spacing or sizing.
**Why:** CSS variables allow for easy theme customization and consistency across the application. For example, using "themes.json" for component customizations ensures consistency in style. By defining variables, you can quickly update the look and feel of your application without modifying individual components.
**Example:**
"""css
/* src/styles/theme.css */
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--font-size-base: 16px;
--spacing-unit: 8px;
}
/* Component Style */
.my-component {
color: var(--primary-color);
font-size: var(--font-size-base);
padding: var(--spacing-unit) * 2;
}
"""
### 2.3. Component Variants with Tailwind CSS
**Do This:**
* Leverage Tailwind CSS's utility-first approach to create component variants.
* Use class composition to define different styles based on component props.
* Use the "cn" utility to organize the setting of styles of component varients.
**Don't Do This:**
* Create overly complex or deeply nested Tailwind CSS classes.
* Modify Tailwind CSS's core configuration unless absolutely necessary.
**Why:** Tailwind CSS allows for creating highly customizable components with ease. Through a utility-first methodology this cuts down on verbose custom CSS files.
**Example:**
"""tsx
import { cn } from "@/lib/utils"
type ButtonVariant = "primary" | "secondary" | "ghost"
type ButtonSize = "sm" | "md" | "lg"
interface ButtonProps extends React.ButtonHTMLAttributes {
variant?: ButtonVariant
size?: ButtonSize
children?: React.ReactNode
}
export function Button({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) {
return (
{children}
)
}
"""
## 3. Component Logic and State Management
### 3.1. Hooks for Reusable Logic
**Do This:**
* Extract complex logic into custom hooks to promote reuse and separation of concerns.
* Name hooks using the "use" prefix (e.g., "useFetchData", "useFormValidation").
* Ensure hooks are composable and can be used in multiple components.
**Don't Do This:**
* Embed complex logic directly within components, making them harder to understand and test.
* Create hooks that are tightly coupled to specific components or contexts.
**Why:** Hooks allow you to reuse stateful logic between components. They improve code organization, testability, and maintainability.
**Example:**
"""tsx
// useFetchData.ts
import { useState, useEffect } from 'react';
interface FetchResult {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetchData(url: string): FetchResult {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
const result: T = await response.json();
setData(result);
} catch (e: any) {
setError(e);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]); // URL is the dependancy. If the url changes, the effect will re-run
return { data, loading, error };
}
export default useFetchData;
// MyComponent.tsx
import useFetchData from './useFetchData';
interface DataItem {
id: number;
name: string;
}
function MyComponent() {
const { data, loading, error } = useFetchData('https://api.example.com/data');
if (loading) {
return Loading...;
}
if (error) {
return Error: {error.message};
}
return (
{data?.map(item => (
{item.name}
))}
);
}
"""
### 3.2. Context for Global State
**Do This:**
* Use React Context for managing global state that needs to be accessed by multiple components.
* Create provider components to wrap the application and make the state available to all descendants.
* Use "useContext" hook to consume the context value in the components.
**Don't Do This:**
* Overuse context for local component state.
* Mutate context values directly without using a state management solution (e.g., "useState" or "useReducer").
**Why:** Context provides a way to pass data through the component tree without having to pass props down manually at every level. This simplifies component composition and reduces prop drilling.
**Example:**
"""tsx
// ThemeContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextProps {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext({
theme: 'light', // default value
toggleTheme: () => {}, // no-op function
});
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
{children}
);
};
export const useTheme = (): ThemeContextProps => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};
// MyComponent.tsx
import { useTheme } from './ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useTheme();
return (
<p>Current theme: {theme}</p>
Toggle Theme
);
}
// App.tsx
import { ThemeProvider } from './ThemeContext';
import MyComponent from './MyComponent';
function App() {
return (
);
}
"""
### 3.3. State Management Libraries
**Do This:**
* Consider using state management libraries like Zustand, Jotai, or Redux for complex applications with advanced state requirements.
* Choose a library that aligns with your project needs and team expertise.
* Follow the library's recommended patterns and best practices.
**Don't Do This:**
* Introduce a state management library prematurely, before assessing the need for it.
* Overuse global state when component-level state would suffice.
**Why:** State management libraries provide structured ways to manage application state, especially when dealing with complex data flows, asynchronous actions, and shared state across multiple components.
### 3.4. Using Shadcn UI React Hooks
**Do This:**
* Properly utilize the available react hooks that are provided by Shadcn for forms and controlled components. Shadcn uses the "useForm" hook to manage form state, validation, and submission.
**Don't Do This:**
* Recreate or rewrite custom logic to handle form submissions and validation if a proper hook exists.
**Why:** Shadcn UI react hooks provide an out-of-the-box way to perform common and complex tasks that are often seen in modern web development. Using hooks from Shadcn promotes standardization across the project which in turn reduces debugging.
**Example**
"""tsx
import { useForm } from "react-hook-form"
type FormValues = {
username: string
}
function MyForm() {
const form = useForm({
defaultValues: {
username: "",
},
})
function onSubmit(values: FormValues) {
console.log(values)
}
return (
Submit
)
}
"""
## 4. Accessibility (A11y)
### 4.1. Semantic HTML
**Do This:**
* Use semantic HTML elements to provide structure and meaning to your components.
* Use appropriate ARIA attributes when semantic HTML is not sufficient.
**Don't Do This:**
* Use generic "div" and "span" elements excessively without providing semantic context.
* Rely solely on CSS to convey meaning or structure.
**Why:** Semantic HTML improves accessibility for users with disabilities and enhances SEO. It allows assistive technologies to understand the content and provide appropriate feedback.
**Example:**
"""tsx
// Good
Home
About
// Bad
Home
About
"""
### 4.2. ARIA Attributes
**Do This:**
* Use ARIA attributes to enhance the accessibility of custom components or when semantic HTML elements are not sufficient.
* Ensure that ARIA attributes are used correctly and do not conflict with native HTML semantics.
**Don't Do This:**
* Use ARIA attributes unnecessarily or incorrectly.
* Override native HTML semantics with ARIA attributes unless absolutely necessary.
**Why:** ARIA attributes provide additional information to assistive technologies, allowing them to understand the role, state, and properties of components.
**Example:**
"""tsx
// Custom Button
× {/* Times Symbol */}
"""
### 4.3. Keyboard Navigation
**Do This:**
* Ensure that all interactive elements are accessible via keyboard navigation.
* Use "tabIndex" attribute to control the focus order of elements.
* Provide visual focus indicators for keyboard users.
**Don't Do This:**
* Create components that can only be interacted with using a mouse.
* Remove the default focus outline without providing an alternative visual indicator.
**Why:** Keyboard navigation is essential for users who cannot use a mouse or other pointing device.
**Example:**
"""tsx
Click me
"""
### 4.4. Color Contrast
**Do This:**
* Ensure sufficient color contrast between text and background colors.
* Use a color contrast checker tool to verify that your color combinations meet accessibility standards (WCAG).
**Don't Do This:**
* Use color combinations that are difficult to read or cause eye strain.
* Rely solely on color to convey important information.
**Why:** Adequate color contrast is crucial for users with low vision or color blindness.
## 5. Testing
### 5.1. Unit Tests
**Do This:**
* Write unit tests for individual components to verify that they function as expected.
* Use testing libraries like Jest and React Testing Library.
* Aim for high test coverage to ensure that all critical code paths are tested.
**Don't Do This:**
* Skip unit tests for components with complex logic.
* Write tests that are too tightly coupled to the implementation details of components.
**Why:** Unit tests provide confidence that your components are working correctly and help prevent regressions when making changes.
**Example:**
"""tsx
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
test('renders button with correct text', () => {
render(Click me);
const buttonElement = screen.getByText('Click me');
expect(buttonElement).toBeInTheDocument();
});
test('calls onClick handler when clicked', () => {
const onClick = jest.fn();
render(Click me);
const buttonElement = screen.getByText('Click me');
fireEvent.click(buttonElement);
expect(onClick).toHaveBeenCalledTimes(1);
});
"""
### 5.2. Integration Tests
**Do This:**
* Write integration tests to verify that components work together correctly.
* Simulate user interactions and verify that the application behaves as expected.
**Don't Do This:**
* Skip integration tests for complex components.
* Rely solely on unit tests without verifying the integration of components.
**Why:** Integration tests ensure that different parts of the application work together seamlessly.
### 5.3. End-to-End (E2E) Tests
**Do This:**
* Write E2E tests to verify the end-to-end functionality of the application.
* Use tools like Cypress or Playwright to automate browser interactions.
* Focus on testing critical user flows and scenarios.
**Don't Do This:**
* Skip E2E tests for important features.
* Write E2E tests that are too brittle or flaky.
**Why:** E2E tests provide confidence that the application is working correctly from the user's perspective.
## 6. Performance Optimization
### 6.1. Memoization
**Do This:**
* Use "React.memo" to prevent unnecessary re-renders of components.
* Ensure that the props passed to memoized components are memoized as well.
* Use "useMemo" and "useCallback" hooks to memoize computed values and callback functions.
* If using Shadcn, many components are already "memoized" but it's important to understand in depth when creating your own.
**Don't Do This:**
* Overuse memoization, as it can introduce unnecessary complexity.
* Memoize components that always receive new props.
**Why:** Memoization can significantly improve performance by preventing unnecessary re-renders of components.
**Example:**
"""tsx
import React, { memo } from 'react';
interface MyComponentProps {
data: any;
onClick: () => void;
}
const MyComponent = ({ data, onClick }: MyComponentProps) => {
console.log('MyComponent rendered');
return (
{data.name}
);
};
export default memo(MyComponent);
"""
### 6.2. Code Splitting
**Do This:**
* Use dynamic imports to split the application code into smaller chunks.
* Load code chunks on demand to reduce the initial load time.
* Consider using React.lazy and Suspense for code splitting.
**Don't Do This:**
* Load all the application code upfront.
* Create too many small code chunks, as it can increase network overhead.
**Why:** Code splitting reduces the initial load time and improves the perceived performance of the application.
**Example:**
"""tsx
import React, { lazy, Suspense } from 'react';
const MyComponent = lazy(() => import('./MyComponent'));
function App() {
return (
Loading...}>
);
}
"""
### 6.3. Image Optimization
**Do This:**
* Optimize images by compressing them and using appropriate file formats (e.g., WebP).
* Use responsive images to serve different image sizes based on the user's device.
* Lazy load images that are not immediately visible.
**Don't Do This:**
* Use large, unoptimized images.
* Load all images upfront, even those that are not visible.
**Why:** Image optimization reduces the page size and improves the loading speed.
## 7. Security
### 7.1. Input Validation
**Do This:**
* Validate all user inputs to prevent security vulnerabilities such as Cross-Site Scripting (XSS) and SQL Injection.
* Use server-side validation in addition to client-side validation.
* Sanitize user inputs to remove or encode potentially harmful characters.
**Don't Do This:**
* Trust user inputs without validation.
* Rely solely on client-side validation for security.
**Why:** Input validation prevents attackers from injecting malicious code or data into the application.
### 7.2. Dependency Management
**Do This:**
* Keep dependencies up to date to patch security vulnerabilities.
* Use a dependency management tool (e.g., npm or yarn) to manage dependencies.
* Regularly audit dependencies for known vulnerabilities.
**Don't Do This:**
* Use outdated dependencies with known security vulnerabilities.
* Ignore security warnings from dependency management tools.
**Why:** Dependency management ensures that the application is using secure versions of its dependencies. Shadcn releases updates frequently and it's important to follow.
### 7.3. Secure API Communication
**Do This:**
* Use HTTPS to encrypt communication between the client and the server.
* Implement proper authentication and authorization mechanisms to protect API endpoints.
* Use JSON Web Tokens (JWT) for authentication.
**Don't Do This:**
* Use HTTP for sensitive data transmission.
* Expose API keys or secrets in client-side code.
**Why:** Secure API communication protects sensitive data from being intercepted or tampered with.
### 7.4. Sanitize HTML
**Do This:**
* Sanitize HTML for any user inputted components.
* Before rendering any user-supplied HTML content, sanitize it with a library like DOMPurify to prevent XSS
* Use a strict Content Security Policy (CSP) to mitigate XSS attacks.
- Set appropriate CSP headers on your server to control the sources from which the browser is allowed to load resources
**Don't Do This:**
* Render unsanitized user input directly to the DOM, which can execute malicious scripts
* Allow inline scripts or styles in your CSP, as this is a common attack vector
**Why:** Even the simple act of rendering a string that a user entered can lead to XSS attacks. It's important to take precautionary measures to ensure this doesn't occur.
**Example:**
"""tsx
import DOMPurify from 'dompurify';
function MyComponent({ userInput }: { userInput: string }) {
const sanitizedHTML = DOMPurify.sanitize(userInput);
return ;
}
"""
This document provides guidelines for writing maintainable, performant, and secure Shadcn components. Following these practices will improve the overall quality of your codebase and facilitate collaboration within your development team.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Code Style and Conventions Standards for Shadcn This document outlines the code style and conventions to be followed when developing with Shadcn. Adhering to these standards ensures consistency, readability, maintainability, and performance across the entire codebase. These guidelines are designed to be used by developers and as context for AI coding assistants, promoting a unified approach to Shadcn development. ## 1. Formatting and General Style ### 1.1. Code Formatting * **Standard:** Use a consistent code formatter like Prettier. Configure it to enforce consistent indentation (2 spaces), line length (120 characters), and trailing commas. * **Why:** Consistent formatting improves readability and reduces unnecessary changes in diffs. * **Do This:** """javascript // .prettierrc.js module.exports = { semi: true, trailingComma: 'all', singleQuote: true, printWidth: 120, tabWidth: 2, useTabs: false, }; """ * **Don't Do This:** Manually format code inconsistently. Relying solely on developer preference leads to a cluttered codebase. * **Standard:** Integrate Prettier with ESLint (or similar linter) to automatically fix formatting issues. * **Why:** Automating formatting reduces cognitive load and ensures compliance. * **Do This:** """bash npm install --save-dev prettier eslint-plugin-prettier eslint-config-prettier """ """javascript // .eslintrc.js module.exports = { extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'prettier', ], plugins: ['react', '@typescript-eslint', 'prettier'], rules: { 'prettier/prettier': 'error', // Add other ESLint rules here }, }; """ * **Don't Do This:** Skip linter integration. This forces developers to manually address style issues during code reviews. ### 1.2. File Structure * **Standard:** Organize components into directories based on functionality (e.g., "components/ui", "components/features"). Use the "components" directory as the root for all Shadcn UI components and custom components. * **Why:** Clear file structure enhances navigation and code discoverability. * **Do This:** """ src/ ├── components/ │ ├── ui/ │ │ ├── button.tsx │ │ ├── card.tsx │ │ └── ... │ ├── features/ │ │ ├── product-list.tsx │ │ ├── checkout-form.tsx │ │ └── ... │ └── layout/ │ ├── header.tsx │ └── footer.tsx ├── app/ │ ├── page.tsx │ └── ... └── lib/ ├── utils.ts └── ... """ * **Don't Do This:** Dump all components into a single directory, making it harder to find and manage. * **Standard:** Favor colocating related files (component, styles, tests) within the same directory. Also include stories for Storybook if using it. * **Why:** Easier to find and maintain related code. * **Do This:** """ src/components/ui/button/ ├── button.tsx ├── button.module.css //Or tailwind classes in the component ├── button.test.tsx └── button.stories.tsx """ * **Don't Do This:** Scatter related files across the project. ### 1.3. Imports and Exports * **Standard:** Use absolute imports for internal modules, with "@" as a root alias (configured in "tsconfig.json"). Use relative imports only for files within the same component directory. * **Why:** Prevents brittle import paths and improves code refactorability. * **Do This:** """typescript // tsconfig.json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } } """ """typescript // Correct Usage (absolute import) import { Button } from '@/components/ui/button'; // Correct Usage (relative import, within the same component directory) import styles from './button.module.css'; """ * **Don't Do This:** """typescript // Avoid: Relative imports from distant directories import { Button } from '../../../../components/ui/button'; """ * **Standard:** Group imports by origin (third-party libraries, internal modules, relative paths). Separate each group with a blank line. Order imports alphabetically within each group. * **Why:** Improves readability and makes it easier to identify dependencies. * **Do This:** """typescript import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { cn } from '@/lib/utils'; import { Input } from '@/components/ui/input'; import styles from './form.module.css'; """ * **Don't Do This:** Mix imports randomly; it obfuscates dependencies. ### 1.4. Comments and Documentation * **Standard:** Write clear, concise comments to explain complex logic, algorithms, or non-obvious code sections. Focus on *why* the code is written, not *what* it does. * **Why:** Clear documentation helps other developers (including your future self) understand the codebase. * **Do This:** """typescript /** * Calculates the total price of items in the cart, * applying any applicable discounts. * * @param {OrderItem[]} cartItems - An array of items in the cart. * @returns {number} The total price after discounts. */ function calculateTotalPrice(cartItems: OrderItem[]): number { // Apply a 10% discount for orders over $100. const basePrice = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0); const discount = basePrice > 100 ? basePrice * 0.1 : 0; return basePrice - discount; } """ * **Don't Do This:** Over-comment obvious code, skip commenting complex parts. * **Standard:** Use JSDoc or TSDoc style comments for documenting components and functions. This allows tools like Storybook and documentation generators to automatically create API documentation. * **Why:** Enables automated API documentation and improves discoverability. * **Do This:** """typescript /** * A styled button component. * * @param {ButtonProps} props - The props for the button. * @returns {JSX.Element} A button element. */ export function Button({ children, ...props }: ButtonProps): JSX.Element { return ( <button {...props} className={cn("button", props.className)}> {children} </button> ); } """ * **Standard:** Keep documentation up-to-date with code changes. Outdated documentation is worse than no documentation. * **Why:** Ensures accuracy and relevance of documentation over time. ## 2. Naming Conventions ### 2.1. Variables and Constants * **Standard:** Use descriptive, camelCase names for variables and functions (e.g., "userName", "calculateTotalPrice"). * **Why:** Improves readability and clarity. * **Do This:** """typescript const userEmail = 'test@example.com'; function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) { // ... } """ * **Don't Do This:** Use short, cryptic names like "x", "y", or single-letter variable names. * **Standard:** Use SCREAMING_SNAKE_CASE for constants (e.g., "MAX_ITEMS", "API_URL"). * **Why:** Clearly distinguishes constants from variables. * **Do This:** """typescript const API_URL = 'https://api.example.com'; const MAX_RETRIES = 3; """ * **Don't Do This:** Use camelCase for constants. ### 2.2. Components * **Standard:** Use PascalCase for component names (e.g., "UserProfile", "ProductCard"). * **Why:** Consistent with React conventions and makes components easily identifiable. * **Do This:** """typescript function ProductCard(props: ProductCardProps): JSX.Element { // ... } """ * **Don't Do This:** Use camelCase or snake_case; it violates React component naming standards. * **Standard:** When creating UI components using Shadcn UI, stick to names related to the function it provides. Try to be as semantic as possible. Example : "LoginButton", "SideBarNavigation", "ProductCardImage" * **Standard:** Name component files the same as the component (e.g., "ProductCard.tsx" for the "ProductCard" component). * **Why:** Simplifies file lookup and improves project structure. * **Do This:** A "ProductCard" component should be placed in "ProductCard.tsx" file. * **Don't Do This:** Name files arbitrarily or inconsistently. ### 2.3. Hooks * **Standard:** Use "use" prefix for custom React hooks (e.g., "useUserData", "useFetch"). * **Why:** Follows React convention, making it clear that a function is a hook. * **Do This:** """typescript function useUserData(userId: string) { // ... } """ * **Don't Do This:** Omit the "use" prefix; it violates Hook naming standards set by React. ### 2.4. CSS Classes * **Standard:** Use a consistent naming convention for CSS classes, preferably BEM (Block, Element, Modifier) or a similar methodology. If using Tailwind CSS, leverage its utility-first approach directly in the component. * **Why:** Improves CSS maintainability and reduces naming conflicts. * **Do This (BEM):** """css .product-card { /* Block styles */ } .product-card__title { /* Element styles */ } .product-card--featured { /* Modifier styles */ } """ * **Do This (Tailwind CSS):** """tsx <div className="bg-white rounded-lg shadow-md p-4"> <h2 className="text-xl font-bold mb-2">Product Title</h2> <p className="text-gray-700">Product description...</p> </div> """ * **Don't Do This:** Use vague or generic class names without any methodology (e.g., "red", "small", "box"). ### 2.5 Type Names * **Standard:** Use PascalCase and the "Type" or "Interface" suffix when declare types for components like "ButtonType", "CardInterface". * **Why:** Improve readability of the code. * **Do This:** """typescript interface ButtonInterface{ variant: "default"|"destructive", size: "sm" | "lg", name: string } """ ## 3. Shadcn-Specific Conventions ### 3.1. Component Composition * **Standard:** Leverage Shadcn UI's composable components to build complex UIs. Avoid creating new low-level components if possible. * **Why:** Promotes consistency and reusability, and reduces code duplication. * **Do This:** """typescript import { Button } from '@/components/ui/button'; import { Card, CardHeader, CardBody } from '@/components/ui/card'; function UserProfileCard() { return ( <Card> <CardHeader>User Profile</CardHeader> <CardBody> <p>Name: John Doe</p> <Button>Edit Profile</Button> </CardBody> </Card> ); } """ * **Don't Do This:** Rebuild basic UI elements from scratch when Shadcn UI already provides them. ### 3.2. Customization with "cn" Utility * **Standard:** Use the "cn" utility (from "class-variance-authority") to easily combine Tailwind CSS classes and handle conditional class names. * **Why:** Simplifies class name management and improves readability. * **Do This:** """typescript import { cn } from '@/lib/utils'; interface ButtonProps { variant?: 'primary' | 'secondary'; size?: 'small' | 'large'; className?: string; children: React.ReactNode; } function Button({ variant = 'primary', size = 'small', className, children }: ButtonProps) { return ( <button className={cn( 'rounded-md font-semibold', { 'bg-blue-500 text-white': variant === 'primary', 'bg-gray-200 text-gray-700': variant === 'secondary', 'px-2 py-1 text-sm': size === 'small', 'px-4 py-2 text-base': size === 'large', }, className )} > {children} </button> ); } """ * **Don't Do This:** Manually concatenate class names with string interpolation; it becomes unwieldy and error-prone. ### 3.3. Overriding Styles * **Standard:** Use CSS variables or Tailwind CSS's customization features to override the default styles of Shadcn UI components. * **Why:** Preserves consistency and makes it easier to maintain a consistent design system. * **Do This (CSS Variables):** """css :root { --button-primary-bg: #007bff; } .button.primary { background-color: var(--button-primary-bg); } """ * **Do This (Tailwind CSS Customization):** In "tailwind.config.js": """javascript module.exports = { theme: { extend: { colors: { primary: '#007bff', secondary: '#6c757d', }, }, }, }; """ Then, in the component: """tsx <button className="bg-primary text-white font-bold py-2 px-4 rounded"> Click me </button> """ * **Don't Do This:** Use inline styles or overly-specific CSS selectors to override styles, as it reduces maintainability and increases the risk of conflicts. ### 3.4 Data Fetching and State Management * **Standard:** When fetching data for components, prefer using React Query or SWR. * **Why:** Handle data fetching and caching with a performatic way. * **Do This:** """tsx import { useQuery } from "@tanstack/react-query"; interface Props { id: string } function UserProfile({id}:Props) { const{isLoading, data:profile} = useQuery({ queryKey: ['profile',id], queryFn: ()=>fetch('/user/'+id).then(res=>res.json()) }) if(isLoading){ return <div>Loading...</div> } return <div>{profile?.name}</div> } """ ## 4. Modern Approaches and Patterns ### 4.1. Functional Components and Hooks * **Standard:** Prefer functional components and hooks over class components for new code. * **Why:** Functional components are simpler, more readable, and easier to test. Hooks promote code reuse and separation of concerns. * **Do This:** """typescript import { useState, useEffect } from 'react'; function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = "Count: ${count}"; }, [count]); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ); } """ * **Don't Do This:** Use class components for new feature development without a specific reason. ### 4.2. TypeScript * **Standard:** Use TypeScript for all new code. Annotate components, functions, and variables with types. * **Why:** TypeScript improves code quality, reduces runtime errors, and enhances maintainability. * **Do This:** """typescript interface User { id: number; name: string; email: string; } function greetUser(user: User): string { return "Hello, ${user.name}!"; } """ * **Don't Do This:** Skip type annotations; it negates the benefits of TypeScript. ### 4.3 Utility Functions * **Standard:** Store utility functions in "lib/utils.ts" or similar dedicated files. * **Why:** Allow use the same function across all the project. * **Do This:** """typescript // lib/utils.ts import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } """ ## 5. Anti-Patterns to Avoid ### 5.1. Over-Engineering * **Anti-Pattern:** Creating overly complex solutions for simple problems. Avoid premature optimization or unnecessary abstraction. * **Why:** Increases code complexity and reduces maintainability. ### 5.2. Prop Drilling * **Anti-Pattern:** Passing props through multiple layers of components without being directly used. * **Why:** Makes code harder to understand and refactor. * **Solution:** Use context or state management libraries like Zustand or Redux to share data between components. ### 5.3. Mutating Props Directly * **Anti-Pattern:** Modifying props directly within a component. * **Why:** Violates React's unidirectional data flow and can lead to unpredictable behavior. * **Solution:** Use state to manage component-specific data and pass down event handlers to update the parent's state. ### 5.4. Inefficient Rendering * **Anti-Pattern:** Causing unnecessary re-renders by not memoizing components or using inefficient data structures. * **Why:** Impacts performance, especially in complex UIs. * **Solution:** Use "React.memo", "useMemo", and "useCallback" to optimize rendering. ### 5.5. Ignoring Accessibility * **Anti-Pattern:** Neglecting accessibility considerations (e.g., proper ARIA attributes, semantic HTML) when building UI components. * **Why:** Excludes users with disabilities and violates accessibility standards (WCAG). * **Solution:** Use semantic HTML, provide ARIA attributes where necessary, and test with assistive technologies. ## 6. Security Best Practices ### 6.1. Input Validation * **Standard:** Always validate user inputs on both the client and server sides. * **Why:** Prevents security vulnerabilities such as XSS and SQL injection. * **Do This:** """typescript // Client-side validation function validateEmail(email: string) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } // Server-side validation (using a library like Zod) import { z } from 'zod'; const userSchema = z.object({ email: z.string().email(), password: z.string().min(8), }); function createUser(data: any) { const validatedData = userSchema.parse(data); // ... } """ ### 6.2. Secure Authentication * **Standard:** Use industry-standard authentication and authorization mechanisms such as OAuth 2.0 and JWT. * **Why:** Protects user accounts and data. ### 6.3. Data Sanitization * **Standard:** Sanitize data before rendering it in the UI to prevent XSS attacks. * **Why:** Prevents malicious scripts from executing in the user's browser. * **Do This:** """typescript function escapeHtml(unsafe: string) { return unsafe .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function renderUserInput(userInput: string) { return <div>{escapeHtml(userInput)}</div>; } """ ### 6.4 Dependency Management * **Standard:** Regularly update dependencies to patch security vulnerabilities. * **Why:** Mitigates risks associated with outdated libraries. * **Do This:** Use "npm audit" or "yarn audit" to identify and fix vulnerabilities. ## 7. Testing ### 7.1. Unit Testing * **Standard:** Write unit tests for individual components and utility functions. * **Why:** Ensures code correctness and facilitates refactoring. * **Do This:** """typescript // button.test.tsx import { render, screen } from '@testing-library/react'; import { Button } from './button'; test('renders button with correct text', () => { render(<Button>Click me</Button>); const buttonElement = screen.getByText('Click me'); expect(buttonElement).toBeInTheDocument(); }); """ ### 7.2. Integration Testing * **Standard:** Write integration tests to verify the interaction between components. * **Why:** Ensures that components work together correctly. ### 7.3. End-to-End Testing * **Standard:** Use end-to-end testing frameworks to simulate user interactions and verify the overall application functionality. * **Why:** Validates the entire user flow and catches integration issues. * **Tools:** Cypress, Playwright ## 8. Performance Optimization ### 8.1. Code Splitting * **Standard:** Implement code splitting to reduce the initial load time by loading only the necessary code. * **Why:** Improves application performance, resulting in better user experience, especially with large applications. * **Do This:** * Lazy load with "React.lazy" and "<Suspense>". * Dynamic import statements. ### 8.2. Memoization * **Standard:** Use "React.memo" for functional components that receive the same props repeatedly. Use also "useMemo" and "useCallback" hooks if needed. * **Why:** Avoids unnecessary re-renders. ### 8.3. Image Optimization * **Standard:** Optimize all images by compressing them, using appropriate formats (WebP), and lazy loading them. * **Why:** Reduces bandwidth consumption and improves page load times. * **Do This:** Use the "<Image>" component provided by Next.js. By following these code style and conventions, development teams can create high-quality, maintainable, and performant Shadcn applications. This guide can also be used to configure AI coding assistants for automatic code formatting and style enforcement.
# Core Architecture Standards for Shadcn This document outlines the core architectural standards for building maintainable, performant, and scalable applications using Shadcn. It provides guidelines for project structure, component organization, data fetching, state management, and other architectural aspects specific to Shadcn. ## 1. Project Structure and Organization A well-defined project structure is crucial for maintainability, collaboration, and scalability. Shadcn, being a UI library that's typically integrated into larger frameworks like Next.js or Remix, benefits significantly from a thoughtfully structured project. ### 1.1. Directory Structure Organize your project using a feature-sliced architecture, grouped by domain and purpose. This promotes modularity and separation of concerns. **Do This:** """ . ├── app/ # Next.js app directory (or equivalent in your framework) │ ├── components/ # Shared, reusable components │ │ ├── ui/ # Shadcn UI components - button, card, etc. │ │ ├── domain/ # Components specific to a business domain (e.g., user profile, product list) │ │ └── utils.ts # Utility functions related to components │ ├── lib/ # Core application logic │ │ ├── utils.ts # General utility functions │ │ ├── api/ # API client and related logic │ │ ├── hooks/ # Custom React hooks │ │ └── services/ # Business logic services │ ├── styles/ # Global styles and theme related files │ │ ├── globals.css # Global CSS file │ │ └── theme.ts # Theme configuration file (using Radix UI themes or similar) │ └── ... ├── public/ # Static assets ├── .env.local # Environment variables ├── tsconfig.json # TypeScript configuration └── ... """ **Don't Do This:** * Flat directory structures * Mixing UI components with business logic within the same directory * Global mutable state **Why This Matters:** * **Maintainability:** Feature-sliced architecture allows for easy modification and extension of specific features without affecting other parts of the application. * **Scalability:** Modular design makes it easier to add new features and scale the application. * **Collaboration:** Clear directory structure improves team collaboration and reduces merge conflicts. ### 1.2. Component Organization Organize components into logical modules based on their functionality and reusability. **Do This:** """ components/ ├── ui/ │ ├── button.tsx │ ├── card.tsx │ └── ... ├── domain/ │ ├── user-profile/ │ │ ├── user-profile.tsx │ │ ├── user-profile.module.css │ │ └── ... │ └── product-list/ │ ├── product-list.tsx │ ├── product-list-item.tsx │ └── ... """ **Don't Do This:** * Large, monolithic components. * Duplicating similar UI logic across multiple components. * Directly embedding complex business logic into UI components. **Why This Matters:** * **Reusability:** Separates UI components from business logic, allowing for easy reuse across different parts of the application. * **Testability:** Smaller, well-defined components are easier to test. * **Readability:** Improves code readability and maintainability. ### 1.3. Shadcn UI Integration When integrating Shadcn UI components, treat them as foundational building blocks and extend or compose them to create higher-level UI elements. **Do This:** """tsx // components/ui/button.tsx (From Shadcn UI) import * as React from "react"; import { cn } from "@/lib/utils"; import { ButtonHTMLAttributes, forwardRef } from "react"; import { VariantProps, cva } from "class-variance-authority"; const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "bg-transparent border border-input hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "bg-transparent underline-offset-4 hover:underline text-primary", }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, } ); export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {} const Button = forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant, size, ...props }, ref) => { return ( <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> ); } ); Button.displayName = "Button"; export { Button, buttonVariants }; // components/domain/user-profile/edit-profile-button.tsx import { Button } from "@/components/ui/button"; export function EditProfileButton() { return ( <Button variant="outline" size="sm"> Edit Profile </Button> ); } """ **Don't Do This:** * Modifying Shadcn UI components directly. Instead, compose them or create wrapper components. * Ignoring the theme and styles provided by Shadcn UI. **Why This Matters:** * **Maintainability:** Upgrading Shadcn UI components becomes easier without conflicts. * **Consistency:** Maintains a consistent look and feel throughout the application. * **Customization:** Allows for easy customization of UI elements while leveraging the base styles and functionalities of Shadcn UI. ## 2. Data Fetching and State Management Efficient data fetching and state management are crucial for application performance and user experience. ### 2.1. Data Fetching Strategies Employ modern data fetching techniques like Server Components or React Server Actions (in Next.js) for optimal initial load performance. Use client-side fetching (e.g., "SWR" or "React Query") only when strictly necessary for interactive updates or mutations. **Do This:** """tsx // app/profile/page.tsx (Next.js Server Component) import { getUserProfile } from "@/lib/api/user"; export default async function ProfilePage() { const user = await getUserProfile(); return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ); } // lib/api/user.ts export async function getUserProfile() { // Replace with your actual data fetching logic const res = await fetch('https://api.example.com/user/profile', { headers: { 'Authorization': "Bearer ${process.env.API_TOKEN}" } }); if (!res.ok) { throw new Error('Failed to fetch data') } return res.json() } // app/actions.ts (React Server Action) 'use server' import { revalidatePath } from 'next/cache'; export async function updateProfile(formData: FormData) { // ... Server-side mutation logic // Assuming you have a "updateUserProfile" function in api/user.ts await updateUserProfile(formData) revalidatePath('/profile') // Revalidate the profile page } """ """tsx // Client-Side Fetching Example (using SWR) import useSWR from 'swr' const fetcher = (...args: [string, ...any[]]) => fetch(...args).then(res => res.json()) function Profile() { const { data, error, isLoading } = useSWR('/api/user', fetcher) if (error) return <div>failed to load</div> if (isLoading) return <div>loading...</div> return ( <div> <h1>{data.name}</h1> <p>{data.email}</p> </div> ) } export default Profile """ **Don't Do This:** * Fetching data directly in client-side components for initial page load, if server-side fetching is possible. * Over-fetching data. * Ignoring caching mechanisms for frequently accessed data. **Why This Matters:** * **Performance:** Server-side rendering and static site generation provide faster initial load times. * **SEO:** Server-rendered content is easily crawled by search engines. * **User Experience:** Reduces perceived latency and improves overall app responsiveness. ### 2.2. State Management Choose a state management solution that aligns with the complexity of your application. For simple component-level state, use "useState" and "useReducer". For more complex application-wide state, consider using Context API with a reducer, or a dedicated state management library like Zustand or Jotai. Avoid Redux unless the application has extremely complex state requirements. **Do This:** """tsx // Simple component state (useState) import { useState } from "react"; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } // Context API with useReducer (application-wide state) import React, { createContext, useReducer, useContext } from 'react'; type State = { theme: 'light' | 'dark'; }; type Action = | { type: 'SET_THEME'; payload: 'light' | 'dark' }; const initialState: State = { theme: 'light', }; const ThemeContext = createContext<{ state: State; dispatch: React.Dispatch<Action>; }>({ state: initialState, dispatch: () => null, }); const themeReducer = (state: State, action: Action): State => { switch (action.type) { case 'SET_THEME': return { ...state, theme: action.payload }; default: return state; } }; const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [state, dispatch] = useReducer(themeReducer, initialState); return ( <ThemeContext.Provider value={{ state, dispatch }}> {children} </ThemeContext.Provider> ); }; const useTheme = () => { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within a ThemeProvider'); } return context; }; export { ThemeProvider, useTheme }; """ """tsx // Zustand example import { create } from 'zustand' interface BearState { bears: number increase: (by: number) => void } const useBearStore = create<BearState>()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) function BearCounter() { const bears = useBearStore((state) => state.bears) const increase = useBearStore((state) => state.increase) return ( <> <h1>{bears} Bears in the store</h1> <button onClick={() => increase(2)}>Increase bears</button> </> ) } """ **Don't Do This:** * Overusing global state for local component state. * Mutating state directly. * Using Redux for simple state management scenarios. **Why This Matters:** * **Predictability:** Makes state changes predictable and easier to debug. * **Performance:** Prevents unnecessary re-renders by only updating components that depend on the changed state. * **Maintainability:** Centralized state management improves codebase organization and makes it easier to reason about application state. ### 2.3. Optimistic Updates When performing mutations, consider using optimistic updates to provide immediate feedback to the user while the request is processing in the background. **Do This:** """tsx // Example using SWR's mutate function for optimistic updates import useSWR, { mutate } from 'swr' function LikeButton({ postId }) { const { data: post } = useSWR("/api/posts/${postId}") const handleLike = async () => { // Optimistically update the UI mutate("/api/posts/${postId}", { ...post, likes: post.likes + 1 }, { revalidate: false }) try { // Send the like request to the server await fetch("/api/posts/${postId}/like", { method: 'POST' }) // Revalidate the data to ensure it's up-to-date mutate("/api/posts/${postId}") } catch (error) { // If the request fails, revert the optimistic update mutate("/api/posts/${postId}", post, { revalidate: false }) console.error('Failed to like post', error) // Optionally, show an error message to the user } } return ( <button onClick={handleLike} disabled={isLoading}> Like ({post?.likes || 0}) </button> ) } """ **Don't Do This:** * Ignoring potential errors during optimistic updates and failing to revert the UI. * Optimistically updating state that depends on server-side validation. **Why This Matters:** * **User Experience:** Provides a more responsive and engaging user experience. * **Perceived Performance:** Makes the application feel faster. ## 3. Code Quality and Best Practices Maintaining high code quality is essential for long-term maintainability, collaboration, and reducing the risk of bugs. ### 3.1. TypeScript Usage Embrace TypeScript for type safety and improved code maintainability. **Do This:** * Use explicit types for function parameters, return values, and variables. * Define custom types and interfaces to represent data structures. * Enable strict mode in "tsconfig.json" for stricter type checking. * Leverage TypeScript's features like generics, enums, and discriminated unions to write more robust and reusable code. """tsx interface User { id: string; name: string; email: string; } async function fetchUser(id: string): Promise<User> { const response = await fetch("/api/users/${id}"); const user: User = await response.json(); return user; } """ **Don't Do This:** * Using "any" type excessively. * Ignoring TypeScript errors and warnings. * Writing code that bypasses type checking. **Why This Matters:** * **Type Safety:** Prevents runtime errors by catching type-related issues during development. * **Maintainability:** Makes code easier to understand, refactor, and maintain. * **Collaboration:** Improves team collaboration by providing clear and consistent type information. ### 3.2. Code Formatting and Linting Use a code formatter (e.g., Prettier) and linter (e.g., ESLint) to enforce consistent code style and identify potential issues. **Do This:** * Configure Prettier and ESLint with opinionated rules for code formatting and linting. * Integrate Prettier and ESLint into your development workflow (e.g., using VS Code extensions or pre-commit hooks). * Follow the configured rules consistently. **Don't Do This:** * Ignoring formatting and linting errors. * Disabling important linting rules. * Committing code that doesn't adhere to the established code style. **Why This Matters:** * **Consistency:** Ensures a consistent code style throughout the project. * **Readability:** Improves code readability and maintainability. * **Error Prevention:** Identifies potential issues and helps prevent bugs. ### 3.3. Error Handling Implement robust error handling to gracefully handle unexpected errors and prevent application crashes. **Do This:** * Use "try...catch" blocks to catch potential errors in asynchronous operations. * Provide informative error messages to the user. * Log errors to a centralized logging service for debugging and monitoring. * Use error boundaries to prevent errors in one component from crashing the entire application. """tsx async function fetchData() { try { const response = await fetch('/api/data'); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } const data = await response.json(); return data; } catch (error) { console.error("Failed to fetch data:", error); // Optionally, display an error message to the user return null; } } """ **Don't Do This:** * Ignoring errors or suppressing them silently. * Displaying technical error messages to the user. * Allowing errors to crash the application. **Why This Matters:** * **Stability:** Prevents application crashes and ensures a more stable user experience. * **Debuggability:** Makes it easier to identify and fix errors. * **User Experience:** Provides a better user experience by gracefully handling errors and providing informative messages. ### 3.4. Accessibility (A11y) Prioritize accessibility to ensure that your application is usable by everyone, including users with disabilities. **Do This:** * Use semantic HTML elements. * Provide alternative text for images. * Ensure sufficient color contrast. * Make sure all interactive elements are keyboard accessible. * Use ARIA attributes to provide additional context to assistive technologies. **Don't Do This:** * Ignoring accessibility guidelines. * Using non-semantic HTML elements for structure. * Creating keyboard traps. **Why This Matters:** * **Inclusivity:** Makes the application usable by a wider audience. * **Ethical Responsibility:** It's the right thing to do. * **SEO:** Accessible websites tend to rank higher in search results. * **Legal Compliance:** Many regions have laws mandating accessibility. ### 3.5. Comments and Documentation Write clear and concise comments to explain complex logic, document components, and provide context. Use JSDoc or TypeScript-style documentation comments to generate API documentation. **Do This:** """tsx /** * Fetches user data from the API. * * @param {string} id - The ID of the user to fetch. * @returns {Promise<User>} The user data. * @throws {Error} If the API request fails. */ async function fetchUser(id: string): Promise<User> { // Implement data fetching logic here } """ **Don't Do This:** * Writing obvious or redundant comments. * Leaving outdated or incorrect comments. * Neglecting to document complex code. **Why This Matters:** * **Maintainability:** Improves code readability and makes it easier for others (and yourself in the future) to understand the code. * **Collaboration:** Facilitates team collaboration by providing clear explanations of code logic. * **Knowledge Sharing:** Preserves knowledge about the codebase.
# State Management Standards for Shadcn This document outlines the standards for managing application state in Shadcn projects. It covers various approaches, best practices, and common pitfalls to avoid, focusing on maintainability, performance, and a consistent development experience. This complements the component-level coding standard for Shadcn. ## 1. Choosing a State Management Solution Shadcn provides a robust foundation for building UIs, but it does not prescribe a specific state management library. The choice of state management solution depends on the complexity of your application. ### 1.1 Standard: Use Context API + "useReducer" for simple state management * **Do This:** Start with the built-in React Context API combined with the "useReducer" hook for simple state management needs within isolated parts of your application. * **Don't Do This:** Immediately reach for a large state management library (like Redux or Zustand) for simple applications. This adds unnecessary complexity. **Why:** React's built-in Context API is sufficient for managing state in isolated components or smaller applications, providing a lightweight and straightforward solution without introducing external dependencies. The "useReducer" hook is a better alternative of "useState" hook for state management of more complex components. **Example:** """jsx // ThemeContext.jsx import React, { createContext, useReducer, useContext } from 'react'; const ThemeContext = createContext(); const initialState = { darkMode: false, }; const reducer = (state, action) => { switch (action.type) { case 'TOGGLE_DARK_MODE': return { ...state, darkMode: !state.darkMode }; default: return state; } }; const ThemeProvider = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); return ( <ThemeContext.Provider value={{ state, dispatch }}> {children} </ThemeContext.Provider> ); }; const useTheme = () => { const context = useContext(ThemeContext); if (context === undefined) { throw new Error("useTheme must be used within a ThemeProvider"); } return context; }; export { ThemeProvider, useTheme }; """ """jsx // ComponentUsingTheme.jsx import React from 'react'; import { useTheme } from './ThemeContext'; import { Button } from "@/components/ui/button" const ComponentUsingTheme = () => { const { state, dispatch } = useTheme(); return ( <div> <p>Dark Mode: {state.darkMode ? 'On' : 'Off'}</p> <Button onClick={() => dispatch({ type: 'TOGGLE_DARK_MODE' })}> Toggle Dark Mode </Button> </div> ); }; export default ComponentUsingTheme; """ ### 1.2 Standard: Use Zustand for moderate complexity * **Do This:** Consider Zustand for global state management in medium-sized applications for its simplicity and minimal boilerplate. * **Don't Do This:** Use Redux or similar verbose state management libraries when Zustand can achieve the same results with less code. **Why:** Zustand offers a simple and unopinionated approach to state management. It's easy to learn and integrate, making it ideal for managing global application state without the complexity of larger libraries. **Example:** """jsx // store.js import { create } from 'zustand'; const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }), })); export default useStore; """ """jsx // CounterComponent.jsx import React from 'react'; import useStore from './store'; import { Button } from "@/components/ui/button" const CounterComponent = () => { const count = useStore((state) => state.count); const increment = useStore((state) => state.increment); const decrement = useStore((state) => state.decrement); const reset = useStore((state) => state.reset) return ( <div> <p>Count: {count}</p> <Button onClick={increment}>Increment</Button> <Button onClick={decrement}>Decrement</Button> <Button onClick={reset}>Reset</Button> </div> ); }; export default CounterComponent; """ ### 1.3 Standard: Use Redux Toolkit **only** for complex global application state * **Do This:** Reserve Redux Toolkit for very complex applications that benefit from its structured approach, middleware, and debugging tools. Only use Redux with Redux Toolkit. * **Don't Do This:** Use Redux without Redux Toolkit. Redux without RTK is too verbose and difficult to manage. **Why:** Redux, especially with Redux Toolkit, provides a predictable state container that's beneficial for large, complex applications. It includes features like middleware for handling asynchronous actions, and tools for debugging and time-traveling state. **Example:** """jsx // store.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './counterSlice'; export const store = configureStore({ reducer: { counter: counterReducer, }, }); """ """jsx // counterSlice.js import { createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { value: 0, }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export default counterSlice.reducer; """ """jsx // CounterComponent.jsx import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement, incrementByAmount } from './counterSlice'; import { Button } from "@/components/ui/button" const CounterComponent = () => { const count = useSelector((state) => state.counter.value); const dispatch = useDispatch(); return ( <div> <p>Count: {count}</p> <Button onClick={() => dispatch(increment())}>Increment</Button> <Button onClick={() => dispatch(decrement())}>Decrement</Button> <Button onClick={() => dispatch(incrementByAmount(5))}>Increment by 5</Button> </div> ); }; export default CounterComponent; """ ### 1.4 Standard: Use React Query/Tanstack Query for server state management * **Do This:** Use React Query (now known as TanStack Query) for managing server state, caching, and background updates. * **Don't Do This:** Use local state or Redux to cache server data manually. React Query is specifically designed for this purpose and handles complexities automatically. **Why:** React Query simplifies fetching, caching, synchronizing, and updating server state. It offers built-in features for caching, background updates, retries, and more, reducing boilerplate and improving performance. **Example:** """jsx // usePosts.js import { useQuery } from '@tanstack/react-query'; const fetchPosts = async () => { const response = await fetch('/api/posts'); if (!response.ok) { throw new Error('Failed to fetch posts'); } return response.json(); }; const usePosts = () => { return useQuery({ queryKey: ['posts'], queryFn: fetchPosts }); }; export default usePosts; """ """jsx // PostsComponent.jsx import React from 'react'; import usePosts from './usePosts'; const PostsComponent = () => { const { data: posts, isLoading, error } = usePosts(); if (isLoading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }; export default PostsComponent; """ ## 2. Data Flow and Reactivity ### 2.1 Standard: Prefer unidirectional data flow. * **Do This:** Ensure data flows in one direction, typically from parent components to children via props, and updates flow back to the parent through callbacks or state management solutions. Avoid two-way data binding. * **Don't Do This:** Mutate props directly in child components. This makes debugging harder and leads to unpredictable behavior. **Why:** Unidirectional data flow increases predictability, makes debugging easier, and simplifies reasoning about application state changes. **Example (Correct):** """jsx // ParentComponent.jsx import React, { useState } from 'react'; import ChildComponent from './ChildComponent'; const ParentComponent = () => { const [message, setMessage] = useState('Hello'); const handleMessageChange = (newMessage) => { setMessage(newMessage); }; return ( <div> <p>Parent Message: {message}</p> <ChildComponent message={message} onMessageChange={handleMessageChange} /> </div> ); }; export default ParentComponent; """ """jsx // ChildComponent.jsx import React from 'react'; import { Input } from "@/components/ui/input" const ChildComponent = ({ message, onMessageChange }) => { const handleChange = (event) => { onMessageChange(event.target.value); }; return ( <div> <Input type="text" value={message} onChange={handleChange} /> </div> ); }; export default ChildComponent; """ **Example (Incorrect - Prop Mutation):** """jsx // IncorrectChildComponent.jsx import React from 'react'; import { Input } from "@/components/ui/input" const IncorrectChildComponent = ({ message }) => { const handleChange = (event) => { //DO NOT DO THIS message = event.target.value; // Directly mutating prop }; return ( <div> <Input type="text" value={message} onChange={handleChange} /> </div> ); }; export default IncorrectChildComponent; """ ### 2.2 Standard: Utilize keys for dynamic lists. * **Do This:** Provide unique "key" props when rendering dynamic lists of elements. The key be a stable and predictable value related to THAT ITEM (e.g. item id). * **Don't Do This:** Use array indices as keys, especially if the order of the list items might change. Also don't use randomly generated UUIDS because those change on every render. **Why:** Keys help React efficiently update the DOM when items are added, removed, or reordered. Using incorrect keys (or no keys) degrades performance and can cause unexpected behavior. **Example:** """jsx // Correct const items = [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, ]; const ItemList = () => { return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> ); }; export default ItemList; """ """jsx // Incorrect const items = [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, ]; const BadItemList = () => { return ( <ul> {items.map((item, index) => ( <li key={index}>{item.name}</li> ))} </ul> ); }; export default BadItemList; """ ### 2.3 Standard: Optimize Re-renders with "React.memo" * **Do This:** Use "React.memo" to prevent unnecessary re-renders of components when their props haven't changed. Shadcn provides visually polished components; avoiding unnecessary re-renders on key UI elements is paramount. * **Don't Do This:** Wrap every component with "React.memo" without profiling first. This can add overhead if the component re-renders infrequently or if the prop comparison is expensive. **Why:** "React.memo" optimizes performance by skipping re-renders for components when the props haven't changed. **Example:** """jsx import React from 'react'; const MyComponent = ({ data, onClick }) => { console.log('MyComponent re-rendered'); return ( <button onClick={onClick}> {data.name} </button> ); }; export default React.memo(MyComponent, (prevProps, nextProps) => { // Custom comparison function (optional) // Return true if props are equal, false if props are different return prevProps.data.id === nextProps.data.id; }); """ Without the second argument (the equality function), "React.memo" will do a shallow comparison of the props. If complex props are used (like objects), consider providing the custom equality function for a precise comparison. ### 2.4 Standard: Use "useCallback" and "useMemo" Hooks * **Do This:** Utilize "useCallback" and "useMemo" hooks for optimizing performance by memoizing functions and values that are passed as props to children components. * **Don't Do This:** Overuse "useCallback" and "useMemo" without careful profiling. It is a common mistake to wrap every possible function/value. Also do not forget the dependency arrays of these functions. **Why:** "useCallback" and "useMemo" prevent recreating functions and values on every render. This improves performance by ensuring that child components only re-render when their props have actually changed. **Example:** """jsx import React, { useState, useCallback, useMemo } from 'react'; import MyComponent from './MyComponent'; const ParentComponent = () => { const [count, setCount] = useState(0); // Memoize the data object const data = useMemo(() => ({ id: 1, name: 'Item' }), []); // Memoize the onClick function const handleClick = useCallback(() => { setCount(count + 1); }, [count]); return ( <div> <p>Count: {count}</p> <MyComponent data={data} onClick={handleClick} /> <button onClick={() => setCount(count + 1)}>Increment Parent</button> </div> ); }; export default ParentComponent; """ In this setup, "handleClick" is only recreated when "count" changes, and "data" is only created once preventing unnecessary renders of "MyComponent". ## 3. Shadcn Specific State Management Considerations ### 3.1 Standard: Manage component-level styling state within the component. * **Do This:** For simple UI interactions like toggling a component's visibility or changing its appearance, manage the state directly within the component using "useState". **Why:** This keeps the component self-contained and reduces complexity. Avoid unnecessary state management overhead for simple UI interactions. **Example:** """jsx import React, { useState } from 'react'; import { Button } from "@/components/ui/button" const ToggleComponent = () => { const [isVisible, setIsVisible] = useState(false); return ( <div> <Button onClick={() => setIsVisible(!isVisible)}> {isVisible ? 'Hide' : 'Show'} </Button> {isVisible && <p>This content is visible.</p>} </div> ); }; export default ToggleComponent; """ ### 3.2 Standard: Use Zod for schema validation of form data * **Do This:** Use Zod for validating forms that leverage Shadcn component. Zod has first class Typescript support. This ensures data accuracy and consistency, improving the user experience and the reliability of the application. * **Don't Do This:** Write custom validation functions or regular expressions without leveraging a validation library, as it can be error-prone and hard to maintain. **Why:** Zod offers a declarative and type-safe way to define schemas and validate data. It integrates well with TypeScript, providing excellent developer experience. **Example:** """jsx import { z } from "zod" const formSchema = z.object({ username: z.string().min(2, { message: "Username must be at least 2 characters.", }), email: z.string().email({ message: "Invalid email address.", }), }) export default formSchema """ ### 3.3 Standard: Use "useTransition" for Optimistic UI Updates. * **Do This:** When performing actions that update server state, use the "useTransition" hook to provide an optimistic UI update. This makes the UI feel more responsive. * **Don't Do This:** Block the UI while waiting for server responses which creates a slow user experience. **Why:** Optimistic UI updates provide immediate feedback to the user, improving the perceived performance and responsiveness of the application. **Example:** """jsx import React, { useState, useTransition } from 'react'; import { Button } from "@/components/ui/button" const LikeButton = ({ postId }) => { const [isLiked, setIsLiked] = useState(false); const [isPending, startTransition] = useTransition(); const handleClick = () => { startTransition(() => { setIsLiked(!isLiked); // Simulate an API call setTimeout(() => { console.log("API call completed"); }, 500); }); }; return ( <Button disabled={isPending} onClick={handleClick}> {isLiked ? 'Unlike' : 'Like'} {isPending ? 'Updating...' : ''} </Button> ); }; export default LikeButton; """ ## 4. Anti-Patterns to Avoid ### 4.1 Anti-Pattern: Prop Drilling * **Avoid This:** Passing props through multiple layers of components when only a deeply nested component needs the data. This can hinder component reusability and makes the code hard to maintain. **Solution:** Use Context API, Zustand, or Redux to provide the data where it’s needed without passing it through intermediate components. ### 4.2 Anti-Pattern: Over-reliance on Global State * **Avoid This:** Storing everything in global state. Managing component-specific state locally can lead to unnecessary re-renders throughout the application. **Solution:** Identify which state truly needs to be global and which can be managed locally within components or using Context API for a specific component tree. ### 4.3 Anti-Pattern: Ignoring React Query Optimizations * **Avoid This:** Neglecting React Query's features like "staleTime", "cacheTime", and "refetchOnWindowFocus". **Solution:** Configure React Query's options based on your application's needs. For frequently updated data, a shorter "staleTime" is appropriate. For less frequently updated data, a longer "staleTime" can reduce unnecessary API calls. Consider disabling "refetchOnWindowFocus" if updates are not needed when the user switches back to the tab. ### 4.4 Anti-Pattern: Mutating State Directly * **Avoid This:** Directly modifying state variables without using state update functions (e.g., "useState"'s "setState", Redux reducers, Zustand's "set"). **Solution:** Always use the proper state update mechanisms to trigger re-renders and maintain predictable state changes. ## 5. Community Standards and Patterns ### 5.1 Standard: Atomic State Updates * **Do This:** When updating state that depends on the previous state, use the functional form of "setState" in "useState" or the updater function in "useReducer" or Zustand. **Why:** This ensures that you are working with the correct previous state, avoiding potential race conditions or stale data issues, especially when dealing with asynchronous updates. **Example:** """jsx // useState with functional update import React, { useState } from 'react'; import { Button } from "@/components/ui/button" const CounterComponent = () => { const [count, setCount] = useState(0); const increment = () => { setCount(prevCount => prevCount + 1); }; return ( <div> <p>Count: {count}</p> <Button onClick={increment}>Increment</Button> </div> ); }; export default CounterComponent; """ ### 5.2 Standard: Colocation of State Logic * **Do This:** Keep state logic close to where it's used. If a component's state logic becomes complex, consider moving it into a custom hook. **Why:** This improves code organization, testability, and maintainability. **Example:** """jsx // useCounter.js import { useState } from 'react'; const useCounter = (initialValue = 0) => { const [count, setCount] = useState(initialValue); const increment = () => { setCount(prevCount => prevCount + 1); }; const decrement = () => { setCount(prevCount => prevCount - 1); }; return { count, increment, decrement }; }; export default useCounter; """ """jsx // CounterComponent.jsx import React from 'react'; import useCounter from './useCounter'; import { Button } from "@/components/ui/button" const CounterComponent = () => { const { count, increment, decrement } = useCounter(10); return ( <div> <p>Count: {count}</p> <Button onClick={increment}>Increment</Button> <Button onClick={decrement}>Decrement</Button> </div> ); }; export default CounterComponent; """ ### 5.3 Standard: Graceful Degradation/Error Handling * **Do This:** When fetching data, especially with React Query, handle loading and error states gracefully. Display informative messages or fallback content to provide a good user experience even when things go wrong. * **Don't Do This:** Display blank screens or unhandled errors to the user. **Why:** Proper error handling makes your application more robust and user-friendly. **Example:** """jsx import React from 'react'; import usePosts from './usePosts'; const PostsComponent = () => { const { data: posts, isLoading, error } = usePosts(); if (isLoading) return <p>Loading posts...</p>; if (error) return <p>Error fetching posts: {error.message}</p>; return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }; export default PostsComponent; """ ## 6. Performance Optimization Techniques ### 6.1 Standard: Code Splitting * **Do This:** Implement code splitting using dynamic imports ("React.lazy" and "Suspense") to reduce the initial load time of your application. Defer loading non-critical components. **Why:** Code splitting improves performance by loading only the code that is needed for the current view, thus reducing the initial bundle size. **Example:** """jsx import React, { Suspense } from 'react'; const LazyComponent = React.lazy(() => import('./HeavyComponent')); const MyComponent = () => { return ( <div> <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> </div> ); }; export default MyComponent; """ ### 6.2 Standard: Virtualization of large lists * **Do This:** Use libraries like "react-window" or "react-virtualized" to efficiently render large lists of data. **Why:** Virtualization renders only the visible items in the list, significantly improving performance for long lists. **Example:** """jsx import React from 'react'; import { FixedSizeList as List } from 'react-window'; const Row = ({ index, style }) => ( <div style={style}>Row {index}</div> ); const VirtualizedList = () => { return ( <List height={150} itemCount={1000} itemSize={35} width={300} > {Row} </List> ); }; export default VirtualizedList; """ ### 6.3 Standard: Debouncing and Throttling * **Do This:** Use debouncing and throttling techniques to limit the rate at which functions are executed, particularly for event handlers that fire rapidly (e.g., "onChange" on an input field). * **Don't Do This:** Execute expensive operations on every event. **Why:** Debouncing and throttling improve performance by reducing the number of function calls, which is especially important for optimizing user input handling and API calls. **Example:** """jsx import React, { useState, useCallback } from 'react'; import { debounce } from 'lodash'; import { Input } from "@/components/ui/input" const SearchComponent = () => { const [searchTerm, setSearchTerm] = useState(''); const handleSearch = (value) => { console.log('Searching for:', value); // Perform API call or other expensive operation here }; const debouncedSearch = useCallback( debounce((value) => { handleSearch(value); }, 300), [] ); const handleChange = (event) => { const { value } = event.target; setSearchTerm(value); debouncedSearch(value); }; return ( <div> <Input type="text" placeholder="Search..." value={searchTerm} onChange={handleChange} /> </div> ); }; export default SearchComponent; """ ## 7. Security Considerations for State Management ### 7.1 Standard: Limit State Exposure * **Do This:** Avoid storing sensitive information (e.g., passwords, API keys, personal data) directly in client-side state if it's not absolutely necessary. If you must store sensitive data temporarily, encrypt it and clear it from the state as soon as it's no longer needed. * **Don't Do This:** Store sensitive data in plaintext in local storage or cookies. **Why:** Minimizing the exposure of sensitive data reduces the risk of it being compromised through XSS attacks or other vulnerabilities. ### 7.2 Standard: Sanitize User Inputs * **Do This:** Sanitize and validate all user inputs before storing them in the state. This helps prevent XSS attacks and other vulnerabilities. * **Don't Do This:** Directly use user inputs without any form of validation or sanitization which could be a security risk. **Why:** Sanitizing user inputs ensures that malicious scripts or code cannot be injected into your application through user-controlled data. ### 7.3 Standard: Secure API Communication * **Do This:** Use HTTPS for all API communication to encrypt data in transit. Implement proper authentication and authorization mechanisms to ensure that only authorized users can access and modify state. Use secure cookies with the "HttpOnly" and "Secure" flags. * **Don't Do This:** Use HTTP for API communication. **Why:** HTTPS encrypts the data exchanged between the client and the server, protecting it from eavesdropping and tampering. Proper authentication and authorization ensure that only authorized users can modify application state. ### 7. 4 Standard: Prevent State Injection * **Do This:** Protect against state injection attacks by ensuring that only authorized code can modify the application's state. Use immutable data structures to prevent unauthorized modifications of state. * **Don't Do This:** Allow external scripts or unauthorized code to directly modify the application state. **Why:** Protecting against unauthorized state modifications can prevent malicious actors from manipulating the state, leading to unexpected behavior or data breaches. This updated document provides a comprehensive guide to state management in Shadcn projects, covering various approaches, best practices, and security considerations. Adhering to these guidelines will result in more maintainable, performant, and secure applications.
# Performance Optimization Standards for Shadcn This document outlines performance optimization standards for Shadcn projects. It provides guidelines, best practices, and code examples to help developers build fast, responsive, and resource-efficient applications using Shadcn. ## 1. Data Fetching and Caching Strategies Efficient data fetching and caching are critical for minimizing network requests and improving application load times. ### 1.1. Server-Side Rendering (SSR) or Static Site Generation (SSG) **Standard:** Use SSR or SSG whenever possible for content that doesn't require frequent updates or personalized data. This enables faster initial page loads as the HTML is pre-rendered on the server or during build time. **Why:** SSR and SSG reduce client-side JavaScript execution, leading to faster First Contentful Paint (FCP) and Largest Contentful Paint (LCP). This dramatically improves perceived performance, especially on low-powered devices or slow network connections. **Do This:** * Use Next.js with Shadcn to leverage SSR and SSG capabilities. * In "app/page.tsx", use "async" functions for data fetching in server components. **Don't Do This:** * Default to Client-Side Rendering (CSR) for content that can be pre-rendered. * Fetch data only on the client when initial page load performance is critical. **Example:** """tsx // app/page.tsx import { db } from "@/lib/db"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; async function getData() { // Simulate fetching posts from a database const posts = await db.post.findMany({ orderBy: { createdAt: 'desc' } }); return posts; } export default async function Home() { const posts = await getData(); return ( <div className="container mx-auto py-10"> <Card> <CardHeader> <CardTitle>Latest Posts</CardTitle> </CardHeader> <CardContent> {posts.map((post) => ( <div key={post.id} className="mb-4"> <h3>{post.title}</h3> <p>{post.content}</p> </div> ))} </CardContent> </Card> </div> ); } """ **Anti-pattern:** Fetching data on the client using "useEffect" for the initial page render when SSR or SSG is viable. ### 1.2. Caching Data **Standard:** Implement caching strategies both on the server and client to avoid redundant data fetching. **Why:** Caching reduces the load on backend servers and improves response times for frequently accessed data. Server-side caching also assists in preventing overages on database queries. **Do This:** * Utilize Next.js's built-in data caching mechanisms (e.g., "fetch" API with "revalidate" options). * Implement client-side caching using libraries like "swr" or "react-query" for frequently accessed data. * Use Incremental Static Regeneration (ISR) to periodically update static content without redeploying. **Don't Do This:** * Disable caching unnecessarily, leading to repeated data fetching. * Cache sensitive data without proper security measures. **Example (Next.js Server Component Caching):** """tsx // app/components/PostsList.tsx import { unstable_cache } from 'next/cache'; import { db } from "@/lib/db"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; const getPosts = unstable_cache( async () => { const posts = await db.post.findMany({ orderBy: { createdAt: 'desc' } }); return posts; }, ['posts'], //cache tag - clears cache when revalidated { revalidate: 60, // Revalidate the cache every 60 seconds } ); export async function PostsList() { const posts = await getPosts(); return ( <CardContent> {posts.map((post) => ( <div key={post.id} className="mb-4"> <h3>{post.title}</h3> <p>{post.content}</p> </div> ))} </CardContent> ); } """ **Example (Client-Side Caching with "swr"):** """tsx // app/components/UserProfile.tsx import useSWR from 'swr'; import { fetcher } from '@/lib/utils'; // Assume this is your fetcher function interface User { id: string; name: string; email: string; } function UserProfile({ userId }: { userId: string }) { const { data: user, error, isLoading } = useSWR<User>("/api/users/${userId}", fetcher); if (isLoading) return <div>Loading...</div>; if (error) return <div>Failed to load user</div>; return ( <div> <h1>{user.name}</h1> <p>Email: {user.email}</p> </div> ); } export default UserProfile; """ **Technology-Specific Detail:** Next.js 13+ offers built-in caching at the route segment level, so you can cache data fetches and even entire React Server Components. This is much easier to implement than custom caching solutions. ### 1.3. Pagination and Infinite Scrolling **Standard:** Implement pagination or infinite scrolling for large datasets to load data in manageable chunks. **Why:** Loading an entire dataset at once can lead to performance bottlenecks and a poor user experience. Pagination and infinite scrolling improve initial load times and reduce memory consumption. **Do This:** * Use server-side pagination to limit the number of items returned per request. * Implement infinite scrolling with libraries like "react-infinite-scroll-component" for a seamless user experience. * Provide loading indicators to inform users that more data is being fetched. **Don't Do This:** * Load the entire dataset at once, especially for large datasets. * Neglect to provide loading indicators, leading to a confusing user experience. **Example (Pagination):** """tsx // app/components/PostsList.tsx import { db } from "@/lib/db"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; interface Post { id: string; title: string; content: string; } async function getPaginatedPosts(page: number, pageSize: number): Promise<Post[]> { const skip = (page - 1) * pageSize; const posts = await db.post.findMany({ skip, take: pageSize, orderBy: { createdAt: 'desc' } }); return posts; // return data } interface Props { page: number; pageSize: number; } export async function PostsList({ page, pageSize }:Props) { const posts = await getPaginatedPosts(page, pageSize); return ( <CardContent> {posts.map((post) => ( <div key={post.id} className="mb-4"> <h3>{post.title}</h3> <p>{post.content}</p> </div> ))} </CardContent> ); } """ ### 1.4 Image Optimization **Standard:** Optimize images for the web to reduce file sizes and improve loading times. **Why:** Large, unoptimized images significantly impact page load times and bandwidth usage. Efficient image optimization is an essential aspect of performance management and a great use of caching as well. **Do This:** * Use optimized image formats like WebP or AVIF. * Implement responsive images with the "<Image>" component from "next/image" to serve different sizes based on the user's device. This component handles lazy loading automatically. * Compress images without sacrificing too much quality (use tools like ImageOptim or TinyPNG). * Use a Content Delivery Network (CDN) for image storage and delivery. **Don't Do This:** * Upload large, unoptimized images directly to your server. * Serve the same image size to all devices, wasting bandwidth on smaller screens. * Skip the "alt" attribute on images, which is crucial for accessibility and SEO. **Example:** """tsx // app/components/Hero.tsx import Image from 'next/image'; export default function Hero() { return ( <div className="relative h-96 w-full"> <Image src="/images/hero.jpg" // Local image, can also be a URL alt="A beautiful landscape" layout="fill" // Required attribute objectFit="cover" // Adjust how the image fills the space quality={75} //optimize using compression priority //preload important images /> <div className="absolute inset-0 flex items-center justify-center"> <h1 className="text-4xl font-bold text-white">Welcome to our Website</h1> </div> </div> ); } """ **Technology Specific Detail:** "next/image" handles much of the complexity of image optimization transparently. When using remote images, you MUST configure "next.config.js" to allow the image domains. Alternatively, use a service like Cloudinary or Imgix that handle this automatically. ## 2. Code Splitting and Lazy Loading Code splitting and lazy loading help reduce the initial JavaScript bundle size, improving application startup time. ### 2.1. Dynamic Imports **Standard:** Use dynamic imports ("import()") to load components or modules only when needed. **Why:** Dynamic imports split the code into smaller chunks loaded on demand, reducing the initial bundle size and improving time-to-interactive (TTI). **Do This:** * Use "React.lazy" and "Suspense" for lazy loading components. * Dynamically import large libraries or modules that are not immediately required. * Use "next/dynamic" for lazy loading components from the "pages" directory (if using the older "pages" router). * In the app directory, you can use "React.lazy" in client components. **Don't Do This:** * Import all modules upfront, increasing the initial bundle size. * Overuse lazy loading, as it can introduce network overhead and negatively impact user experience if done excessively. **Example:** """tsx // app/components/MyComponent.tsx (client component) 'use client'; import React, { Suspense, lazy } from 'react'; import { Skeleton } from "@/components/ui/skeleton" const LazyComponent = lazy(() => import('./HeavyComponent')); function MyComponent() { return ( <div> <h1>My Component</h1> <Suspense fallback={<Skeleton className="w-[200px] h-[20px]" />}> <LazyComponent /> </Suspense> </div> ); } export default MyComponent; """ """tsx // app/components/HeavyComponent.tsx export default function HeavyComponent() { // This component contains complex logic or a large library import return ( <div> <h2>This is a Heavy Component</h2> <p>Loaded dynamically!</p> </div> ); } """ **Anti-pattern:** Loading all components eagerly in the main application bundle. ### 2.2. Route-Based Code Splitting **Standard:** Leverage Next.js's automatic route-based code splitting. **Why:** Route-based code splitting ensures that only the code required for the current route is loaded, reducing initial load times. This is done automatically in the app directory. **Do This:** * Organize your application into separate routes or pages. * Ensure that you code is properly bundled for each unique URL path. **Don't Do This:** * Create a single, monolithic application with all code loaded upfront. **Example:** (This is more of an organizational principle than a code example, but crucial for performance.) * "/app/page.tsx" - Home page * "/app/products/page.tsx" - Products page * "/app/blog/page.tsx" - Blog page Each of these routes (pages) will be in its own code split automatically. ## 3. Optimizing React Components Optimizing React components can significantly improve rendering performance and responsiveness. ### 3.1. Memoization **Standard:** Use "React.memo" to prevent unnecessary re-renders of pure components. **Why:** "React.memo" memoizes the rendered output of a component and only re-renders it if the props have changed. This avoids wasteful rendering cycles, especially for components that receive props from parent components that re-render frequently. **Do This:** * Wrap pure components with "React.memo". * Use "useMemo" and "useCallback" hooks to memoize calculated values and event handlers, respectively. **Don't Do This:** * Memoize components unnecessarily, as the memoization process itself has a cost. * Forget to update the memoization key when the component's dependencies change. **Example:** """tsx // app/components/MyComponent.tsx import React, { useState, useCallback } from 'react'; import { Button } from "@/components/ui/button" interface Props { name: string; onClick: () => void; } const MyComponent = React.memo(function MyComponent({ name, onClick }: Props) { console.log("Rendering MyComponent with name: ${name}"); return ( <Button onClick={onClick}> Hello, {name}! </Button> ); }); function ParentComponent() { const [count, setCount] = useState(0); //useCallback to prevent function from being recreated on every render const handleClick = useCallback(() => { setCount((prevCount) => prevCount + 1); }, []); return ( <div> <p>Count: {count}</p> {/* my component is memoized so only renders when name prop changes */} <MyComponent name="World" onClick={handleClick} /> <Button onClick={() => setCount((prevCount) => prevCount + 1)}>Increment</Button> </div> ); } export default ParentComponent; """ **Anti-pattern:** Omitting "useCallback" for event handlers passed as props to memoized components. ### 3.2. Virtualization **Standard:** Use virtualization libraries like "react-window" or "react-virtualized" for rendering large lists of data. **Why:** Virtualization renders only the visible portion of a list, improving performance for lists with thousands of items. This prevents the browser from being overloaded by rendering elements that are not currently on screen. **Do This:** * Use virtualization for long lists where performance is critical. * Adjust the virtualization settings (e.g., row height, overscan count) to optimize performance and user experience. **Don't Do This:** * Render all items in a long list without virtualization. * Use virtualization for short lists, as the overhead may outweigh the benefits. **Example:** """tsx // app/components/MyList.tsx import React from 'react'; import { FixedSizeList as List } from 'react-window'; interface Props { items: string[]; } function Row({ index, style, data }: { index: number; style: React.CSSProperties; data: Props["items"] }) { const item = data[index]; return ( <div style={style} className="p-2 border-b"> {"Row ${index}: ${item}"} </div> ); } function MyList({ items }: Props) { return ( <List height={400} itemCount={items.length} itemSize={35} width={300} data={items} > {Row} </List> ); } export default MyList; """ ### 3.3. Avoiding Unnecessary Re-renders **Standard:** Prevent unnecessary re-renders by carefully managing component state and props. **Why:** Frequent and unnecessary re-renders can cause performance issues, especially for complex components or when rendering large lists. **Do This:** * Use immutable data structures to easily detect changes in state and props. * Avoid mutating state directly; use the "setState" function or the spread operator. * Optimize context providers to prevent unnecessary updates to context consumers. * Use tools such as the React Profiler to measure component render times and identify bottlenecks. **Don't Do This:** * Mutate state directly, leading to unexpected re-renders. * Update context values unnecessarily, causing all consumers to re-render. **Example(Avoiding Mutation):** """tsx // Avoid direct mutation for a better component update lifecycle import React, {useState} from 'react'; function MyComponent() { const [items, setItems] = useState([{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }]); // ❌ Don't do this: mutating the array directly const addItemBad = () => { items.push({ id: items.length + 1, name: "Item ${items.length + 1}" }); setItems(items); // Doesn't trigger a re-render because the reference is the same }; // ✅ Do this: create a new array with the new item const addItemGood = () => { setItems([...items, { id: items.length + 1, name: "Item ${items.length + 1}" }]); // Creates a new array }; return ( <> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={addItemGood}>Add Item (Correct)</button> </> ); } export default MyComponent; """ ### 3.4. Using Keys Effectively **Standard:** Provide unique and stable keys for list items. **Why:** Keys help React identify which items have changed, been added, or been removed in a list. Using the wrong keys can lead to unnecessary re-renders or incorrect component behavior. **Do This:** * Use unique and stable identifiers as keys (e.g., IDs from a database). * Avoid using array indexes as keys when the list is dynamic. **Don't Do This:** * Use random values as keys. * Use array indexes as keys when the list items can be reordered or filtered. **Example:** """tsx // app/components/MyList.tsx import React from 'react'; interface Item { id: string; name: string; } interface Props { items: Item[]; } function MyList({ items }: Props) { return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> // ✅ Use unique and stable IDs as keys ))} </ul> ); } export default MyList; """ ## 4. Shadcn Component Considerations While Shadcn provides pre-built, accessible components styled with Tailwind CSS, some aspects require attention for optimal performance. ### 4.1 Tailwind CSS Optimization **Standard:** Purge unused Tailwind CSS classes **Why:** Tailwind CSS generates a large CSS file. Purging ensures only the CSS classes used in your project are included in the production build, reducing the file size. Shadcn components use tailwind so this is very important **Do This:** * Configure "purge" in your "tailwind.config.js" or "tailwind.config.ts" file to specify which files to scan for CSS classes. **Don't Do This:** * Skip purging CSS classes, leading to a large and inefficient CSS file. * Purge too aggressively, removing CSS classes that are actually used. **Example tailwind.config.js:** """javascript /** @type {import('tailwindcss').Config} */ module.exports = { purge: [ './app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}', './src/**/*.{js,ts,jsx,tsx}', //for example ], darkMode: 'media', // or 'class' theme: { extend: {}, }, variants: { extend: {}, }, plugins: [], } """ ### 4.2. Component Composition for Reusability **Standard:** Favor composition over deep nesting within Shadcn components. **Why:** Excessive nesting of components can increase the rendering cost and make it harder to optimize re-renders. Composition promotes modularity and makes it easier to isolate and optimize individual components. **Do This:** * Break down complex Shadcn components into smaller, reusable components. * Use composition (passing children as props) to create flexible and composable interfaces. * Aim for a shallow component tree to minimize rendering overhead. **Don't Do This:** * Create deeply nested component structures. * Pass large amounts of data as props to deeply nested components. **Example:** Instead of: """tsx // app/components/ComplexForm.tsx import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; function ComplexForm() { return ( <form className="space-y-4"> <div> <Label htmlFor="name">Name:</Label> <Input type="text" id="name" /> </div> <div> <Label htmlFor="email">Email:</Label> <Input type="email" id="email" /> </div> <div> <Label htmlFor="password">Password:</Label> <Input type="password" id="password" /> </div> <Button>Submit</Button> </form> ); } export default ComplexForm; """ Do This: """tsx // app/components/FormInput.tsx import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; interface Props { label: string; id: string; type: string; } function FormInput({ label, id, type }: Props) { return ( <div> <Label htmlFor={id}>{label}:</Label> <Input type={type} id={id} /> </div> ); } export default FormInput; """ """tsx // app/components/ComplexForm.tsx import { Button } from "@/components/ui/button"; import FormInput from "./FormInput"; function ComplexForm() { return ( <form className="space-y-4"> <FormInput label="Name" id="name" type="text" /> <FormInput label="Email" id="email" type="email" /> <FormInput label="Password" id="password" type="password" /> <Button>Submit</Button> </form> ); } export default ComplexForm; """ The refactored code keeps components smaller and focused on their core functionality which helps with performance. ### 4.3 Conditional Rendering **Standard:** Use conditional rendering judiciously. **Why:** While conditional rendering is essential, excessive or deeply nested conditional logic can hinder performance. It increases the complexity of rendering and can make it more difficult to optimize. **Do this:** * Use short-circuit evaluation ("&&", "||") for simple conditional rendering scenarios. * Extract complex conditional logic into separate components. * Use the "React.Fragment" ("<> </>") or "React.createElement" when rendering null or multiple elements conditionally. * Use "useMemo" to memoize parts of the template that should not be re rendered. **Don't Do this:** * Create deeply nested conditional structures that make it difficult to follow the rendering flow and can degrade performance. **Example:** """tsx // app/components/ConditionalComponent.tsx import React, {useState} from 'react'; import { Button } from "@/components/ui/button" function ConditionalComponent() { const [isLoading, setIsLoading] = useState(false); // ✅ Short-circuit evaluation for simple rendering return ( <> {isLoading ? ( // Use the ternary operator for explicit conditions <p>Loading...</p> ) : ( <Button onClick={() => setIsLoading(true)}>Load Data</Button> )} </> ); } export default ConditionalComponent; """ ## 5. Monitoring and Tooling **Standard:** Regularly monitor application performance and use profiling tools to identify and address bottlenecks. **Why:** Monitoring and profiling provide valuable insights into application behavior and help you identify areas for optimization. **Do This:** * Use the React Profiler to measure component render times and identify performance bottlenecks. * Monitor key performance metrics like FCP, LCP, and TTI using tools like Google PageSpeed Insights or WebPageTest. * Use browser developer tools to analyze network requests, JavaScript execution, and memory usage. * Implement logging and error tracking to identify and resolve performance-related issues in production. **Don't Do This:** * Ignore performance issues until they become critical problems. * Rely solely on manual testing to identify performance bottlenecks. * Fail to monitor application performance in production. **Example:** 1. **Using React Profiler:** In development mode, use the React Profiler in the React DevTools to identify slow-rendering components. 2. **Performance Metrics:** Use PageSpeed Insights to get a high-level overview of your application's performance and suggestions for improvement. 3. **Browser DevTools:** Use the Network tab to analyze network requests and identify large files or slow API calls. Use the Performance tab to profile JavaScript execution and identify bottlenecks. By adhering to these performance optimization standards, Shadcn developers can build fast, responsive, and efficient applications that provide a great user experience. Regularly review and update these standards to stay current with the latest best practices and technologies.
# Testing Methodologies Standards for Shadcn This document outlines the coding standards for testing methodologies in Shadcn projects. It provides guidelines for unit, integration, and end-to-end (E2E) testing, with a focus on best practices that improve code quality, maintainability, and reliability within the Shadcn ecosystem. These guidelines are designed to inform both developers and AI coding assistants, helping to ensure consistent and high-quality testing across all Shadcn codebases. ## 1. General Testing Principles ### 1.1 Test Pyramid **Principle:** Follow the test pyramid structure to ensure a balanced testing strategy: many unit tests, fewer integration tests, and even fewer E2E tests. **Why:** This approach provides fast feedback through unit tests, while integration and E2E tests validate interactions between components and the overall application. The cost and maintenance increase as you move up the pyramid. **Do This:** * Write comprehensive unit tests for individual components and functions. * Create integration tests to verify component interactions and data flow. * Use E2E tests for critical user flows and system-level validation. **Don't Do This:** * Rely solely on E2E tests, as they are slower and more brittle. * Neglect unit tests in favor of integration or E2E tests. ### 1.2 Test-Driven Development (TDD) **Principle:** Consider adopting TDD principles by writing tests before implementing the actual code. **Why:** TDD drives design by forcing you to think about the component's interface and behavior before implementation. It improves code coverage and leads to more testable and maintainable code. **Do This:** * Write a failing test that defines the desired functionality. * Implement the code to make the test pass. * Refactor the code while ensuring all tests still pass. **Don't Do This:** * Write tests only after the code is complete. * Skip the refactoring step, leading to potentially less maintainable code. ### 1.3 Test Isolation **Principle:** Ensure tests are isolated from each other and external dependencies as much as possible. **Why:** Isolation makes tests more predictable and easier to debug. It also prevents tests from interfering with each other, ensuring reliable results. **Do This:** * Use mocking and stubbing to isolate components from external services and dependencies. * Reset the state of the application and environment between tests. * Avoid shared mutable state that can affect test outcomes. **Don't Do This:** * Rely on external databases or APIs in unit tests without proper mocking. * Allow tests to modify global state, leading to unpredictable results. ## 2. Unit Testing Standards ### 2.1 Scope **Principle:** Unit tests should focus on individual components, functions, or modules in isolation. **Why:** Isolating your components helps narrow down exactly where failures occur and allows you to test edge cases efficiently. **Do This:** * Test component props, state updates, and rendering logic. * Test utility functions for correct output based on different inputs. * Verify that event handlers trigger the expected actions. **Don't Do This:** * Test multiple components interacting with each other in a single unit test (that's integration testing). * Over-test implementation details that are likely to change, instead focus on public APIs or externally visible behaviour. ### 2.2 Tools and Libraries **Principle:** Use appropriate testing libraries and frameworks, such as Jest, Vitest, React Testing Library, or Testing Playground. **Why:** These tools provide utilities for rendering components, simulating user interactions, and asserting expected outcomes. The React Testing Library encourages testing from the user's perspective. **Do This:** * Use "React Testing Library"'s "render", "screen", and "fireEvent" utilities. * Utilize Jest or Vitest for test runners, assertions, and mocking. * Use "msw" (Mock Service Worker) to mock API requests. * Consider "jest-dom" for extended DOM assertion matchers. **Don't Do This:** * Use Enzyme, which is becoming deprecated; prefer React Testing Library. * Rely on shallow rendering unless specifically needed; prefer full rendering for behavior testing. ### 2.3 Code Examples **Example 1: Testing a simple Shadcn button component** """typescript // components/Button.tsx (Shadcn based) import * as React from "react" import { cn } from "@/lib/utils" export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant?: | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined size?: "default" | "sm" | "lg" | "icon" | null | undefined } const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ({ className, children, variant = "default", size = "default", ...props }, ref) => { return ( <button className={cn( "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background hover:bg-accent hover:text-accent-foreground", variant === "default" && "bg-primary text-primary-foreground hover:bg-primary/90", variant === "destructive" && "bg-destructive text-destructive-foreground hover:bg-destructive/90", variant === "outline" && "bg-transparent border border-input hover:bg-accent hover:text-accent-foreground", variant === "secondary" && "bg-secondary text-secondary-foreground hover:bg-secondary/80", variant === "ghost" && "hover:bg-accent hover:text-accent-foreground", size === "default" && "h-10 px-4 py-2", size === "sm" && "h-9 rounded-md px-3", size === "lg" && "h-11 rounded-md px-8", size === "icon" && "h-10 w-10", className )} ref={ref} {...props} > {children} </button> ) } ) Button.displayName = "Button" export { Button }; """ """typescript // components/Button.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import { Button } from './Button'; describe('Button Component', () => { it('renders with default props', () => { render(<Button>Click me</Button>); const buttonElement = screen.getByRole('button', { name: 'Click me' }); expect(buttonElement).toBeInTheDocument(); }); it('applies the correct class names based on the variant prop', () => { render(<Button variant="primary">Primary Button</Button>); const buttonElement = screen.getByRole('button', { name: 'Primary Button' }); expect(buttonElement).toHaveClass('bg-primary'); }); it('applies the correct class names based on the size prop', () => { render(<Button size="sm">Small Button</Button>); const buttonElement = screen.getByRole('button', { name: 'Small Button' }); expect(buttonElement).toHaveClass('h-9'); }); it('handles click events', () => { const onClick = jest.fn(); render(<Button onClick={onClick}>Clickable Button</Button>); const buttonElement = screen.getByRole('button', { name: 'Clickable Button' }); fireEvent.click(buttonElement); expect(onClick).toHaveBeenCalledTimes(1); }); it('is disabled when the disabled prop is true', () => { render(<Button disabled>Disabled Button</Button>); const buttonElement = screen.getByRole('button', {name: 'Disabled Button'}); expect(buttonElement).toBeDisabled(); }); }); """ **Example 2: Testing utility functions** """typescript // lib/utils.ts import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } """ """typescript // lib/utils.test.ts import { cn } from './utils'; describe('cn utility function', () => { it('merges class names correctly', () => { const result = cn('class1', 'class2', 'class3'); expect(result).toBe('class1 class2 class3'); }); it('handles conditional class names', () => { const result = cn('class1', { 'class2': true }, { 'class3': false }); expect(result).toBe('class1 class2'); }); it('overrides conflicting class names with tailwind-merge', () => { const result = cn('mr-2', 'mr-4'); // tailwind-merge will prefer "mr-4" as it is later in the input expect(result).toBe('mr-4'); }); }); """ ### 2.4 Anti-Patterns * **Testing implementation details:** Focus on the component's output and behavior rather than internal implementation. * **Skipping edge cases:** Ensure tests cover all possible input and state combinations, including error conditions. * **Ignoring accessibility:** Use tools like "jest-axe" to verify that components are accessible. ## 3. Integration Testing Standards ### 3.1 Scope **Principle:** Integration tests should verify interactions between multiple components or modules. **Why:** Integration tests ensure that different parts of the application work together correctly, focusing on data flow and correct functionality when components are combined. **Do This:** * Test interactions between components that rely on each other. * Verify data flow between different parts of the application. * Test state management and data persistence. * Specifically check interactions between UI components and backend code. **Don't Do This:** * Test the entire application in a single integration test; that's E2E testing. * Over-mock component dependencies; aim for realistic interaction scenarios. ### 3.2 Tools and Libraries **Principle:** Use libraries that help simulate user interactions and data flow in a realistic manner. **Why:** Replicating real-world scenarios in testing helps ensure that the integrated components function as expected in the actual application **Do This:** * Use "React Testing Library" to simulate user interactions across multiple components. * Use mock APIs (e.g., "msw") for realistic data fetching scenarios. * Utilize state management libraries (e.g., Zustand, Redux Toolkit) to mock stores. **Don't Do This:** * Rely solely on unit tests to verify component interactions. * Avoid mocking external endpoints completely, preventing realistic integration scenarios. ### 3.3 Code Examples **Example: Testing a component that fetches and displays data** """typescript // components/DataDisplay.tsx import React, { useState, useEffect } from 'react'; import { useData } from './DataContext'; // Example using useContext for data fetching function DataDisplay() { const { data, loading, error } = useData(); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error}</p>; return ( <ul> {data.map((item: any) => ( <li key={item.id}>{item.name}</li> ))} </ul> ); } export default DataDisplay; """ """typescript // components/DataDisplay.test.tsx import { render, screen, waitFor } from '@testing-library/react'; import DataDisplay from './DataDisplay'; import { DataContext } from './DataContext'; // Match context location const mockData = [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, ]; const MockDataProvider = ({ children, data, loading, error }: any) => ( <DataContext.Provider value={{ data, loading, error }}> {children} </DataContext.Provider> ); describe('DataDisplay Integration', () => { it('displays data correctly after loading', async () => { render( <MockDataProvider data={mockData} loading={false} error={null}> <DataDisplay /> </MockDataProvider> ); await waitFor(() => { expect(screen.getByText('Item 1')).toBeInTheDocument(); expect(screen.getByText('Item 2')).toBeInTheDocument(); }); }); it('shows a loading message while data is loading', () => { render( <MockDataProvider data={[]} loading={true} error={null}> <DataDisplay /> </MockDataProvider> ); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); it('displays an error message if there is an error', () => { render( <MockDataProvider data={[]} loading={false} error={'Failed to fetch data'}> <DataDisplay /> </MockDataProvider> ); expect(screen.getByText('Error: Failed to fetch data')).toBeInTheDocument(); }); }); """ ### 3.4 Anti-Patterns * **Over-mocking:** Over-mocking component dependencies can lead to tests that do not accurately reflect real-world interactions. * **Ignoring asynchronous behavior:** Ensure tests correctly handle asynchronous operations like data fetching and state updates. * **Testing implementation details:** Focus on testing the component's output and behavior rather than internal implementation details. ## 4. End-to-End (E2E) Testing Standards ### 4.1 Scope **Principle:** E2E tests should simulate real user scenarios to validate the entire application flow. **Why:** E2E tests offer the highest confidence by ensuring that the entire system, including the UI, backend services, and database, works together correctly from the user's perspective. **Do This:** * Test critical user flows, such as login, signup, and checkout. * Simulate user interactions with the UI to ensure correct behavior. * Verify data persistence and consistency across different application states. * Perform cross-browser testing to ensure compatibility. **Don't Do This:** * Use E2E tests for every small component or function; this is inefficient and leads to brittle tests. * Rely solely on E2E tests as the primary testing strategy; balance with unit and integration tests. ### 4.2 Tools and Libraries **Principle:** Employ reliable E2E testing frameworks and libraries that simulate user interactions in a browser environment **Why:** Using these tools helps ensure realistic testing that replicates how users interact with your application. **Do This:** * Use Playwright or Cypress for browser automation and test execution. * Utilize screenshot and video recording features for debugging and reporting. * Use mock APIs (e.g., "msw", "nock") for controlled testing environments. **Don't Do This:** * Use outdated or less reliable E2E testing tools. * Avoid taking advantage of debugging and reporting features. ### 4.3 Code Examples **Example: Testing a login flow with Playwright** """typescript // tests/login.spec.ts import { test, expect } from '@playwright/test'; test('successful login', async ({ page }) => { await page.goto('/login'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'password123'); await page.click('button[type="submit"]'); await page.waitForURL('/dashboard'); await expect(page).toHaveURL('/dashboard'); await expect(page.locator('text=Welcome')).toBeVisible(); }); test('invalid login displays error message', async ({ page }) => { await page.goto('/login'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'wrongpassword'); await page.click('button[type="submit"]'); await expect(page.locator('text=Invalid credentials')).toBeVisible(); }); """ ### 4.4 Anti-Patterns * **Flaky tests:** Write robust tests that are not susceptible to timing issues or environment inconsistencies. * **Over-reliance on UI locators:** Use stable locators (e.g., "data-testid", "aria-label") rather than brittle CSS selectors. * **Ignoring test setup and teardown:** Properly set up and tear down the test environment to ensure consistent results. ## 5. Shadcn Specific Testing Considerations ### 5.1 Component Variant Testing Shadcn UI often relies on component variants (using "cn" utility). Ensure your tests cover all possible variant combinations. **Example:** """typescript // Tests if the 'destructive' variant of a button renders the correct style it('renders destructive button style', () => { render(<Button variant="destructive">Delete</Button>); const buttonElement = screen.getByRole('button', { name: 'Delete' }); expect(buttonElement).toHaveClass('bg-destructive'); }); """ ### 5.2 Accessibility Testing with Radix UI Primitives Shadcn UI often uses Radix UI primitives, making accessibility a priority. **Do This:** * Use "jest-axe" or similar tools to automatically check for accessibility issues in your components. * Write tests to ensure components have proper ARIA attributes and keyboard navigation. **Example:** """typescript import { axe, toHaveNoViolations } from 'jest-axe'; import { render } from '@testing-library/react'; import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogCancel, AlertDialogAction } from "@/components/ui/alert-dialog" expect.extend(toHaveNoViolations); it('should pass accessibility tests', async () => { const { container } = render( <AlertDialog> <AlertDialogTrigger>Open</AlertDialogTrigger> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogDescription> This action cannot be undone. This will permanently delete your account and remove your data from our servers. </AlertDialogDescription> </AlertDialogHeader> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction>Continue</AlertDialogAction> </AlertDialogContent> </AlertDialog> ); const results = await axe(container); expect(results).toHaveNoViolations(); }); """ ### 5.3 Theme and Customization Testing Shadcn emphasizes theming and component customization. Test that these customizations are correctly applied. **Do This:** * If your application utilizes a custom theme, ensure that your tests render components within a ThemeProvider or similar context. * Verify that custom class names and styles are correctly merged and applied to components. **Example:** *Assume usage of Next Themes* """typescript import { ThemeProvider } from 'next-themes'; import { render, screen } from '@testing-library/react'; import { Button } from './Button'; describe('Button theming', () => { it('applies custom class names correctly', () => { render( <ThemeProvider attribute="class" defaultTheme="system" enableSystem={false}> <Button className="custom-button">Themed Button</Button> </ThemeProvider> ); const buttonElement = screen.getByRole('button', { name: 'Themed Button' }); expect(buttonElement).toHaveClass('custom-button'); }); }); """ ## 6. Continuous Integration (CI) ### 6.1 Automated Test Execution **Principle:** Integrate tests into the CI/CD pipeline to automatically run tests on every code change. **Why:** Automated testing helps catch regressions early and ensures that code changes do not break existing functionality. **Do This:** * Set up CI/CD pipelines using tools like GitHub Actions, GitLab CI, or Jenkins. * Configure the pipeline to run unit, integration, and E2E tests automatically. * Use code coverage tools (e.g., Istanbul, Jest coverage) to track test coverage. * Fail the build if tests fail to prevent broken code from being deployed. **Don't Do This:** * Manually run tests before deployment. * Ignore failing tests in the CI/CD pipeline. * Deploy code with low test coverage. This document provides a comprehensive guideline for testing methodologies in Shadcn projects. Adhering to these standards will help ensure the creation of robust, maintainable, and reliable applications.