Creating Reusable and Maintainable Component Libraries

Building a reusable and maintainable component library can significantly enhance the development process by promoting consistency, reusability, and scalability. Here’s a guide to creating such a library in React:

1. Setting Up the Project

  • Initialize the Project:

    npx create-react-app component-library --template typescript
    cd component-library
  • Project Structure:

    component-library/
    src/ components/ Button/ Button.tsx Button.test.tsx Button.module.css Input/ Input.tsx Input.test.tsx Input.module.css index.ts styles/ index.css package.json tsconfig.json

2. Creating a Basic Component

  • Button Component:

    // src/components/Button/Button.tsx
    import React from 'react'; import styles from './Button.module.css'; interface ButtonProps { label: string; onClick: () => void; disabled?: boolean; } const Button: React.FC<ButtonProps> = ({ label, onClick, disabled = false }) => { return ( <button className={styles.button} onClick={onClick} disabled={disabled}> {label} </button> ); }; export default Button;
    /* src/components/Button/Button.module.css */
    .button { padding: 10px 20px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } .button:disabled { background-color: #cccccc; cursor: not-allowed; }
  • Button Test:

    // src/components/Button/Button.test.tsx
    import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import Button from './Button'; test('renders button with label', () => { render(<Button label="Click Me" onClick={() => {}} />); const buttonElement = screen.getByText(/click me/i); expect(buttonElement).toBeInTheDocument(); }); test('calls onClick handler when clicked', () => { const handleClick = jest.fn(); render(<Button label="Click Me" onClick={handleClick} />); const buttonElement = screen.getByText(/click me/i); fireEvent.click(buttonElement); expect(handleClick).toHaveBeenCalledTimes(1); }); test('is disabled when disabled prop is true', () => { render(<Button label="Click Me" onClick={() => {}} disabled />); const buttonElement = screen.getByText(/click me/i); expect(buttonElement).toBeDisabled(); });

3. Creating Another Component

  • Input Component:

    // src/components/Input/Input.tsx
    import React, { ChangeEvent } from 'react'; import styles from './Input.module.css'; interface InputProps { value: string; onChange: (value: string) => void; placeholder?: string; } const Input: React.FC<InputProps> = ({ value, onChange, placeholder = '' }) => { const handleChange = (e: ChangeEvent<HTMLInputElement>) => { onChange(e.target.value); }; return ( <input className={styles.input} type="text" value={value} onChange={handleChange} placeholder={placeholder} /> ); }; export default Input;
    /* src/components/Input/Input.module.css */
    .input { padding: 10px; border: 1px solid #cccccc; border-radius: 4px; width: 100%; }
  • Input Test:

    // src/components/Input/Input.test.tsx
    import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import Input from './Input'; test('renders input with value', () => { render(<Input value="test" onChange={() => {}} />); const inputElement = screen.getByDisplayValue(/test/i); expect(inputElement).toBeInTheDocument(); }); test('calls onChange handler when value changes', () => { const handleChange = jest.fn(); render(<Input value="test" onChange={handleChange} />); const inputElement = screen.getByDisplayValue(/test/i); fireEvent.change(inputElement, { target: { value: 'new value' } }); expect(handleChange).toHaveBeenCalledWith('new value'); }); test('displays placeholder text', () => { render(<Input value="" onChange={() => {}} placeholder="Enter text" />); const inputElement = screen.getByPlaceholderText(/enter text/i); expect(inputElement).toBeInTheDocument(); });

4. Exporting Components

  • Index File:
    // src/index.ts
    export { default as Button } from './components/Button/Button'; export { default as Input } from './components/Input/Input';

5. Styling and Theming

  • Global Styles:

    /* src/styles/index.css */
    body { margin: 0; font-family: Arial, sans-serif; }
  • Theming:

    // src/theme.ts
    export const theme = { colors: { primary: '#007bff', secondary: '#6c757d', success: '#28a745', danger: '#dc3545', warning: '#ffc107', info: '#17a2b8', light: '#f8f9fa', dark: '#343a40', }, };
  • Using Theme in Components:

    // src/components/Button/Button.tsx
    import React from 'react'; import styles from './Button.module.css'; import { theme } from '../../theme'; interface ButtonProps { label: string; onClick: () => void; disabled?: boolean; variant?: 'primary' | 'secondary' | 'danger'; } const Button: React.FC<ButtonProps> = ({ label, onClick, disabled = false, variant = 'primary' }) => { return ( <button className={styles.button} onClick={onClick} disabled={disabled} style={{ backgroundColor: theme.colors[variant] }} > {label} </button> ); }; export default Button;

6. Documentation with Storybook

  • Setting Up Storybook:

    npx sb init
  • Creating Stories:

    // src/components/Button/Button.stories.tsx
    import React from 'react'; import { Meta, Story } from '@storybook/react'; import Button, { ButtonProps } from './Button'; export default { title: 'Components/Button', component: Button, } as Meta; const Template: Story<ButtonProps> = (args) => <Button {...args} />; export const Primary = Template.bind({}); Primary.args = { label: 'Primary Button', onClick: () => alert('Button clicked!'), variant: 'primary', }; export const Secondary = Template.bind({}); Secondary.args = { label: 'Secondary Button', onClick: () => alert('Button clicked!'), variant: 'secondary', }; export const Disabled = Template.bind({}); Disabled.args = { label: 'Disabled Button', onClick: () => alert('Button clicked!'), disabled: true, };

7. Publishing the Component Library

  • Build the Library:

    npm run build
  • Configure package.json for Publishing:

    // package.json
    { "name": "my-component-library", "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc" }, "peerDependencies": { "react": "^17.0.2", "react-dom": "^17.0.2" }, "devDependencies": { "@types/react": "^17.0.5", "@types/react-dom": "^17.0.3", "typescript": "^4.2.4" } }
  • Publish to npm:

    npm publish --access public

By following these steps, you can create a reusable and maintainable component library in React, ensuring consistency and efficiency across your projects.