Skip to Content

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 completa

Componentes 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

AspectoFeature Standalone (Navbar)Feature Modular (Login)
Ubicaciónsrc/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 UsoNavegación, layout, UI globalLogin, 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.

Last updated on