Skip to content

Components and props (defineProps energy) ​

A React component is a function. Props are the arguments. That's the whole pitch.

A component with props ​

Vue:

vue
<script setup lang="ts">
defineProps<{ title: string; count?: number }>()
</script>
<template>
  <h2>{{ title }} ({{ count ?? 0 }})</h2>
</template>

React:

tsx
type CardProps = {
  title: string
  count?: number
}

function Card({ title, count = 0 }: CardProps) {
  return (
    <h2>
      {title} ({count})
    </h2>
  )
}

Comments for the code above:

  • Type the props object - same idea as defineProps<{…}>()
  • Default values via destructuring (count = 0) instead of withDefaults
  • No props.title required if you destructure (you can keep props if you prefer)

children ≈ default slot ​

tsx
type PanelProps = {
  title: string
  children: React.ReactNode
}

function Panel({ title, children }: PanelProps) {
  return (
    <section>
      <h3>{title}</h3>
      <div>{children}</div>
    </section>
  )
}

// usage
<Panel title="Notes">
  <p>Anything you put here is children.</p>
</Panel>

As you can see - that's your default <slot />. Named slots don't have a first-class twin. People usually:

  • Pass extra props that are React nodes: footer={<Footer />}
  • Or split into smaller components
  • Or use a render prop: renderFooter={() => <Footer />}

One-way data flow (same story) ​

Parent owns state. Child gets values + callbacks:

tsx
function Parent() {
  const [open, setOpen] = useState(false)
  return (
    <Modal
      open={open}
      onClose={() => setOpen(false)}
    />
  )
}

type ModalProps = {
  open: boolean
  onClose: () => void
}

function Modal({ open, onClose }: ModalProps) {
  if (!open) return null
  return (
    <div className="modal">
      <button type="button" onClick={onClose}>
        Close
      </button>
    </div>
  )
}

Comments for the code above:

  • v-model doesn't exist as magic - you pass value + onChange / open + onClose
  • Naming onSomething for callbacks is the community habit (like Vue's emit('close'))

Export styles ​

tsx
// default export - common for page-level components
export default function App() { /* … */ }

// named exports - nice for shared UI
export function Button() { /* … */ }

I've been leaning named exports more lately - easier refactors, better autocomplete. Not a religion though.

File naming ​

Not enforced. Common patterns:

  • UserCard.tsx for the component
  • useUser.ts for a custom hook
  • Colocate: UserCard.tsx + UserCard.module.css

Next: state - useState and friends. This is where Vue ref muscle memory gets remapped.

Best,

-- ł.