Senior frontend
Koleksi 20 Claude Skills siap pakai untuk pengembangan SaaS, web modern, dan praktik rekayasa perangkat lunak tingkat lanjut.
npx -y skills add roedyrustam/claudevibeskills --skill senior-frontendAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Expert frontend engineering skill for React 19, Next.js 15, and Tailwind CSS v4. Use this skill whenever the user is building, debugging, or reviewing frontend code — React components, server components, client components, hooks, forms, routing, layouts, data fetching, bundle optimization, or anything related to the modern React/Next.js ecosystem. Trigger on any mention of React, Next.js, Tailwind, useActionState, useOptimistic, useFormStatus, Server Components (RSC), Turbopack, App Router, or frontend performance. Also trigger for questions about component architecture, hydration errors, code-splitting, or image/font optimization.
SKILL.md
12.5 KB, as published. Nobody here has run it
Senior Frontend Engineer Skill
Expert guidance for modern React 19 + Next.js 15 + Tailwind CSS v4 development.
Core Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| UI Library | React 19 |
| Styling | Tailwind CSS v4 |
| Language | TypeScript 5.x (strict) |
| Bundler | Turbopack (dev), webpack (prod fallback) |
| Package Manager | pnpm or Bun |
React 19 Patterns
New Hooks — Use These, Not the Old Equivalents
useActionState (replaces useFormState)
'use client'
import { useActionState } from 'react'
import { submitForm } from '@/app/actions'
type State = { error?: string; success?: boolean }
export function ContactForm() {
const [state, formAction, isPending] = useActionState<State, FormData>(
submitForm,
{ error: undefined, success: false }
)
return (
<form action={formAction}>
<input name="email" type="email" required />
{state.error && <p className="text-red-500 text-sm">{state.error}</p>}
<SubmitButton />
</form>
)
}
useOptimistic — Instant UI feedback
'use client'
import { useOptimistic, useTransition } from 'react'
type Todo = { id: string; text: string; done: boolean }
export function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
)
const [isPending, startTransition] = useTransition()
const handleAdd = (text: string) => {
const tempTodo = { id: crypto.randomUUID(), text, done: false }
startTransition(async () => {
addOptimistic(tempTodo)
await createTodo(text)
})
}
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} className={todo.done ? 'line-through opacity-50' : ''}>
{todo.text}
</li>
))}
</ul>
)
}
useFormStatus — Submit button state
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton({ label = 'Submit' }: { label?: string }) {
const { pending } = useFormStatus()
return (
<button
type="submit"
disabled={pending}
className="btn-primary disabled:opacity-60 disabled:cursor-not-allowed"
>
{pending ? 'Loading…' : label}
</button>
)
}
React 19 use() Hook
import { use, Suspense } from 'react'
// Pass a Promise directly to use()
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise) // suspends until resolved
return <div>{user.name}</div>
}
// Usage: wrap in Suspense
export default function Page() {
const userPromise = fetchUser(123)
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}
Next.js 15 App Router Patterns
File Structure
app/
├── layout.tsx # Root layout (server component)
├── page.tsx # Home page
├── globals.css # Tailwind CSS v4 entry
├── (marketing)/ # Route group (no URL segment)
│ ├── layout.tsx
│ └── about/page.tsx
├── (app)/ # Protected route group
│ ├── layout.tsx # Auth guard here
│ └── dashboard/
│ ├── page.tsx
│ └── loading.tsx # Automatic Suspense boundary
├── api/
│ └── [...route]/
│ └── route.ts # Route handlers
└── _components/ # Private folder (not routable)
Server vs Client Components
Rule: Default to Server Components. Add 'use client' only when needed.
| Need | Use |
|---|---|
| Data fetching, DB access, secrets | Server Component |
useState, useEffect, event handlers | Client Component |
| Browser APIs (window, localStorage) | Client Component |
| Animations, gesture handling | Client Component |
| Everything else | Server Component |
// ✅ Server Component — no directive needed
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: params.id } })
if (!product) notFound()
return (
<div>
<h1>{product.name}</h1>
{/* Pass serializable data to client */}
<AddToCartButton productId={product.id} />
</div>
)
}
Data Fetching in Server Components
// Next.js 15: fetch() is no longer cached by default!
// Must explicitly opt-in to caching.
// No cache (default in Next.js 15)
const data = await fetch('https://api.example.com/data')
// Cache permanently (like old Next.js default)
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache'
})
// Revalidate every N seconds
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
// Tag-based revalidation
const data = await fetch('https://api.example.com/data', {
next: { tags: ['products'] }
})
// Then call: revalidateTag('products') in a Server Action
Server Actions
// app/actions.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'
const schema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
})
export async function createPost(prevState: unknown, formData: FormData) {
const parsed = schema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
})
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors }
}
try {
await db.post.create({ data: parsed.data })
revalidateTag('posts')
revalidatePath('/blog')
} catch {
return { error: { _form: ['Database error. Please try again.'] } }
}
redirect('/blog') // Throws internally — don't wrap in try/catch
}
Parallel Routes & Intercepting Routes
app/
├── @modal/ # Parallel slot
│ └── (.)photo/[id]/
│ └── page.tsx # Intercepted route (modal)
├── layout.tsx # Receives { children, modal }
└── photo/[id]/
└── page.tsx # Full page (on hard refresh)
// app/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{children}
{modal}
</>
)
}
Tailwind CSS v4
Setup — CSS-First Configuration
/* app/globals.css */
@import "tailwindcss";
/* Define theme in CSS, not tailwind.config.js */
@theme {
--color-brand-50: oklch(0.97 0.02 260);
--color-brand-500: oklch(0.55 0.22 260);
--color-brand-900: oklch(0.25 0.12 260);
--font-sans: "Inter Variable", sans-serif;
--font-mono: "JetBrains Mono", monospace;
--radius-card: 0.75rem;
--shadow-card: 0 4px 24px oklch(0 0 0 / 0.08);
--animate-fade-in: fade-in 0.2s ease-out;
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
v4 Breaking Changes to Know
// ❌ v3 — JIT config in tailwind.config.js
// ✅ v4 — everything in CSS via @theme
// ❌ v3
<div className="bg-opacity-50">
// ✅ v4 — use slash syntax
<div className="bg-black/50">
// ❌ v3 — ring-offset
<div className="ring-2 ring-offset-2">
// ✅ v4 — outline or shadow instead
// New: arbitrary values with CSS vars
<div className="text-[var(--color-brand-500)]">
Component Patterns with Tailwind v4
// Use cva for variant-based components
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-brand-500 text-white hover:bg-brand-600',
outline: 'border border-brand-500 text-brand-500 hover:bg-brand-50',
ghost: 'hover:bg-brand-50 text-brand-500',
destructive: 'bg-red-500 text-white hover:bg-red-600',
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base',
},
},
defaultVariants: { variant: 'default', size: 'md' },
}
)
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button className={cn(buttonVariants({ variant, size }), className)} {...props} />
)
}
Performance Optimization
Image Optimization
import Image from 'next/image'
// Always specify width/height or fill
<Image
src="/hero.webp"
alt="Hero image"
width={1200}
height={630}
priority // LCP image — load eagerly
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
// Dynamic images — use fill + sized container
<div className="relative aspect-video w-full">
<Image
src={post.coverImage}
alt={post.title}
fill
className="object-cover rounded-lg"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
Bundle Optimization
// Dynamic imports for heavy components
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <ChartSkeleton />,
ssr: false, // Client-only components
})
const RichEditor = dynamic(() => import('@/components/RichEditor'), {
loading: () => <EditorSkeleton />,
})
Font Optimization
// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter', // Use as CSS variable
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.variable}>
<body className="font-sans">{children}</body>
</html>
)
}
Metadata & SEO
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.ogImage, width: 1200, height: 630 }],
},
alternates: {
canonical: `https://example.com/blog/${params.slug}`,
},
}
}
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({ slug: post.slug }))
}
Error Handling Patterns
// error.tsx — must be 'use client'
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
// Log to Sentry, Datadog, etc.
}, [error])
return (
<div className="flex flex-col items-center gap-4 p-8">
<h2 className="text-xl font-semibold">Something went wrong</h2>
<p className="text-muted-foreground text-sm">{error.message}</p>
<button onClick={reset} className="btn-primary">Try again</button>
</div>
)
}
Key Rules (Always Follow)
- Never use
any— useunknownand narrow types, or proper generics - Server Components by default — only
'use client'when necessary - Validate all Server Action inputs with Zod before touching DB
- Never
redirect()inside try/catch — it throws internally - Use
next/imagefor all<img>tags - Use
next/font— never<link>to Google Fonts directly - Suspense boundaries around async data fetching trees
loading.tsxfor route-level skeletons (automatic Suspense)error.tsxfor route-level error recovery (must be'use client')not-found.tsxfor 404 states — callnotFound()from server components