CtrlK
BlogDocsLog inGet started
Tessl Logo

product-factory/code-quality

Apply this skill when writing, reviewing, or refactoring any code in a React + TypeScript + Tailwind + shadcn + Drizzle project. Triggers on requests like "review this code", "refactor this component", "is this good practice", "write a hook for", "clean this up", "add types to", "write a form", or any time you are producing code that should meet production standards. Use proactively — all generated code should conform to these standards without being asked.

87

Quality

87%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Skills
Evals
Files

SKILL.md

name:
code-quality
description:
Apply this skill when writing, reviewing, or refactoring any code in a React + TypeScript + Tailwind + shadcn + Drizzle project. Triggers on requests like "review this code", "refactor this component", "is this good practice", "write a hook for", "clean this up", "add types to", "write a form", or any time you are producing code that should meet production standards. Use proactively — all generated code should conform to these standards without being asked.

Code Quality: React + TypeScript + Tailwind + shadcn/ui + Drizzle

TypeScript Standards

Never Use any — Use unknown + Type Guards

function processUser(user: unknown): User {
  if (!isUser(user)) throw new Error('Invalid user shape')
  return user
}

function isUser(value: unknown): value is User {
  return typeof value === 'object' && value !== null && 'id' in value && 'email' in value
}

Never Use Type Assertions as a Shortcut

// ❌ Lying to the compiler — blows up at runtime
const user = JSON.parse(body) as User

// ✅ Validate at runtime with Zod
const user = UserSchema.parse(JSON.parse(body))

Infer from Drizzle — Don't Duplicate Types

// ✅ Single source of truth
import { users } from '@/lib/db/schema/users'
type User = typeof users.$inferSelect
type NewUser = typeof users.$inferInsert

// ❌ Duplicating types manually
interface User { id: string; email: string; ... }

Discriminated Unions for State — Not Enums

// ✅ String literal unions — better inference, simpler serialisation
type Status = 'loading' | 'success' | 'error'

type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error }

// ❌ Enums have awkward runtime behaviour and don't serialise cleanly
enum Status { Loading = 'loading', Success = 'success' }

Annotate Non-Trivial Return Types Explicitly

// ✅ Refactors that change the shape are caught immediately
async function getUser(id: string): Promise<User | undefined> {
  return db.query.users.findFirst({ where: eq(users.id, id) })
}

Avoid Over-Engineered Generic Types

If a teammate can't read a type without mentally executing the type system, simplify it.

Use == null for Nullish Checks

value == null is the idiomatic way to check for both null and undefined. Use === everywhere else.


React Component Standards

Component Props Pattern

interface UserCardProps {
  user: User
  onEdit?: (id: string) => void
  className?: string
}

export function UserCard({ user, onEdit, className }: UserCardProps) {
  return (
    <div className={cn('rounded-lg border p-4', className)}>
      <p className="font-medium">{user.name}</p>
      {onEdit && (
        <Button variant="ghost" size="sm" onClick={() => onEdit(user.id)}>Edit</Button>
      )}
    </div>
  )
}

Component Size Rules

  • Single responsibility — one clear purpose per component
  • Hard limit: 300 lines per file (components, hooks, utilities — everything)
  • Custom hooks for any stateful logic beyond simple toggles

Custom Hooks for Data Fetching

export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => fetchUsers(),
    staleTime: 1000 * 60 * 5,
  })
}

export function useCreateUser() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: (data: UserInsert) => createUser(data),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
  })
}

Never Define Components Inside Components

Inline definitions get a new reference every render, causing full remounts.

Never Use Array Index as List Key

Keys must be stable, unique identifiers — use item.id, not index.


Forms — react-hook-form + Zod

Always use this combination. Never manage form state manually with useState. Always use shadcn FormField/FormItem/FormMessage — they handle aria-invalid, aria-describedby, etc.

const schema = z.object({
  email: z.string().email('Invalid email'),
  name: z.string().min(1, 'Name is required').max(255),
})
type FormValues = z.infer<typeof schema>

export function UserForm({ onSubmit }: { onSubmit: (data: FormValues) => void }) {
  const form = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: { email: '', name: '' },
  })

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl><Input placeholder="you@example.com" {...field} /></FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? 'Saving...' : 'Save'}
        </Button>
      </form>
    </Form>
  )
}

Tailwind Standards

Use cn() for Conditional Classes

<div className={cn('rounded-lg border p-4', isActive && 'border-primary bg-primary/5', className)}>

// ❌ String concatenation breaks Tailwind purging
<div className={`rounded-lg ${isActive ? 'border-primary' : ''}`}>

Use cva() for Variant APIs

const badge = cva('inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium', {
  variants: {
    variant: {
      default: 'bg-primary text-primary-foreground',
      secondary: 'bg-secondary text-secondary-foreground',
      destructive: 'bg-destructive text-destructive-foreground',
    },
  },
  defaultVariants: { variant: 'default' },
})

Tailwind Rules

  • Mobile-first: sm:, md:, lg: prefixes always go larger
  • Use design tokens (CSS vars from shadcn) not raw hex values
  • Avoid arbitrary values like w-[347px] unless genuinely necessary
  • Group classes: layout > spacing > typography > colour > interactive > responsive

Drizzle Standards

Select Only What You Need

// ✅ Efficient — only fetch needed columns
const result = await db
  .select({ id: users.id, email: users.email })
  .from(users)
  .where(eq(users.active, true))

// ❌ Fetches all columns
const result = await db.select().from(users)

Transactions for Multi-Step Writes

await db.transaction(async (tx) => {
  const [order] = await tx.insert(orders).values(orderData).returning()
  await tx.insert(orderItems).values(items.map(i => ({ ...i, orderId: order.id })))
})

Use Relations for Joins — Not Multiple Round-Trips

const usersWithPosts = await db.query.users.findMany({
  with: { posts: true },
  where: eq(users.active, true),
})

Always Handle Null From Left Joins

Left join results can be null — type and handle them explicitly.

Use $onUpdateFn for updatedAt — Not Manual Set

export const users = pgTable('users', {
  updatedAt: timestamp('updated_at').$onUpdateFn(() => new Date()),
})

Never Manually Edit Migration Files

Always use drizzle-kit generate and drizzle-kit migrate. Never use drizzle-kit push in production.


useEffect Anti-Patterns

Never Use useEffect for Data Fetching

You're using TanStack Query. A bare useEffect fetch is always wrong.

Never Use useEffect for Derived State

If a value can be calculated from props or state, calculate it — don't store it.

// ❌ Extra render cycle
useEffect(() => { setFullName(`${first} ${last}`) }, [first, last])

// ✅ Plain variable or useMemo
const fullName = `${first} ${last}`
const sortedList = useMemo(() => [...items].sort(), [items])

Never Use useEffect for Event Handling

If something happens because of a user action, handle it in the event handler — not an effect.

Always Clean Up Effects

Any effect that subscribes must return a cleanup function.

Always Include All Dependencies

Never suppress exhaustive-deps. If adding a dependency causes a loop, the architecture is wrong — fix that instead.


TanStack Query Anti-Patterns

Never Sync Query Results Into State

TanStack Query IS your state. Don't copy it into useState or Redux.

// ❌ Double state — cache and useState diverge
const { data } = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
useEffect(() => { if (data) setUsers(data) }, [data])

// ✅ Use query result directly
const { data: users, isLoading, isError } = useQuery({ queryKey: ['users'], queryFn: fetchUsers })

Always Handle All Three States

if (isLoading) return <Skeleton />
if (isError) return <ErrorMessage />
return <UserList users={data} />

Never Call refetch() After Mutations — Use invalidateQueries

onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] })

Use queryOptions() for Reusable Query Definitions

export const userQueryOptions = (id: string) => queryOptions({
  queryKey: ['user', id],
  queryFn: () => fetchUser(id),
  staleTime: 1000 * 60 * 5,
})

Use select to Transform — Not a Separate useMemo

const { data: activeUsers } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  select: (users) => users.filter(u => u.active),
})

Never Use TanStack Query for Client/UI State

UI state belongs in useState, Zustand, or Context.


Error Handling

Server Actions / API Routes

type ActionResult<T> =
  | { success: true; data: T }
  | { success: false; error: string }

export async function createUser(input: unknown): Promise<ActionResult<User>> {
  const parsed = UserInsertSchema.safeParse(input)
  if (!parsed.success) return { success: false, error: parsed.error.issues[0].message }
  try {
    const [user] = await db.insert(users).values(parsed.data).returning()
    return { success: true, data: user }
  } catch (error) {
    console.error('createUser failed', error)
    return { success: false, error: 'Failed to create user' }
  }
}

Always Add Loading States to Async Buttons

<Button onClick={handleSubmit} disabled={mutation.isPending}>
  {mutation.isPending ? 'Saving...' : 'Save'}
</Button>

Wrap route-level components in an ErrorBoundary. Use shadcn Alert for inline errors.

Install with Tessl CLI

npx tessl i product-factory/code-quality@0.2.0

SKILL.md

tile.json