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-componentscontext 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 { ... }. Notype FooProps. NoIprefix on prop interfaces. - Co-located in the same
.tsxfile as the component. Never imported fromsrc/interfaces/. Thesrc/interfaces/directory is for shared domain types (seetypescript.md). - One exception currently:
CustomCSATPopupMessage.tsxuses 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:
- A manager (singleton,
Map<botDivId, T>registry — seemanagers.md). - A utility module in
src/misc/orsrc/utils.ts. - The
CoreWidgetclass 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
| Category | Convention | Examples |
|---|---|---|
| React component (renders JSX) | PascalCase.tsx | AdaptiveCardDecorator.tsx, CustomCSATPopup.tsx, Icons/CloseIcon.tsx |
| Manager that renders JSX | camelCase.tsx | csat/csatManager.tsx, ratingForm/ratingFormManager.tsx, notifications/notificationManager.tsx |
| Manager / utility (no JSX) | camelCase.ts | history/historyManager.ts, preChatSteps/preChatStepsManager.ts, locale/LocaleManager.ts |
| Non-React UI helper | camelCase.ts | components/emojiPicker.ts, components/popUpBubbleWidget.ts, components/homeButtonHandler.ts, components/customActionButtonHandler.ts |
| Widget class | camelCase.ts / .tsx | widgets/coreWidget.ts, widgets/bubbleWidget.tsx, widgets/embedWidget.tsx |
| Style file | camelCase.scss | bubbleWidget.scss, csatPopup.scss, customVariables.scss |
| Asset / translation | lowercase.json | assets/translations/en.json, assets/translations/de.json |
Rules of thumb:
.tsxextension is reserved for files that contain JSX. A manager that callscreateRoot(...).render(<X />)must be.tsx. A manager that only manipulates DOM is.ts.- Component files are
PascalCasebecause their default export is a component. Manager files arecamelCasebecause their default export is a class instance / factory pattern. - The
Icons/subfolder undersrc/components/holds React-component SVG wrappers and follows the samePascalCase.tsxrule. - File names match their default export name; deviation (e.g.
LocaleManager.tsexports classLocaleManagerbut starts with a capital letter) is the exception and predates the current convention.