QUI React

React Component

In the previous example, we set up our Tag's classes and TypeScript definitions. Now we'll implement the corresponding React component.

React components are located in the @qui/react package at packages/react.

react
src
components
accordion
button
etc...

This guide will focus on the Tag component.

react
src
components
tag
index.ts
q-tag.tsx

Not a lot to unpack here, so we'll focus on the component itself. For the comments in the code that start with a number (e.g. /** 1. Default HTML Element */), refer to the corresponding section of the Detailed Breakdown.

q-tag.tsx

import {ElementType, MouseEvent, SyntheticEvent} from "react"
import {X} from "lucide-react"
import {QTagBaseInput, QTagClasses, QTagSize} from "@qui/base"
import {useControlledId, useControlledState} from "../../hooks"
import {As, PolymorphicComponentPropsWithRef} from "../../system"
import {QIcon} from "../icon"
/* 1. Default HTML Element */
type DefaultAs = "button"
/*
* 2. Input Properties. Must be tagged with `@public` and @interface to be
* picked up by our documentation props parser.
*/
/**
* @public
* @interface
*/
export type QTagProps<C extends As = DefaultAs> =
PolymorphicComponentPropsWithRef<
C,
QTagBaseInput & {
/**
* The component used for the root node. It can be a React component or
* element.
*
* @default 'button'
*/
as?: C
/**
* Callback fired when user clicks on the tag when {@link toggleable} is
* `true`.
*/
onChange?: (event: SyntheticEvent, value: boolean) => void
/**
* Callback fired when user clicks on the tag when {@link dismissable} is
* `true` and {@link toggleable} is `true`.
*/
onDismiss?: (event: MouseEvent, value: boolean) => void
/**
* If true, the component is read-only.
*
* @default false
*/
readOnly?: boolean
/**
* The size of the tag.
*
* @default 'm'
*/
size?: QTagSize
/**
* Use the stretch property in combination with CSS flexbox to evenly
* distribute the tabs based on the available horizontal space.
*
* @default false
*/
stretch?: boolean
/**
* If true, the component is toggleable between on/off states.
*
* @default false
*/
toggleable?: boolean
}
>
/* 3. function declaration with alias */
export function QTag<C extends ElementType = DefaultAs>({
as,
checked: checkedProp,
defaultValue,
disabled = false,
dismissable = false,
id: idProp,
label,
onChange: onChangeProps,
onDismiss,
readOnly = false,
ref,
size = "m",
stretch = false,
tabIndex,
toggleable = false,
...props
}: QTagProps<C>) {
/* 4. Polymorphic Element */
const Element = as || "button"
/* 5. Unique Identifier */
const id = useControlledId(idProp)
/* 6. Controlled State */
const [checked, setChecked] = useControlledState<boolean>({
controlled: checkedProp,
defaultValue,
name: "QTag",
})
const onClick = (event: MouseEvent) => {
if (toggleable) {
if (onChangeProps) {
onChangeProps(event, checked ?? false)
}
setChecked(!checked)
} else if (dismissable && onDismiss) {
onDismiss(event, true)
}
}
return (
<Element
ref={ref}
className={QTagClasses.root(
size,
checked ?? false,
readOnly,
disabled,
stretch,
)}
disabled={disabled}
id={id}
onClick={onClick}
tabIndex={tabIndex}
{...props}
>
<span className={QTagClasses.label()}>{label}</span>
{dismissable && !toggleable && !readOnly ? (
<QIcon className={QTagClasses.closeIcon()} icon={X} size="xs" />
) : null}
</Element>
)
}
/** 7. Display Name */
QTag.displayName = "QTag"

Detailed Breakdown

1. Default HTML Element

Most of our components are polymorphic. This means that the root node renders a default HTML element that can be configured. For example, the Tag uses the button element by default, but can be changed to another element by passing a different value to the as prop:

<QTag as="a" href="some-link.com" />

We achieve this through a few utility types which dynamically infer the passed tag's properties. It even provides accurate intellisense based on the passed element.

Note that some properties, like onChange, are typically overridden.

2. Input Properties

Our component props are defined as TypeScript interfaces. We decorate each input prop with JSDoc comments including a description and @default, where appropriate.

We've developed a JSDoc parser that transforms our interfaces into usable documentation.

Guidelines for Polymorphic Components

  1. Declare a default HTML element. For most components this will be a div or span. For interactive components, this is typically a button.

    type DefaultAs = "button"
  2. Use the polymorphic type aliases to define the component's interfaces. These type aliases enable us to define strongly-typed React components that can inherit props from arbitrary HTML elements. You can ignore the JSDoc tags for now, as we'll cover them in the final section: React Documentation.

    export type QTagProps<C extends As = DefaultAs> =
    PolymorphicComponentPropsWithRef<
    C,
    {
    /**
    * The component used for the root node. It can be a React component or an HTML element.
    *
    * @default 'button'
    */
    as?: C
    // ...
    }
    >

3. Function declaration with alias

NOTE

As of React 19, ref is now included as a prop on function components. Read more about refs in the official documentation.

Each component is exported as a function with a Generic type argument. This argument is inferred from the value of the as prop.

export function QTag<C extends ElementType = DefaultAs>({
ref,
...props
}: QTagProps<C>) {
// ...
}

4. Polymorphic Element

The as prop is used to customize the component's root element.

export function QTag<C extends ElementType = DefaultAs>({
as,
ref,
...props
}: QTagProps<C>) {
/**
* The component used for the root node.
* It can be a React component or an HTML element.
*/
const Element = as || "button"
return (
<Element ref={ref} {...props}>
{/* ...omitted for brevity */}
</Element>
)
}

5. Unique Identifier

Some of our components require a unique identifier for accessibility (i.e. aria-labelledby). The useControlledId hook uses the id prop if supplied, and falls back to a random unique ID if the id prop is not supplied.

6. Controlled State

Our components follow the recommended conventions for controlling state. The gist is this:

  • Each component with optional controlled state defines at least three properties:
    • an onChange function
    • a "controlled" value
    • a "default" value for specifying the initial uncontrolled value.
  • When the controlled property is undefined, the component is assumed to be in uncontrolled mode.
  • When the controlled property is defined, the component is assumed to be in controlled mode.

Our components combine the best of both strategies: by default, they're uncontrolled. If the user passes in the controlled property, they become controlled and refer to their controlled value.

When a component is controlled, the state you pass in is the single source of truth. Any changes to the component's state must be made through the state passed in as props. Internal change events will trigger the corresponding onChange callback, but you will need to update the controlled state in response to these events. In other words, the component's state is not allowed to change unless the state passed in as props changes.

These guidelines ensure that each state has a single source of truth. This makes the component more predictable and easier to work with. Refer to the official documentation on this subject for more information.

7. Display Name

The displayName property in React is used primarily for debugging purposes. It helps you identify components in the React Developer Tools. Function components do not automatically receive a displayName, so it's best to set this explicitly.

Barrel Files

Also note that each component contains an index.ts barrel file. When you add a new component, ensure that you include this file.

src/components/tag/index.ts

export * from "./q-tag"

Also ensure that you update the src/components/index.ts file with the appropriate export. Otherwise, the new component won't be exported by the bundler.

src/components/index.ts

// ...
export * from "./tag"
// ...

Next, we'll cover documenting components.