Enforce modern React 19 and Next.js App Router patterns - server-first data fetching, minimal useEffect, Server Components, Server Actions, and form hooks. Use when reviewing React/Next.js code, migrating legacy patterns, or building new features with App Router.
Enforce modern React 19 and Next.js App Router patterns by eliminating unnecessary `useEffect` usage and moving data fetching to Server Components. Use when reviewing code, migrating from legacy patterns, or building new features to ensure server-first architecture and proper use of Server Actions.
/plugin marketplace add Tylerbryy/codewarden/plugin install codewarden@codewarden-marketplaceThis skill is limited to using the following tools:
This skill enforces the architectural shift from client-side synchronization to async-native patterns using React 19 and Next.js App Router. The goal is to eliminate unnecessary useEffect usage, move data fetching to the server, and leverage modern primitives like Server Components and Server Actions.
Legacy Era (Pre-2024): Client components mount, then sync with server via useEffect
Modern Era (2025): Server components execute async logic, stream results to client
This shift eliminates:
Activate when:
useEffect-heavy componentsWhen reviewing React/Next.js code:
Identify the stack
package.json for Next.js versionapp/ directory exists (App Router)Scan for code smells
useEffect, useState, useCallback, useMemofetch in Client Components, API routes used for internal datagetServerSideProps, getStaticProps (legacy patterns)Apply the checklists below
Prioritize fixes
useEffect is ONLY for synchronizing with external systems outside React's control.
It is NOT for:
It IS for:
❌ Bad - Effect for derived state:
const [firstName, setFirstName] = useState("John")
const [lastName, setLastName] = useState("Doe")
const [fullName, setFullName] = useState("")
useEffect(() => {
setFullName(`${firstName} ${lastName}`)
}, [firstName, lastName])
✅ Good - Calculate during render:
const [firstName, setFirstName] = useState("John")
const [lastName, setLastName] = useState("Doe")
const fullName = `${firstName} ${lastName}` // Just compute it!
Why: Eliminates an entire render cycle and guarantees consistency.
❌ Bad - Using effect for user actions:
const [submitted, setSubmitted] = useState(false)
useEffect(() => {
if (submitted) {
submitForm(formData)
}
}, [submitted, formData])
// Later:
<button onClick={() => setSubmitted(true)}>Submit</button>
✅ Good - Direct event handler:
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
await submitForm(formData)
}
// Later:
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
Why: User actions should trigger in event handlers, not via state flags.
❌ Bad - Client-side fetch in effect:
"use client"
function ProductsPage() {
const [products, setProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data)
setLoading(false)
})
}, [])
if (loading) return <Spinner />
return <ProductsList products={products} />
}
✅ Good - Server Component with async/await:
// app/products/page.tsx - Server Component (no "use client")
import { db } from '@/lib/db'
export default async function ProductsPage() {
// This runs on the server, directly queries DB
const products = await db.select().from(productsTable)
return <ProductsList products={products} />
}
Why:
✅ Good - External system subscription:
"use client"
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const connection = createConnection(roomId)
connection.connect()
return () => {
connection.disconnect() // Cleanup
}
}, [roomId])
return <div>Connected to {roomId}</div>
}
✅ Good - Browser API sync:
"use client"
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true)
useEffect(() => {
const handleOnline = () => setIsOnline(true)
const handleOffline = () => setIsOnline(false)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
return isOnline
}
When the same effect pattern appears multiple times:
✅ Extract to custom hook:
// hooks/useMediaQuery.ts
"use client"
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false)
useEffect(() => {
const media = window.matchMedia(query)
setMatches(media.matches)
const listener = (e: MediaQueryListEvent) => setMatches(e.matches)
media.addEventListener('change', listener)
return () => media.removeEventListener('change', listener)
}, [query])
return matches
}
// Usage:
const isMobile = useMediaQuery('(max-width: 768px)')
In Next.js App Router (app/ directory):
✅ Default: Server Component (no directive)
// app/dashboard/page.tsx
import { db } from '@/lib/db'
export default async function DashboardPage() {
// Direct database access - runs on server
const stats = await db.query.stats.findFirst()
return <div>Revenue: ${stats.revenue}</div>
}
✅ Opt-in: Client Component (with directive)
"use client" // Only add when you need interactivity
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
| Feature | Server Component | Client Component |
|---|---|---|
| Directive | None (default) | "use client" |
| Data fetching | async/await DB queries | use() or data libraries |
| Interactivity | ❌ No event handlers | ✅ onClick, onChange, etc. |
| React hooks | ❌ No useState, useEffect | ✅ All hooks available |
| Bundle size | ✅ Zero JS shipped | ❌ Increases bundle |
| Browser APIs | ❌ No window, localStorage | ✅ Full access |
| Environment vars | ✅ Private vars safe | ⚠️ Only NEXT_PUBLIC_* |
Problem: Multiple components need the same data. Legacy solution: prop drilling or Context.
Solution: Request memoization with React.cache.
✅ Modern pattern - No prop drilling:
// lib/data.ts
import { cache } from 'react'
import { db } from './db'
// Wrap in cache() for automatic request memoization
export const getCurrentUser = cache(async () => {
return await db.query.users.findFirst({ where: ... })
})
// app/layout.tsx
import { getCurrentUser } from '@/lib/data'
export default async function RootLayout({ children }) {
const user = await getCurrentUser() // Call #1
return (
<html>
<Header user={user} />
{children}
</html>
)
}
// app/profile/page.tsx
import { getCurrentUser } from '@/lib/data'
export default async function ProfilePage() {
const user = await getCurrentUser() // Call #2 - Same request, cached!
return <div>Email: {user.email}</div>
}
Why: Both components call getCurrentUser(), but the DB query executes only once per request. No props, no Context, no drilling.
Pattern: Server Component starts a query, Client Component displays it.
✅ Server Component (initiates fetch):
// app/search/page.tsx
import { db } from '@/lib/db'
import { SearchResults } from './search-results'
export default async function SearchPage() {
// Don't await - pass the promise directly
const resultsPromise = db.query.products.findMany()
return (
<Suspense fallback={<ResultsSkeleton />}>
<SearchResults promise={resultsPromise} />
</Suspense>
)
}
✅ Client Component (unwraps promise):
// app/search/search-results.tsx
"use client"
import { use } from 'react'
import type { Product } from '@/lib/db'
export function SearchResults({ promise }: { promise: Promise<Product[]> }) {
// use() unwraps the promise - suspends until resolved
const results = use(promise)
return (
<ul>
{results.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
Why:
❌ Bad - Can't import Server Component in Client:
"use client"
import { ServerDetails } from './server-details' // ERROR!
export function ClientCard() {
return (
<div onClick={...}>
<ServerDetails /> {/* This won't work */}
</div>
)
}
✅ Good - Pass Server Component as children:
// app/page.tsx (Server Component)
import { ClientCard } from './client-card'
import { ServerDetails } from './server-details'
export default function Page() {
return (
<ClientCard>
<ServerDetails /> {/* Composed in Server Component */}
</ClientCard>
)
}
// client-card.tsx
"use client"
export function ClientCard({ children }: { children: React.ReactNode }) {
return (
<div onClick={...}>
{children} {/* Server Component renders here */}
</div>
)
}
| Use Case | Tool | Why |
|---|---|---|
| Form submission | Server Action | Integrated with React, auto cache revalidation |
| UI mutation (like, delete) | Server Action | Type-safe, works without JS (progressive) |
| Public API endpoint | Route Handler | Mobile app, webhooks, third-party access |
| Webhook receiver | Route Handler | External system, not tied to UI |
Replaces manual useState for form errors and submission.
✅ Server Action:
// actions/auth.ts
"use server"
import { z } from 'zod'
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8)
})
export async function loginAction(prevState: any, formData: FormData) {
// Validate input
const parsed = loginSchema.safeParse({
email: formData.get('email'),
password: formData.get('password')
})
if (!parsed.success) {
return { errors: parsed.error.flatten().fieldErrors }
}
// Perform login
const result = await login(parsed.data)
if (!result.success) {
return { errors: { _form: ['Invalid credentials'] } }
}
redirect('/dashboard')
}
✅ Client Component (form):
"use client"
import { useActionState } from 'react'
import { loginAction } from '@/actions/auth'
export function LoginForm() {
const [state, formAction, isPending] = useActionState(loginAction, null)
return (
<form action={formAction}>
<input name="email" type="email" />
{state?.errors?.email && <span>{state.errors.email}</span>}
<input name="password" type="password" />
{state?.errors?.password && <span>{state.errors.password}</span>}
<button type="submit" disabled={isPending}>
{isPending ? 'Logging in...' : 'Log in'}
</button>
{state?.errors?._form && <span>{state.errors._form}</span>}
</form>
)
}
Key Points:
(prevState, formData) => newStateisPending tracks submission automaticallyAllows any child of a <form> to access the form's pending state.
✅ Extract submit button:
"use client"
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
)
}
✅ Use in form:
<form action={formAction}>
<input name="title" />
<SubmitButton /> {/* Automatically knows form state */}
</form>
Constraint: Must be used in a child component, not the same component that renders <form>.
Updates UI immediately while Server Action processes.
✅ Optimistic like button:
"use client"
import { useOptimistic } from 'react'
import { likePost } from '@/actions/posts'
export function LikeButton({ postId, initialLikes }: Props) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, amount: number) => state + amount
)
return (
<form
action={async () => {
// Update UI instantly
addOptimisticLike(1)
// Server Action runs in background
await likePost(postId)
}}
>
<button type="submit">❤️ {optimisticLikes}</button>
</form>
)
}
Behavior:
❌ Bad - Trusting FormData:
"use server"
export async function updateUser(formData: FormData) {
const email = formData.get('email') // Could be anything!
await db.update(users).set({ email })
}
✅ Good - Zod validation:
"use server"
import { z } from 'zod'
const updateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100)
})
export async function updateUser(formData: FormData) {
const parsed = updateUserSchema.safeParse({
email: formData.get('email'),
name: formData.get('name')
})
if (!parsed.success) {
return { errors: parsed.error.flatten().fieldErrors }
}
await db.update(users).set(parsed.data)
return { success: true }
}
❌ Bad - No auth check:
"use server"
export async function deletePost(postId: string) {
await db.delete(posts).where(eq(posts.id, postId))
}
✅ Good - Auth check first:
"use server"
import { auth } from '@/lib/auth'
export async function deletePost(postId: string) {
const session = await auth()
if (!session?.user) {
throw new Error('Unauthorized')
}
// Verify ownership
const post = await db.query.posts.findFirst({
where: eq(posts.id, postId)
})
if (post.authorId !== session.user.id) {
throw new Error('Forbidden')
}
await db.delete(posts).where(eq(posts.id, postId))
revalidatePath('/posts')
}
❌ Bad - Inline action with closure:
export default async function Page() {
const secretKey = process.env.SECRET_KEY // Sensitive!
async function dangerousAction() {
"use server"
// This closure is encrypted but sent to client
console.log(secretKey) // Risky!
}
return <form action={dangerousAction}>...</form>
}
✅ Good - Module-level action:
// actions/posts.ts
"use server"
export async function createPost(formData: FormData) {
const secretKey = process.env.SECRET_KEY // Stays on server
// Safe - no closure, explicit arguments only
}
// app/page.tsx
import { createPost } from '@/actions/posts'
export default function Page() {
return <form action={createPost}>...</form>
}
Add rate limiting to prevent abuse:
"use server"
import { ratelimit } from '@/lib/rate-limit'
export async function sendEmail(formData: FormData) {
const session = await auth()
if (!session?.user) throw new Error('Unauthorized')
const { success } = await ratelimit.limit(session.user.id)
if (!success) {
return { error: 'Too many requests. Try again later.' }
}
// Proceed with action...
}
❌ Bad - Sequential client fetches:
"use client"
function Dashboard() {
const [user, setUser] = useState(null)
const [posts, setPosts] = useState([])
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser)
}, [])
useEffect(() => {
if (user) {
// Waits for user fetch!
fetch(`/api/posts?userId=${user.id}`).then(r => r.json()).then(setPosts)
}
}, [user])
return <div>...</div>
}
✅ Good - Parallel server fetches:
// app/dashboard/page.tsx
import { db } from '@/lib/db'
export default async function Dashboard() {
// Both queries run in parallel on server
const [user, posts] = await Promise.all([
db.query.users.findFirst(),
db.query.posts.findMany()
])
return <DashboardUI user={user} posts={posts} />
}
✅ Lazy load heavy Client Components:
import dynamic from 'next/dynamic'
const RichTextEditor = dynamic(() => import('@/components/editor'), {
loading: () => <p>Loading editor...</p>,
ssr: false // Don't render on server
})
export default function Page() {
return <RichTextEditor />
}
✅ Keep heavy libraries in Server Components:
// Server Component - markdown lib stays on server
import { marked } from 'marked'
export default async function BlogPost({ slug }: Props) {
const post = await getPost(slug)
const html = marked(post.content) // Runs on server
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
With the React Compiler enabled:
useMemo and useCallbackBefore (manual memoization):
const sortedUsers = useMemo(() => {
return users.sort((a, b) => a.name.localeCompare(b.name))
}, [users])
const handleClick = useCallback(() => {
console.log('clicked')
}, [])
After (React Compiler handles it):
// Just write normal code - compiler optimizes
const sortedUsers = users.sort((a, b) => a.name.localeCompare(b.name))
const handleClick = () => {
console.log('clicked')
}
❌ Legacy class:
class UserProfile extends React.Component {
state = { user: null }
componentDidMount() {
fetchUser().then(user => this.setState({ user }))
}
render() {
return <div>{this.state.user?.name}</div>
}
}
✅ Modern function component:
// Server Component - no hooks needed!
export default async function UserProfile() {
const user = await fetchUser()
return <div>{user.name}</div>
}
❌ Legacy Pages Router:
// pages/products.tsx
export async function getServerSideProps() {
const products = await db.query.products.findMany()
return { props: { products } }
}
export default function ProductsPage({ products }) {
return <ProductsList products={products} />
}
✅ Modern App Router:
// app/products/page.tsx
export default async function ProductsPage() {
const products = await db.query.products.findMany()
return <ProductsList products={products} />
}
Benefits: Simpler, fewer concepts, more composable.
❌ Legacy API route:
// pages/api/users.ts
export default async function handler(req, res) {
if (req.method === 'POST') {
const { name, email } = req.body
const user = await createUser({ name, email })
res.json({ user })
}
}
✅ Modern Server Action:
// actions/users.ts
"use server"
export async function createUserAction(formData: FormData) {
const user = await createUser({
name: formData.get('name'),
email: formData.get('email')
})
revalidatePath('/users')
return { success: true, user }
}
| Scenario | Solution | Key Tool |
|---|---|---|
| Initial page data | Server Component async/await | await db.query |
| Interactive list/form | Client Component + Server Action | useActionState |
| Real-time polling | Client Component + TanStack Query | useQuery({ refetchInterval }) |
| Optimistic update | Client Component | useOptimistic |
| WebSocket/external sub | Client Component | useEffect + cleanup |
| Browser API sync | Custom hook | useSyncExternalStore |
| Public API endpoint | Route Handler | app/api/route.ts |
| Form with validation | Server Action + Zod | useActionState |
| Derived value | Render calculation | const x = a + b |
| User event logic | Event handler | onClick, onSubmit |
When using this skill for code review:
High-level assessment of how modern the codebase is and where the biggest wins are.
A. useEffect and Side Effects
B. Data Fetching and Server Usage
C. Hooks and State Management
D. Component Structure
Prioritized list of actionable items:
❌ Bad:
"use client" // Entire tree is client-side!
export default function Layout({ children }) {
return <div>{children}</div>
}
✅ Good:
// Layout is server component
export default function Layout({ children }) {
return (
<div>
<InteractiveHeader /> {/* Only this is "use client" */}
{children}
</div>
)
}
❌ Bad:
"use client"
import { db } from '@/lib/db' // ERROR: Can't import server code
export function ClientComponent() {
// db is not available in browser!
}
✅ Good:
// Pass data from Server Component as props
export function ClientComponent({ data }: { data: Data }) {
// Use the data here
}
❌ Bad:
"use server"
export async function deleteUser(userId: string) {
await db.delete(users).where(eq(users.id, userId))
// No error handling!
}
✅ Good:
"use server"
export async function deleteUser(userId: string) {
try {
const session = await auth()
if (!session?.user) {
return { error: 'Unauthorized' }
}
await db.delete(users).where(eq(users.id, userId))
revalidatePath('/users')
return { success: true }
} catch (error) {
console.error('Delete user failed:', error)
return { error: 'Failed to delete user' }
}
}
Server Action:
// actions/create-post.ts
"use server"
import { z } from 'zod'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
const createPostSchema = z.object({
title: z.string().min(3).max(100),
content: z.string().min(10)
})
export async function createPostAction(prevState: any, formData: FormData) {
const session = await auth()
if (!session?.user) {
return { errors: { _form: ['You must be logged in'] } }
}
const parsed = createPostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content')
})
if (!parsed.success) {
return { errors: parsed.error.flatten().fieldErrors }
}
try {
await db.insert(posts).values({
...parsed.data,
authorId: session.user.id
})
revalidatePath('/posts')
return { success: true }
} catch (error) {
return { errors: { _form: ['Failed to create post'] } }
}
}
Form Component:
// components/create-post-form.tsx
"use client"
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import { createPostAction } from '@/actions/create-post'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
)
}
export function CreatePostForm() {
const [state, formAction] = useActionState(createPostAction, null)
return (
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="title">Title</label>
<input id="title" name="title" type="text" required />
{state?.errors?.title && (
<p className="error">{state.errors.title}</p>
)}
</div>
<div>
<label htmlFor="content">Content</label>
<textarea id="content" name="content" required />
{state?.errors?.content && (
<p className="error">{state.errors.content}</p>
)}
</div>
{state?.errors?._form && (
<p className="error">{state.errors._form}</p>
)}
{state?.success && <p className="success">Post created!</p>}
<SubmitButton />
</form>
)
}
// components/like-button.tsx
"use client"
import { useOptimistic } from 'react'
import { likePost } from '@/actions/posts'
interface Props {
postId: string
initialLikes: number
userHasLiked: boolean
}
export function LikeButton({ postId, initialLikes, userHasLiked }: Props) {
const [optimisticLikes, addOptimistic] = useOptimistic(
{ likes: initialLikes, liked: userHasLiked },
(state, newLiked: boolean) => ({
likes: state.likes + (newLiked ? 1 : -1),
liked: newLiked
})
)
return (
<form
action={async () => {
const newLiked = !optimisticLikes.liked
addOptimistic(newLiked)
await likePost(postId, newLiked)
}}
>
<button type="submit">
{optimisticLikes.liked ? '❤️' : '🤍'} {optimisticLikes.likes}
</button>
</form>
)
}
Data flows from Server → Client
Mutations flow from Client → Server → Client
Effects are for external systems only
Forms are declarative
<form action={serverAction}> - no event handlersuseActionState for state/errorsuseFormStatus for pending UIuseOptimistic for instant feedbackThis is the modern React architecture: Async-Native, Server-First, Effect-Minimal.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.