Bis 2025 hat sich das React-Ökosystem erheblich weiterentwickelt, wobei sich Shadcn/UI und Radix als De-facto-Standards für die Entwicklung moderner, zugänglicher Schnittstellen etabliert haben. In diesem Leitfaden werden Best Practices für die Entwicklung benutzerdefinierter Plugins untersucht, die sich nahtlos in dieses Ökosystem integrieren lassen.
Grundlagen und Architektur
Empfohlene Plugin-Struktur
// src/plugins/MyCustomPlugin/index.ts import { createPlugin } from '@shadcn/plugin-core'; import { Root, Trigger, Content } from '@radix-ui/react-dialog'; import { Settings2 } from 'lucide-react'; export interface MyCustomPluginProps { theme?: 'light' | 'dark'; position?: 'left' | 'right'; onStateChange?: (state: PluginState) => void; } export const MyCustomPlugin = createPlugin<MyCustomPluginProps>({ name: 'my-custom-plugin', version: '1.0.0', setup(props) { return { components: { Root, Trigger, Content, }, icons: { Settings: Settings2, }, theme: props.theme || 'light', }; }, });
Starkes Typsystem
// src/types/plugin.ts export interface PluginConfig { name: string; version: string; dependencies?: Record<string, string>; peerDependencies?: Record<string, string>; } export interface PluginContext<T = unknown> { theme: 'light' | 'dark'; components: Record<string, React.ComponentType>; icons: Record<string, LucideIcon>; state: T; } export interface PluginHooks<T = unknown> { useState(): [T, (value: T) => void]; useEffect(effect: () => void, deps?: any[]): void; useContext(): PluginContext<T>; }
Integration mit Shadcn/UI
Benutzerdefinierte Basiskomponente
// src/components/CustomComponent.tsx import * as React from 'react'; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Dialog } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; interface CustomComponentProps { className?: string; children?: React.ReactNode; onAction?: () => void; } export const CustomComponent = React.forwardRef< HTMLDivElement, CustomComponentProps >(({ className, children, onAction, ...props }, ref) => { const [open, setOpen] = React.useState(false); return ( <div ref={ref} className={cn( "flex flex-col space-y-4 rounded-lg border p-4", className )} {...props} > <Dialog open={open} onOpenChange={setOpen}> <Dialog.Trigger asChild> <Button variant="outline">Abrir</Button> </Dialog.Trigger> <Dialog.Content> <Dialog.Header> <Dialog.Title>Configurações do Plugin</Dialog.Title> <Dialog.Description> Ajuste as configurações do seu plugin aqui. </Dialog.Description> </Dialog.Header> <div className="grid gap-4 py-4"> <Input id="config" placeholder="Configuração" className="col-span-3" /> </div> <Dialog.Footer> <Button onClick={onAction}>Salvar</Button> </Dialog.Footer> </Dialog.Content> </Dialog> {children} </div> ); }); CustomComponent.displayName = "CustomComponent";
Benutzerdefinierte Haken
// src/hooks/usePluginState.ts import { create } from 'zustand'; interface PluginState { isEnabled: boolean; config: Record<string, unknown>; theme: 'light' | 'dark'; } export const usePluginState = create<PluginState>((set) => ({ isEnabled: false, config: {}, theme: 'light', toggleEnabled: () => set((state) => ({ isEnabled: !state.isEnabled })), updateConfig: (config: Partial<Record<string, unknown>>) => set((state) => ({ config: { ...state.config, ...config }, })), setTheme: (theme: 'light' | 'dark') => set({ theme }), }));
Integration mit Radix Primitives
Zugängliche Komponenten
// src/components/AccessiblePlugin.tsx import * as React from 'react'; import * as Tooltip from '@radix-ui/react-tooltip'; import * as Switch from '@radix-ui/react-switch'; import { styled } from '@stitches/react'; const StyledSwitch = styled(Switch.Root, { width: 42, height: 25, backgroundColor: 'var(--black-a9)', borderRadius: '9999px', position: 'relative', '&[data-state="checked"]': { backgroundColor: 'var(--primary)', }, }); const StyledThumb = styled(Switch.Thumb, { width: 21, height: 21, backgroundColor: 'white', borderRadius: '9999px', transition: 'transform 100ms', transform: 'translateX(2px)', '&[data-state="checked"]': { transform: 'translateX(19px)', }, }); export const AccessiblePlugin: React.FC = () => { const [enabled, setEnabled] = React.useState(false); return ( <Tooltip.Provider> <Tooltip.Root> <Tooltip.Trigger asChild> <StyledSwitch checked={enabled} onCheckedChange={setEnabled} aria-label="Toggle plugin" > <StyledThumb /> </StyledSwitch> </Tooltip.Trigger> <Tooltip.Portal> <Tooltip.Content className="rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground animate-in fade-in-0 zoom-in-95" sideOffset={5} > {enabled ? 'Desativar' : 'Ativar'} plugin <Tooltip.Arrow className="fill-primary" /> </Tooltip.Content> </Tooltip.Portal> </Tooltip.Root> </Tooltip.Provider> ); };
Integration mit Lucide-react
Benutzerdefiniertes Symbolsystem
// src/components/IconSystem.tsx import * as React from 'react'; import { Settings, Plus, Minus, Check, X, ChevronRight, ChevronLeft, ChevronUp, ChevronDown, } from 'lucide-react'; export const iconMap = { settings: Settings, plus: Plus, minus: Minus, check: Check, close: X, chevronRight: ChevronRight, chevronLeft: ChevronLeft, chevronUp: ChevronUp, chevronDown: ChevronDown, } as const; interface IconProps { name: keyof typeof iconMap; size?: number; className?: string; } export const Icon: React.FC<IconProps> = ({ name, size = 24, className, }) => { const IconComponent = iconMap[name]; return <IconComponent size={size} className={className} />; };
Best Practices
1. Staatsverwaltung
// src/store/pluginStore.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface PluginStore { plugins: Record<string, boolean>; settings: Record<string, unknown>; togglePlugin: (id: string) => void; updateSettings: (id: string, settings: unknown) => void; } export const usePluginStore = create<PluginStore>()( persist( (set) => ({ plugins: {}, settings: {}, togglePlugin: (id) => set((state) => ({ plugins: { ...state.plugins, [id]: !state.plugins[id], }, })), updateSettings: (id, settings) => set((state) => ({ settings: { ...state.settings, [id]: settings, }, })), }), { name: 'plugin-store', } ) );
2. Themensystem
// src/themes/plugin-theme.ts import { createTheme } from '@shadcn/theme'; export const lightTheme = createTheme({ colors: { primary: 'hsl(222.2 47.4% 11.2%)', secondary: 'hsl(217.2 32.6% 17.5%)', accent: 'hsl(210 40% 96.1%)', background: 'hsl(0 0% 100%)', foreground: 'hsl(222.2 47.4% 11.2%)', }, }); export const darkTheme = createTheme({ colors: { primary: 'hsl(210 40% 98%)', secondary: 'hsl(217.2 32.6% 17.5%)', accent: 'hsl(217.2 32.6% 17.5%)', background: 'hsl(222.2 47.4% 11.2%)', foreground: 'hsl(210 40% 98%)', }, });
3. Ereignissystem
// src/events/plugin-events.ts type EventCallback = (...args: any[]) => void; class PluginEventSystem { private events: Map<string, Set<EventCallback>>; constructor() { this.events = new Map(); } on(event: string, callback: EventCallback) { if (!this.events.has(event)) { this.events.set(event, new Set()); } this.events.get(event)!.add(callback); } off(event: string, callback: EventCallback) { this.events.get(event)?.delete(callback); } emit(event: string, ...args: any[]) { this.events.get(event)?.forEach((callback) => { callback(...args); }); } } export const eventSystem = new PluginEventSystem();
Vollständiges Plugin-Beispiel
// src/plugins/DataTablePlugin/index.tsx import * as React from 'react'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { MoreHorizontal, ArrowUpDown } from 'lucide-react'; interface DataTablePluginProps<T> { data: T[]; columns: { key: keyof T; label: string; sortable?: boolean; }[]; onSort?: (key: keyof T) => void; } export function DataTablePlugin<T>({ data, columns, onSort, }: DataTablePluginProps<T>) { const [sortKey, setSortKey] = React.useState<keyof T | null>(null); const [searchTerm, setSearchTerm] = React.useState(""); const handleSort = (key: keyof T) => { setSortKey(key); onSort?.(key); }; const filteredData = React.useMemo(() => { if (!searchTerm) return data; return data.filter((item) => Object.values(item).some((value) => String(value) .toLowerCase() .includes(searchTerm.toLowerCase()) ) ); }, [data, searchTerm]); return ( <div className="space-y-4"> <Input placeholder="Pesquisar..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="max-w-sm" /> <Table> <TableHeader> <TableRow> {columns.map((column) => ( <TableHead key={String(column.key)}> <div className="flex items-center space-x-2"> <span>{column.label}</span> {column.sortable && ( <Button variant="ghost" size="sm" onClick={() => handleSort(column.key)} > <ArrowUpDown className="h-4 w-4" /> </Button> )} </div> </TableHead> ))} <TableHead className="w-[100px]">Ações</TableHead> </TableRow> </TableHeader> <TableBody> {filteredData.map((row, i) => ( <TableRow key={i}> {columns.map((column) => ( <TableCell key={String(column.key)}> {String(row[column.key])} </TableCell> ))} <TableCell> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="h-8 w-8 p-0" > <MoreHorizontal className="h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem> Editar </DropdownMenuItem> <DropdownMenuItem> Excluir </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </TableCell> </TableRow> ))} </TableBody> </Table> </div> ); }
Tests und Qualität
1. Unit-Tests
// src/plugins/__tests__/DataTablePlugin.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import { DataTablePlugin } from '../DataTablePlugin'; describe('DataTablePlugin', () => { const mockData = [ { id: 1, name: 'John', age: 30 }, { id: 2, name: 'Jane', age: 25 }, ]; const columns = [ { key: 'name', label: 'Nome', sortable: true }, { key: 'age', label: 'Idade', sortable: true }, ]; it('renders all columns and rows', () => { render( <DataTablePlugin data={mockData} columns={columns} /> ); expect(screen.getByText('Nome')).toBeInTheDocument(); expect(screen.getByText('Idade')).toBeInTheDocument(); expect(screen.getByText('John')).toBeInTheDocument(); expect(screen.getByText('30')).toBeInTheDocument(); }); it('filters data based on search term', () => { render( <DataTablePlugin data={mockData} columns={columns} /> ); const searchInput = screen.getByPlaceholderText('Pesquisar...'); fireEvent.change(searchInput, { target: { value: 'John' } }); expect(screen.getByText('John')).toBeInTheDocument(); expect(screen.queryByText('Jane')).not.toBeInTheDocument(); }); });
2. Integrationstests
// src/plugins/__tests__/integration.test.tsx import { render, act } from '@testing-library/react'; import { MyCustomPlugin } from '../MyCustomPlugin'; import { usePluginStore } from '../../store/pluginStore'; describe('Plugin Integration', () => { beforeEach(() => { usePluginStore.setState({ plugins: {}, settings: {}, }); }); it('integrates with plugin store', () => { render(<MyCustomPlugin />); act(() => { usePluginStore.getState().togglePlugin('my-custom-plugin'); }); expect( usePluginStore.getState().plugins['my-custom-plugin'] ).toBe(true); }); });
Checkliste für die Umsetzung
Vor der Entwicklung
- Definieren Sie klare Plugin-Anforderungen
- Identifizieren Sie notwendige Abhängigkeiten
- Architektur und Komponenten planen
- Code-Standards festlegen
Während der Entwicklung
- Befolgen Sie die SOLID-Prinzipien
- Unit-Tests implementieren
- Funktionen und Komponenten dokumentieren
- Behalten Sie die Konsistenz mit Shadcn/UI bei
Nachentwicklung
- Integrationstests ausführen
- Überprüfen Sie Barrierefreiheit
- Leistung optimieren
- Dokumentation vorbereiten
Fazit
Die Entwicklung von Plugins für React mit Shadcn/UI, Radix und Lucide-react erfordert einen strukturierten Ansatz und Liebe zum Detail. Zu den Best Practices gehören:
- Modulare Architektur: Wiederverwendbare und gut organisierte Komponenten
- Strong Typing: TypeScript für mehr Sicherheit
- Barrierefreiheit: Radix-Komponenten zur Gewährleistung der Barrierefreiheit
- Visuelle Konsistenz: Integration mit Shadcn/UI und Lucide-react
- Testbarkeit: Angemessene Testabdeckung
Nächste Schritte
- Sehen Sie sich die bereitgestellten Beispiele an
- Passen Sie die Muster an Ihr Projekt an
- Tragen Sie zur Gemeinschaft bei
- Bleiben Sie über die Entwicklungen im Ökosystem auf dem Laufenden
Entwickeln Sie Plugins für React? Teilen Sie Ihre Erfahrungen und Fragen in den Kommentaren unten!
Lesen Sie auch
- Moderne Designsysteme: Erstellen skalierbarer Komponentenbibliotheken mit React und TypeScript
- Best Practices für die Internationalisierung (i18n) in React und Next.js im Jahr 2025
- Next.js App Router: Der Leitfaden für Thinking Server by Default
- Was sind React-Server-Komponenten und warum Logic zurück zum Server wechselt
- Cache und Streaming in Next.js: Leistung wurde zu einer architektonischen Entscheidung
- Mikro-Frontends: Wann und warum sie in skalierbaren Projekten eingesetzt werden sollten
