Skip to main content
Glama
modern-frontend-2025.md13.5 kB
# Modern Frontend Development 2025 **Updated**: 2025-11-23 | **Stack**: React 19, TypeScript, Vite, TailwindCSS --- ## The Frontend Landscape 2025 ### What Actually Matters ``` ✅ ESSENTIAL: - React 19 (Server Components, Actions) - TypeScript everywhere - Vite (build tool) - TailwindCSS + shadcn/ui - React Query (server state) - Zustand (client state) ❌ OVERHYPED: - CSS-in-JS (performance issues) - Redux (too complex for most apps) - Webpack (Vite is faster) - jQuery (2010s tech) ``` --- ## React 19 Revolution ### Server Components Reality Check ```tsx // ❌ OLD WAY: Client-side data fetching 'use client'; import { useEffect, useState } from 'react'; export default function Posts() { const [posts, setPosts] = useState([]); useEffect(() => { fetch('/api/posts') .then(res => res.json()) .then(setPosts); }, []); return posts.map(post => <PostCard key={post.id} post={post} />); } // Problems: // - Loading state flicker // - SEO issues // - Waterfall requests // - Larger bundle size ``` ```tsx // ✅ NEW WAY: Server Components import { db } from '@/lib/db'; export default async function Posts() { const posts = await db.post.findMany({ orderBy: { createdAt: 'desc' }, take: 20 }); return posts.map(post => <PostCard key={post.id} post={post} />); } // Benefits: // ✓ No loading state needed // ✓ Perfect SEO // ✓ Zero JavaScript to client // ✓ Direct database access ``` ### Server Actions (The Game Changer) ```tsx // app/actions.ts 'use server'; export async function createPost(formData: FormData) { const title = formData.get('title') as string; const content = formData.get('content') as string; // Validation if (!title || title.length < 3) { return { error: 'Title must be at least 3 characters' }; } // Database operation const post = await db.post.create({ data: { title, content, authorId: await getSessionUserId() } }); // Revalidate cache revalidatePath('/posts'); return { success: true, post }; } // app/new-post/page.tsx import { createPost } from '../actions'; export default function NewPost() { return ( <form action={createPost}> <input name="title" required /> <textarea name="content" required /> <button type="submit">Create Post</button> </form> ); } // Magic: // - No API route needed // - Type-safe // - Automatic revalidation // - Progressive enhancement (works without JS!) ``` ### use() Hook Pattern ```tsx import { use, Suspense } from 'react'; async function fetchPost(id: string) { const res = await fetch(`/api/posts/${id}`); return res.json(); } function PostContent({ postPromise }: { postPromise: Promise<Post> }) { const post = use(postPromise); // Suspends until resolved return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> </article> ); } export default function PostPage({ params }: { params: { id: string } }) { // Start fetching immediately (parallel) const postPromise = fetchPost(params.id); const commentsPromise = fetchComments(params.id); return ( <> <Suspense fallback={<PostSkeleton />}> <PostContent postPromise={postPromise} /> </Suspense> <Suspense fallback={<CommentsSkeleton />}> <Comments commentsPromise={commentsPromise} /> </Suspense> </> ); } // Pattern: Start fetching early, suspend when needed ``` --- ## TypeScript Mastery ### Advanced Patterns 2025 ```typescript // Type-safe form handling type FormField<T> = { value: T; error?: string; touched: boolean; }; type FormState<T extends Record<string, any>> = { [K in keyof T]: FormField<T[K]>; }; // Usage type LoginForm = { email: string; password: string; }; const [form, setForm] = useState<FormState<LoginForm>>({ email: { value: '', touched: false }, password: { value: '', touched: false } }); // Type-safe API responses type ApiResponse<T> = | { success: true; data: T } | { success: false; error: string }; async function fetchUser(id: number): Promise<ApiResponse<User>> { try { const res = await fetch(`/api/users/${id}`); const data = await res.json(); return { success: true, data }; } catch (error) { return { success: false, error: error.message }; } } // Discriminated unions for state type LoadingState<T> = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: string }; function UserProfile({ userId }: { userId: number }) { const [state, setState] = useState<LoadingState<User>>({ status: 'idle' }); // TypeScript knows exact shape based on status if (state.status === 'success') { return <div>{state.data.name}</div>; // state.data exists } if (state.status === 'error') { return <div>Error: {state.error}</div>; // state.error exists } return <div>Loading...</div>; } ``` --- ## State Management 2025 ### React Query for Server State ```typescript import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // Query function PostsList() { const { data, isLoading, error } = useQuery({ queryKey: ['posts'], queryFn: async () => { const res = await fetch('/api/posts'); return res.json(); }, staleTime: 5 * 60 * 1000, // 5 minutes cacheTime: 10 * 60 * 1000 // 10 minutes }); if (isLoading) return <Skeleton />; if (error) return <Error />; return data.map(post => <PostCard key={post.id} post={post} />); } // Mutation with optimistic update function LikeButton({ postId }: { postId: number }) { const queryClient = useQueryClient(); const { mutate, isPending } = useMutation({ mutationFn: async () => { await fetch(`/api/posts/${postId}/like`, { method: 'POST' }); }, onMutate: async () => { // Cancel outgoing queries await queryClient.cancelQueries({ queryKey: ['posts', postId] }); // Snapshot previous value const previous = queryClient.getQueryData(['posts', postId]); // Optimistically update queryClient.setQueryData(['posts', postId], (old: Post) => ({ ...old, likes: old.likes + 1 })); return { previous }; }, onError: (err, variables, context) => { // Rollback on error queryClient.setQueryData(['posts', postId], context?.previous); }, onSettled: () => { // Refetch after error or success queryClient.invalidateQueries({ queryKey: ['posts', postId] }); } }); return ( <button onClick={() => mutate()} disabled={isPending}> Like {isPending && '...'} </button> ); } ``` ### Zustand for Client State ```typescript import { create } from 'zustand'; import { persist } from 'zustand/middleware'; type CartStore = { items: CartItem[]; addItem: (item: CartItem) => void; removeItem: (id: string) => void; clearCart: () => void; total: () => number; }; const useCartStore = create<CartStore>()( persist( (set, get) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (id) => set((state) => ({ items: state.items.filter(item => item.id !== id) })), clearCart: () => set({ items: [] }), total: () => get().items.reduce((sum, item) => sum + item.price, 0) }), { name: 'cart-storage' } // Persists to localStorage ) ); // Usage function Cart() { const { items, removeItem, total } = useCartStore(); return ( <div> {items.map(item => ( <div key={item.id}> {item.name} - ${item.price} <button onClick={() => removeItem(item.id)}>Remove</button> </div> ))} <div>Total: ${total()}</div> </div> ); } ``` --- ## Performance Optimization ### Code Splitting Done Right ```typescript import { lazy, Suspense } from 'react'; // ❌ BAD: Import everything import AdminPanel from './AdminPanel'; import UserDashboard from './UserDashboard'; // ✅ GOOD: Lazy load heavy components const AdminPanel = lazy(() => import('./AdminPanel')); const UserDashboard = lazy(() => import('./UserDashboard')); function App() { const { user } = useAuth(); return ( <Suspense fallback={<Loading />}> {user.role === 'admin' ? <AdminPanel /> : <UserDashboard />} </Suspense> ); } // Advanced: Preload on hover function Navigation() { const preloadAdmin = () => import('./AdminPanel'); return ( <nav> <Link to="/admin" onMouseEnter={preloadAdmin} // Preload on hover > Admin </Link> </nav> ); } ``` ### Virtual Scrolling for Large Lists ```typescript import { useVirtualizer } from '@tanstack/react-virtual'; function LargeList({ items }: { items: Item[] }) { const parentRef = useRef<HTMLDivElement>(null); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, // Estimated row height overscan: 5 // Render 5 extra items for smooth scrolling }); return ( <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}> {virtualizer.getVirtualItems().map((virtualRow) => ( <div key={virtualRow.index} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualRow.size}px`, transform: `translateY(${virtualRow.start}px)` }} > <ItemCard item={items[virtualRow.index]} /> </div> ))} </div> </div> ); } // Renders only visible items (e.g., 12 out of 10,000) ``` --- ## Accessibility (A11y) ### Keyboard Navigation ```typescript function Dropdown() { const [isOpen, setIsOpen] = useState(false); const [focusedIndex, setFocusedIndex] = useState(0); const itemsRef = useRef<(HTMLButtonElement | null)[]>([]); const handleKeyDown = (e: KeyboardEvent) => { if (!isOpen) { if (e.key === 'Enter' || e.key === ' ') { setIsOpen(true); } return; } switch (e.key) { case 'ArrowDown': e.preventDefault(); setFocusedIndex((i) => (i + 1) % items.length); break; case 'ArrowUp': e.preventDefault(); setFocusedIndex((i) => (i - 1 + items.length) % items.length); break; case 'Enter': case ' ': e.preventDefault(); items[focusedIndex].onClick(); setIsOpen(false); break; case 'Escape': setIsOpen(false); break; } }; useEffect(() => { if (isOpen) { itemsRef.current[focusedIndex]?.focus(); } }, [focusedIndex, isOpen]); return ( <div onKeyDown={handleKeyDown}> <button onClick={() => setIsOpen(!isOpen)} aria-haspopup="true" aria-expanded={isOpen}> Menu </button> {isOpen && ( <div role="menu"> {items.map((item, index) => ( <button key={item.id} ref={(el) => (itemsRef.current[index] = el)} role="menuitem" tabIndex={focusedIndex === index ? 0 : -1} onClick={item.onClick} > {item.label} </button> ))} </div> )} </div> ); } ``` --- ## Testing Strategy ### Vitest + React Testing Library ```typescript import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; describe('LoginForm', () => { it('submits form with valid credentials', async () => { const onSubmit = vi.fn(); const user = userEvent.setup(); render(<LoginForm onSubmit={onSubmit} />); // Type in fields await user.type(screen.getByLabelText(/email/i), 'user@example.com'); await user.type(screen.getByLabelText(/password/i), 'password123'); // Submit await user.click(screen.getByRole('button', { name: /sign in/i })); // Assert await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ email: 'user@example.com', password: 'password123' }); }); }); it('shows error for invalid email', async () => { render(<LoginForm />); const emailInput = screen.getByLabelText(/email/i); await userEvent.type(emailInput, 'invalid-email'); await userEvent.tab(); // Blur expect(await screen.findByText(/invalid email/i)).toBeInTheDocument(); }); }); ``` --- ## Key Takeaways 1. **React 19 is a paradigm shift** - Server Components + Actions eliminate most APIs 2. **TypeScript everywhere** - Type safety saves hours of debugging 3. **React Query** - Perfect for server state (fetching, caching, sync) 4. **Zustand** - Simple client state (UI, temp data) 5. **Performance matters** - Code splitting, virtual scrolling, memoization 6. **Accessibility is non-negotiable** - Keyboard nav, ARIA, screen readers --- ## References - React 19 Documentation - TypeScript Handbook 5.9 - "Testing JavaScript" - Kent C. Dodds - Web.dev Performance Guide **Related**: `react-patterns.md`, `typescript-advanced.md`, `performance.md`

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/seanshin0214/persona-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server