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.

69

Quality

87%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files
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.

Workspace
product-factory
Visibility
Public
Created
Last updated
Publish Source
CLI
Badge
product-factory/code-quality badge