Em 2025, o ecossistema React evoluiu significativamente, com Shadcn/UI e Radix estabelecendo-se como padrões de facto para desenvolvimento de interfaces modernas e acessíveis. Este guia explora as melhores práticas para desenvolver plugins personalizados que se integram perfeitamente com esse ecossistema.
Fundamentos e Arquitetura
Estrutura Recomendada de Plugins
// 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', }; }, });
Sistema de Tipos Forte
// 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>; }
Integrando com Shadcn/UI
Componente Base Personalizado
// 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";
Hooks Personalizados
// 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 }), }));
Integrando com Radix Primitives
Componentes Acessíveis
// 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> ); };
Integrando com Lucide-react
Sistema de Ícones Personalizado
// 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} />; };
Melhores Práticas
1. Gerenciamento de Estado
// 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. Sistema de Temas
// 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. Sistema de Eventos
// 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();
Exemplo de Plugin Completo
// 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> ); }
Testes e Qualidade
1. Testes Unitários
// 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. Testes de Integração
// 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); }); });
Checklist de Implementação
Antes do Desenvolvimento
- Definir requisitos claros do plugin
- Identificar dependências necessárias
- Planejar arquitetura e componentes
- Estabelecer padrões de código
Durante o Desenvolvimento
- Seguir princípios SOLID
- Implementar testes unitários
- Documentar funções e componentes
- Manter consistência com Shadcn/UI
Pós-Desenvolvimento
- Executar testes de integração
- Verificar acessibilidade
- Otimizar performance
- Preparar documentação
Conclusão
O desenvolvimento de plugins para React usando Shadcn/UI, Radix e Lucide-react requer uma abordagem estruturada e atenção aos detalhes. As melhores práticas incluem:
- Arquitetura Modular: Componentes reutilizáveis e bem organizados
- Tipagem Forte: TypeScript para maior segurança
- Acessibilidade: Componentes Radix para garantir acessibilidade
- Consistência Visual: Integração com Shadcn/UI e Lucide-react
- Testabilidade: Cobertura adequada de testes
Próximos Passos
- Explore os exemplos fornecidos
- Adapte os padrões ao seu projeto
- Contribua com a comunidade
- Mantenha-se atualizado com as evoluções do ecossistema
Está desenvolvendo plugins para React? Compartilhe suas experiências e dúvidas nos comentários abaixo!
Leia também
- Design Systems Modernos: Construindo Component Libraries Escaláveis com React e TypeScript
- Boas Práticas de Internacionalização (i18n) em React e Next.js em 2025
- Next.js App Router: o Guia para Pensar em Servidor por Padrão
- O Que São React Server Components e Por Que a Lógica Está Voltando para o Servidor
- Cache e streaming no Next.js: performance virou decisão de arquitetura
- Micro-frontends: Quando e Por Que Adotar em Projetos Escaláveis