Skip to main content

Patterns: Components

React component conventions for hbf-webchat: functional vs class, prop typing, default props, hooks, file naming, React import form. Load via the patterns-components context group.

Functional vs Class

The codebase is mixed and split roughly 50/50: 10 class components and 10 functional components in src/components/.

Rule: match the surrounding file. Do not migrate a class component to functional just because you touched it, and do not add a class component to a file that already uses functional. There is no directional pressure either way; the inconsistency is acknowledged in anti-patterns.md but is not actively being cleaned up.

Class components dominate the decorator-style components mounted via webchat middlewares (AdaptiveCardDecorator, BackButtonDecorator, BotActivityAvatarDecorator, CarouselDecorator, CSATPopupDecorator, LiveChatMessageActivityDecorator, RatingFormPopupDecorator, BotActivityStatusWithReactions, EndLiveChatButton, LocaleDropdown).

Functional components dominate the icon and popup-message layer (Icons/CloseIcon, Icons/InfoIcon, Icons/StarIcon, CustomCSATPopup, CustomCSATPopupMessage, CustomRatingFormPopup, CustomRatingFormPopupMessage, CustomNotifications, CustomNotificationMessage, ListeningOverlay).

Class component shape

import React from "react";

interface BackButtonDecoratorProps {
children?: React.ReactNode;
activity: IMessageActivity;
}

export default class BackButtonDecorator extends React.Component<BackButtonDecoratorProps> {
render() {
return <div>{this.props.children}</div>;
}
}

Functional component shape

import React from "react";

interface CloseIconProps {
primaryFill?: string;
secondaryFill?: string;
width?: number;
height?: number;
}

const CloseIcon = ({ primaryFill, secondaryFill, width = 24, height = 24 }: CloseIconProps) => (
<svg width={width} height={height}>...</svg>
);

export default CloseIcon;

Prop Typing

  • Always interface FooProps { ... }. No type FooProps. No I prefix on prop interfaces.
  • Co-located in the same .tsx file as the component. Never imported from src/interfaces/. The src/interfaces/ directory is for shared domain types (see typescript.md).
  • One exception currently: CustomCSATPopupMessage.tsx uses an inline anonymous object type for its props. Do not extend that pattern — name the interface.

Default Props

Destructuring defaults only. Static defaultProps is not used anywhere in the codebase and should not be introduced.

const CloseIcon = ({ width = 24, height = 24 }: CloseIconProps) => ...

For class components, set defaults in the prop type as optional and apply them in render() if needed:

class Foo extends React.Component<FooProps> {
render() {
const size = this.props.size ?? 24;
...
}
}

Hooks

No custom hooks exist in the codebase. The pattern of extracting reusable logic into a useFoo hook is absent. Hooks usage is limited to React's built-in useState, useEffect, useCallback, etc., inside functional components.

If you find yourself wanting to write a custom hook, first check whether the logic belongs in:

  1. A manager (singleton, Map<botDivId, T> registry — see managers.md).
  2. A utility module in src/misc/ or src/utils.ts.
  3. The CoreWidget class itself (if it is widget-lifecycle-bound).

Custom hooks are not forbidden but are unprecedented; introducing one should be a deliberate choice with rationale.

React Import Form

import React from "react";          // ✅ default form (preferred)
import * as React from "react"; // ❌ namespace form (legacy, do not extend)

The codebase is split exactly 50/50 between the two forms (14 files each). The default form is the codified direction; new files use it. Existing namespace-form files are tracked in .agent-files/tasks/20260428_hbf-webchat-pattern-migration.md for incremental cleanup.

React.Component, React.ReactNode, etc. are still accessible under the default import — React is the same namespace either way under modern TypeScript.

File Naming

CategoryConventionExamples
React component (renders JSX)PascalCase.tsxAdaptiveCardDecorator.tsx, CustomCSATPopup.tsx, Icons/CloseIcon.tsx
Manager that renders JSXcamelCase.tsxcsat/csatManager.tsx, ratingForm/ratingFormManager.tsx, notifications/notificationManager.tsx
Manager / utility (no JSX)camelCase.tshistory/historyManager.ts, preChatSteps/preChatStepsManager.ts, locale/LocaleManager.ts
Non-React UI helpercamelCase.tscomponents/emojiPicker.ts, components/popUpBubbleWidget.ts, components/homeButtonHandler.ts, components/customActionButtonHandler.ts
Widget classcamelCase.ts / .tsxwidgets/coreWidget.ts, widgets/bubbleWidget.tsx, widgets/embedWidget.tsx
Style filecamelCase.scssbubbleWidget.scss, csatPopup.scss, customVariables.scss
Asset / translationlowercase.jsonassets/translations/en.json, assets/translations/de.json

Rules of thumb:

  • .tsx extension is reserved for files that contain JSX. A manager that calls createRoot(...).render(<X />) must be .tsx. A manager that only manipulates DOM is .ts.
  • Component files are PascalCase because their default export is a component. Manager files are camelCase because their default export is a class instance / factory pattern.
  • The Icons/ subfolder under src/components/ holds React-component SVG wrappers and follows the same PascalCase.tsx rule.
  • File names match their default export name; deviation (e.g. LocaleManager.ts exports class LocaleManager but starts with a capital letter) is the exception and predates the current convention.