Desenvolvendo Plugins para React com Shadcn/UI, Radix e Lucide-react
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!