Matheus Breguêz (matbrgz)
Desenvolvendo Plugins para React com Shadcn/UI, Radix e Lucide-react: Guia Completo
Desenvolvimento

Desenvolvendo Plugins para React com Shadcn/UI, Radix e Lucide-react: Guia Completo

Índice

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:

  1. Arquitetura Modular: Componentes reutilizáveis e bem organizados
  2. Tipagem Forte: TypeScript para maior segurança
  3. Acessibilidade: Componentes Radix para garantir acessibilidade
  4. Consistência Visual: Integração com Shadcn/UI e Lucide-react
  5. Testabilidade: Cobertura adequada de testes

Próximos Passos

  1. Explore os exemplos fornecidos
  2. Adapte os padrões ao seu projeto
  3. Contribua com a comunidade
  4. 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!

React Shadcn/UI Radix Lucide-react Plugins UI/UX

Compartilhe este artigo

Transforme suas ideias em realidade

Vamos trabalhar juntos para criar soluções inovadoras que impulsionem seu negócio.