Builder helpers

.logic()

Colocate pure setup logic with your classmate component and expose the result to the template, variants and DOM output.

What is a logic header?

Call .logic() before the template literal to run arbitrary JavaScript on every render. The function receives the current props and can return additional props that should be merged back in. Anything prefixed with $ is stripped from the DOM but stays available for interpolations and variants.

import rc from "react-classmate"

type DayStatus = "completed" | "skipped" | "pending"

interface WorkoutProps {
  workouts: number
  completed: number
  skipped: number
  $status?: DayStatus
}

export const WorkoutDay = rc.div
  .logic<WorkoutProps>((props) => {
    if (props.completed === props.workouts) {
      return { $status: "completed" }
    }
    if (props.skipped > 0) {
      return { $status: "skipped" }
    }
    return { $status: "pending" }
  })
  .variants<WorkoutProps, { $status: DayStatus }>({
    base: "rounded-md border p-4 transition-colors",
    variants: {
      $status: {
        completed: "border-green-500 bg-green-50",
        skipped: "border-orange-400 bg-orange-50",
        pending: "border-slate-300 bg-white",
      },
    },
    defaultVariants: {
      $status: "pending",
    },
  })

Logic handlers must stay pure. They cannot use hooks or render JSX. Think of them as a small “header” that derives data for the actual component.

Compose multiple logic blocks

You can chain as many logic handlers as you need. Later handlers receive the props returned by previous ones which makes it trivial to set derived variant props, data-* attributes or accessibility metadata in one place.

import rc from "react-classmate"

interface NotificationProps {
  events: { type: "error" | "info" }[]
  $severity?: "idle" | "error"
  $hasErrors?: boolean
}

export const NotificationBadge = rc.button
  .logic<NotificationProps>((props) => {
    const hasErrors = props.events.some((event) => event.type === "error")
    return {
      $hasErrors: hasErrors,
      $severity: hasErrors ? "error" : "idle",
    }
  })
  .logic<NotificationProps>((props) => ({
    ["aria-live"]: props.$hasErrors ? "assertive" : "polite",
    ["data-has-errors"]: props.$hasErrors ? "true" : "false",
  }))
  .variants<NotificationProps, { $severity: "idle" | "error" }>({
    base: `
      inline-flex items-center gap-2 rounded-full px-4 py-1.5 text-sm
      border transition-colors duration-150
    `,
    variants: {
      $severity: {
        idle: "border-slate-300 bg-white text-slate-900",
        error: "border-red-500 bg-red-50 text-red-900",
      },
    },
  })

Why chain?

Chaining keeps concerns focused: derive your computed values in the first handler, attach DOM attributes in the second, etc. The logic stack executes in order, so later handlers can override anything from earlier ones if necessary.

Guidelines

  • Always return plain objects. Anything else is ignored. Use $ prefixes for props that should never reach the DOM.
  • Combine logic headers with rc.extend or useClassmate to reuse the same derived data in different contexts.
  • When you need heavy computations, memoize or pre-calculate the inputs before passing them into the logic handler to keep the render path fast.