Features
Las features son unidades funcionales de UI que encapsulan componentes, hooks y lógica de presentación para una funcionalidad específica. Son los bloques de construcción visuales que el usuario final ve e interactúa.
Tipos de Features
1. Features Standalone (Independientes)
Funcionalidades transversales que no pertenecen a un dominio específico.
2. Features Modulares
Funcionalidades de UI que pertenecen a un módulo específico y dependen de él para la lógica de negocio.
Feature Standalone: Ejemplo Navbar
Vamos a analizar la Navbar Feature como ejemplo perfecto de una feature standalone. Esta feature maneja la navegación global de la aplicación.
Estructura Completa
src/features/navbar/
├── actions/ # ✅ Server Actions propias
│ └── get_button_text.ts
├── api/ # ✅ Queries y mutations propias
│ ├── queries.ts
│ └── mutations.ts
├── components/ # ✅ Componentes UI
│ ├── root.tsx # Componente principal (Server)
│ ├── logo.tsx # Logo de la aplicación (Server)
│ ├── links.tsx # Links de navegación (Server)
│ ├── nav-actions.tsx # Acciones de navegación (Server)
│ ├── dynamic_items.tsx # Items dinámicos (Server)
│ ├── _navigation_links.tsx # Links interactivos (Client)
│ └── _theme-toggle.tsx # Toggle de tema (Client)
├── constants/ # ✅ Contenido estático
│ ├── content.ts # Textos y contenido
│ └── styles.ts # Estilos y variantes
├── fallbacks/ # ✅ Estados de carga
│ └── navbar_content_fallback.tsx
├── hooks/ # ✅ Hooks específicos
│ ├── useNav.tsx # Lógica de navegación
│ ├── useRenderContent.tsx # Lógica de renderizado
│ └── useThemeToggle.tsx # Lógica del tema
├── store/ # ✅ Estado local
│ └── navbar_store.tsx # Store con Zustand
├── utils/ # ✅ Utilidades
│ └── active_to_classname.ts # Helpers específicos
├── docs.md # ✅ Documentación
├── index.tsx # ✅ Compound Components
└── navbar_feat.tsx # ✅ Integración completaComponentes de la Feature
1. actions/ - Server Actions Propias
Las features standalone pueden tener sus propias server actions para funcionalidades específicas.
// src/features/navbar/actions/navbar.actions.ts
'use server';
export async function getButtonText(buttonType: string) {
// Lógica para obtener texto dinámico de botones
const buttonTexts = {
login: 'Iniciar Sesión',
signup: 'Registrarse',
dashboard: 'Dashboard',
};
return buttonTexts[buttonType] || 'Acción';
}
export async function getNavbarConfigAction() {
// Simula la obtención de datos de una API
const response = await fetch('https://api.example.com/navbar/config');
if (!response.ok) {
throw new Error('Failed to fetch navbar config');
}
return response.json();
}
export async function getUserNavigationAction() {
// Simula la obtención de datos de una API
const response = await fetch('https://api.example.com/user/navigation');
if (!response.ok) {
throw new Error('Failed to fetch user navigation');
}
return response.json();
}
export async function updateNavPreferencesAction(preferences: any) { // Asumiendo 'any' para simplicidad
// Simula el envío de datos a una API
const response = await fetch('https://api.example.com/user/nav-preferences', {
method: 'POST',
body: JSON.stringify(preferences),
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to update nav preferences');
}
return response.json();
}2. api/ - Queries y Mutations
Manejo de estado del servidor usando TanStack Query para datos específicos de la navbar.
// src/features/navbar/api/queries.ts
import { useQuery } from '@tanstack/react-query';
import { getNavbarConfigAction, getUserNavigationAction } from '../actions/navbar.actions'; // Importamos las acciones
export function useNavbarConfig() {
return useQuery({
queryKey: ['navbar-config'],
queryFn: async () => {
return await getNavbarConfigAction(); // Llama a la acción
},
});
}
export function useUserNavigation() {
return useQuery({
queryKey: ['user-navigation'],
queryFn: async () => {
return await getUserNavigationAction(); // Llama a la acción
},
});
}// src/features/navbar/api/mutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateNavPreferencesAction } from '../actions/navbar.actions'; // Importamos la acción
export function useUpdateNavPreferences() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (preferences: NavPreferences) => {
return await updateNavPreferencesAction(preferences); // Llama a la acción
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user-navigation'] });
},
});
}3. components/ - Componentes UI
Server Components (sin prefijo)
// src/features/navbar/components/root.tsx
import { Logo } from './logo';
import { Links } from './links';
import { NavActions } from './nav-actions';
export function NavbarRoot() {
return (
<nav className="navbar">
<Logo />
<Links />
<NavActions />
</nav>
);
}// src/features/navbar/components/logo.tsx
import Link from 'next/link';
import { NAVBAR_CONTENT } from '../constants/content';
export function Logo() {
return (
<Link href="/" className="navbar-logo">
<img src={NAVBAR_CONTENT.logo.src} alt={NAVBAR_CONTENT.logo.alt} />
<span>{NAVBAR_CONTENT.logo.text}</span>
</Link>
);
}Client Components (prefijo _)
// src/features/navbar/components/_navigation_links.tsx
'use client';
import { usePathname } from 'next/navigation';
import { useNav } from '../hooks/useNav';
import { activeToClassname } from '../utils/active_to_classname';
export function NavigationLinks() {
const pathname = usePathname();
const { links } = useNav();
return (
<ul className="nav-links">
{links.map((link) => (
<li key={link.href}>
<a
href={link.href}
className={activeToClassname(pathname === link.href)}
>
{link.label}
</a>
</li>
))}
</ul>
);
}// src/features/navbar/components/_theme-toggle.tsx
'use client';
import { useThemeToggle } from '../hooks/useThemeToggle';
export function ThemeToggle() {
const { theme, toggleTheme } = useThemeToggle();
return (
<button
onClick={toggleTheme}
className="theme-toggle"
aria-label="Cambiar tema"
>
{theme === 'dark' ? '🌙' : '☀️'}
</button>
);
}4. constants/ - Contenido Estático
// src/features/navbar/constants/content.ts
export const NAVBAR_CONTENT = {
logo: {
src: '/logo.svg',
alt: 'Creative Minds Logo',
text: 'Creative Minds',
},
links: [
{ href: '/', label: 'Inicio' },
{ href: '/docs', label: 'Documentación' },
{ href: '/blog', label: 'Blog' },
{ href: '/contact', label: 'Contacto' },
],
actions: {
login: 'Iniciar Sesión',
signup: 'Registrarse',
profile: 'Perfil',
logout: 'Cerrar Sesión',
},
} as const;// src/features/navbar/constants/styles.ts
export const NAVBAR_STYLES = {
root: 'flex items-center justify-between px-6 py-4 bg-white dark:bg-gray-900',
logo: 'flex items-center space-x-2 font-bold text-xl',
links: 'hidden md:flex space-x-6',
link: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white',
activeLink: 'text-blue-600 dark:text-blue-400',
actions: 'flex items-center space-x-4',
button: 'px-4 py-2 rounded-md font-medium',
primaryButton: 'bg-blue-600 text-white hover:bg-blue-700',
secondaryButton: 'border border-gray-300 text-gray-700 hover:bg-gray-50',
} as const;5. hooks/ - Hooks Específicos
// src/features/navbar/hooks/useNav.tsx
'use client';
import { useNavbarStore } from '../store/navbar_store';
import { NAVBAR_CONTENT } from '../constants/content';
export function useNav() {
const { isOpen, toggle, close } = useNavbarStore();
return {
isOpen,
toggle,
close,
links: NAVBAR_CONTENT.links,
};
}// src/features/navbar/hooks/useThemeToggle.tsx
'use client';
import { useState, useEffect } from 'react';
export function useThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark';
if (savedTheme) {
setTheme(savedTheme);
document.documentElement.classList.toggle('dark', savedTheme === 'dark');
}
}, []);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
document.documentElement.classList.toggle('dark', newTheme === 'dark');
};
return { theme, toggleTheme };
}6. store/ - Estado Local
// src/features/navbar/store/navbar_store.tsx
'use client';
import { create } from 'zustand';
interface NavbarState {
isOpen: boolean;
toggle: () => void;
close: () => void;
open: () => void;
}
export const useNavbarStore = create<NavbarState>((set) => ({
isOpen: false,
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
close: () => set({ isOpen: false }),
open: () => set({ isOpen: true }),
}));7. utils/ - Utilidades
// src/features/navbar/utils/active_to_classname.ts
import { NAVBAR_STYLES } from '../constants/styles';
export function activeToClassname(isActive: boolean): string {
return isActive
? `${NAVBAR_STYLES.link} ${NAVBAR_STYLES.activeLink}`
: NAVBAR_STYLES.link;
}8. fallbacks/ - Estados de Carga
// src/features/navbar/fallbacks/navbar_content_fallback.tsx
export function NavbarContentFallback() {
return (
<nav className="navbar animate-pulse">
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gray-300 rounded"></div>
<div className="w-32 h-6 bg-gray-300 rounded"></div>
</div>
<div className="hidden md:flex space-x-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="w-16 h-4 bg-gray-300 rounded"></div>
))}
</div>
<div className="flex space-x-4">
<div className="w-20 h-8 bg-gray-300 rounded"></div>
<div className="w-24 h-8 bg-gray-300 rounded"></div>
</div>
</nav>
);
}9. index.tsx - Compound Components
// src/features/navbar/index.tsx
import { NavbarRoot } from './components/root';
import { Logo } from './components/logo';
import { Links } from './components/links';
import { NavActions } from './components/nav-actions';
import { NavigationLinks } from './components/_navigation_links';
import { ThemeToggle } from './components/_theme-toggle';
// Exportación como Compound Component
export const Navbar = {
Root: NavbarRoot,
Logo,
Links,
Actions: NavActions,
NavigationLinks,
ThemeToggle,
};
// Exportación individual para flexibilidad
export {
NavbarRoot,
Logo,
Links,
NavActions,
NavigationLinks,
ThemeToggle,
};10. navbar_feat.tsx - Integración Completa
// src/features/navbar/navbar_feat.tsx
import { Suspense } from 'react';
import { Navbar } from './index';
import { NavbarContentFallback } from './fallbacks/navbar_content_fallback';
// Feature completa lista para usar
export function NavbarFeature() {
return (
<Suspense fallback={<NavbarContentFallback />}>
<Navbar.Root />
</Suspense>
);
}
// Versión personalizable
export function CustomNavbar({
showThemeToggle = true,
showActions = true
}: {
showThemeToggle?: boolean;
showActions?: boolean;
}) {
return (
<nav className="navbar">
<Navbar.Logo />
<Navbar.NavigationLinks />
<div className="navbar-actions">
{showActions && <Navbar.Actions />}
{showThemeToggle && <Navbar.ThemeToggle />}
</div>
</nav>
);
}Uso de la Feature
En una Página
// app/layout.tsx
import { NavbarFeature } from '@/features/navbar/navbar_feat';
export default function RootLayout({ children }) {
return (
<html>
<body>
<NavbarFeature />
<main>{children}</main>
</body>
</html>
);
}Uso Personalizado
// app/special-layout.tsx
import { Navbar } from '@/features/navbar';
export default function SpecialLayout({ children }) {
return (
<div>
<nav className="custom-navbar">
<Navbar.Logo />
<Navbar.NavigationLinks />
{/* Solo mostrar toggle de tema */}
<Navbar.ThemeToggle />
</nav>
{children}
</div>
);
}Características de Features Standalone
✅ Ventajas
- Autocontenidas: Toda la lógica necesaria está incluida
- Reutilizables: Pueden usarse en cualquier parte de la aplicación
- Flexibles: Compound Components permiten uso granular
- Testables: Cada parte puede testearse independientemente
⚠️ Consideraciones
- Responsabilidad limitada: Solo para funcionalidades transversales
- Evitar duplicación: No crear features standalone para lógica de dominio específico
- Mantener simplicidad: La lógica de negocio debe ser simple
Diferencias con Features Modulares
| Aspecto | Feature Standalone (Navbar) | Feature Modular (Login) |
|---|---|---|
| Ubicación | src/features/navbar/ | src/modules/auth/features/login/ |
| Server Actions | ✅ Propias (actions/) | ❌ Usa las del módulo (@/modules/auth/actions) |
| Estado | ✅ Propio (store/) | ❌ Usa hooks del módulo |
| Lógica de Negocio | ✅ Simple y propia | ❌ Delegada al módulo |
| Caso de Uso | Navegación, layout, UI global | Login, checkout, perfil |
La Navbar Feature es un ejemplo perfecto de cómo una feature standalone puede ser completa, reutilizable y bien estructurada, manteniendo toda su lógica autocontenida mientras proporciona flexibilidad de uso.