snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,480 @@
# Building components
A component is a reusable piece of code that represents a single UI element. Components vary by complexity and type, existing at either the **application** level or the **Fuselage library** level:
- **Application components** remain specific to particular Rocket.Chat applications, without cross-application reusability.
- **Fuselage library components** are reusable across all Rocket.Chat applications and represent the recommended approach.
## Component rules matrix
| Combination | Fuselage level | Application level |
| ---------------- | -------------- | ----------------- |
| Simple & Visual | ✅ | ❌ |
| Complex & Visual | ✅ | ✅ |
| Simple & Logical | ❌ | ❌ |
| Complex & Logical| ❌ | ✅ |
## Simple components
Simple components represent atomic UI elements like buttons or text fields.
### Prefer variation over styles
Use prop names suggesting component variation rather than style-based props. Instead of `color="blue"`, use `variation="primary"` for improved readability and consistency.
### Avoid hardcoded values or magic numbers
Preferred:
```jsx
<Button small square>
<Icon name='circle-arrow-down' size='x24' />
</Button>
```
Not recommended:
```jsx
<Button height='50px' width='50px' square>
<Icon name='circle-arrow-down' size='x24' />
</Button>
```
### Customize via CSS variables
Prioritize CSS variable customization over arbitrary values:
```scss
$modal-margin: theme('modal-margin', auto);
.rcx-modal {
position: static;
display: flex;
width: 100%;
max-height: 100%;
margin: $modal-margin;
}
```
### Document variations in Storybook
Display all possible variations with descriptive explanations for non-obvious options.
### Unit test all behaviors
Comprehensive unit tests ensure reliability across all intended scenarios:
```jsx
describe('[Menu Component]', () => {
it('should renders without crashing', () => {
render(<Simple {...Simple.args} />);
});
it('should open options when click', async () => {
const { getByTestId } = render(<Simple {...Simple.args} />);
const button = getByTestId('menu');
userEvent.click(button);
expect(await screen.findByText('Make Admin')).toBeInTheDocument();
});
it('should have no options when click twice', async () => {
const { getByTestId } = render(<Simple {...Simple.args} />);
const button = getByTestId('menu');
await userEvent.click(button);
await userEvent.click(button);
expect(screen.queryByText('Make Admin')).toBeNull();
});
it('should have no options when click on menu and then elsewhere', async () => {
const { getByTestId } = render(<Simple {...Simple.args} />);
const button = getByTestId('menu');
await userEvent.click(button);
await userEvent.click(document.body);
expect(screen.queryByText('Make Admin')).toBeNull();
});
});
```
### Avoid "Boxed" components
The `Box` component works as a wildcard primarily for simple or complex components during prototyping. For simple components specifically, construct using HTML tags instead.
## Complex components
Complex components combine multiple simple components for sophisticated UI elements like modals or tables.
### Visual only, no logic
Concentrate solely on the user interface design, ensuring it is poised to incorporate the required logic seamlessly.
```jsx
export const Default = () => (
<Modal>
<ModalHeader>
<ModalHeaderText>
<ModalTitle>Modal Header</ModalTitle>
</ModalHeaderText>
<ModalClose />
</ModalHeader>
<ModalContent>Modal Body</ModalContent>
<ModalFooter>
<ModalFooterControllers>
<Button>Cancel</Button>
<Button primary onClick={action('click')}>
Submit
</Button>
</ModalFooterControllers>
</ModalFooter>
</Modal>
);
```
### Split components for clarity
Structure components into understandable, logical segments.
### Develop with Storybook first
Start with Storybook to separate interface from logic:
```tsx
export const CallingDM: ComponentStory<typeof VideoConfMessage> = () => (
<VideoConfMessage>
<VideoConfMessageRow>
<VideoConfMessageIcon variant='incoming' />
<VideoConfMessageText>Calling...</VideoConfMessageText>
</VideoConfMessageRow>
<VideoConfMessageFooter>
<VideoConfMessageAction primary>Join</VideoConfMessageAction>
<VideoConfMessageFooterText>Waiting for answer</VideoConfMessageFooterText>
</VideoConfMessageFooter>
</VideoConfMessage>
);
export const CallEndedDM: ComponentStory<typeof VideoConfMessage> = () => (
<VideoConfMessage>
<VideoConfMessageRow>
<VideoConfMessageIcon />
<VideoConfMessageText>Call ended</VideoConfMessageText>
</VideoConfMessageRow>
<VideoConfMessageFooter>
<VideoConfMessageAction>Call Back</VideoConfMessageAction>
<VideoConfMessageFooterText>Call was not answered</VideoConfMessageFooterText>
</VideoConfMessageFooter>
</VideoConfMessage>
);
```
### Child components must remain scoped
❌ Incorrect:
```tsx
export const MyComponent: ComponentStory<typeof VideoConfMessage> = () => (
<Box display='flex'>
<form>
<VideoConfMessageAction>Call ended</VideoConfMessageAction>
</form>
</Box>
);
```
✅ Correct:
```tsx
export const MyComponent: ComponentStory<typeof VideoConfMessage> = () => (
<Box display='flex'>
<form>
<VideoConfMessage>
<VideoConfMessageFooter>
<VideoConfMessageAction>Call ended</VideoConfMessageAction>
</VideoConfMessageFooter>
</VideoConfMessage>
</form>
</Box>
);
```
### HTML elements, box, and box props should be encapsulated
Encapsulation ensures predictable behavior and streamlined debugging by accessing HTML elements and Box props solely through the Box component's API.
❌ Incorrect:
```tsx
export const VideoConfMessage = () => (
<Box mbs='x4' maxWidth='345px' borderWidth={2} borderColor='neutral-200' borderRadius='x4'>
<Box p='x16' display='flex' alignItems='center'>
<Icon name='link' />
<div>My Text</div>
</Box>
</Box>
);
```
✅ Correct:
```tsx
export type VideoConfMessageProps = Omit<AllHTMLAttributes<HTMLDivElement>, 'is'>;
const VideoConfMessage = (props: VideoConfMessageProps) => (
<Box mbs='x4' maxWidth='345px' borderWidth={2} borderColor='neutral-200' borderRadius='x4' {...props} />
);
```
### Provide hooks as helpers
In the following example, the `useVideoConfControllers` hook is provided as a helper to manage the state of the popup's controllers:
```ts
export const useVideoConfControllers = (
initialPreferences: controllersConfigProps = { mic: true, cam: false },
): { controllersConfig: controllersConfigProps; handleToggleMic: () => void; handleToggleCam: () => void } => {
const [controllersConfig, setControllersConfig] = useState(initialPreferences);
const handleToggleMic = useCallback((): void => {
setControllersConfig((prevState) => ({ ...prevState, mic: !prevState.mic }));
}, []);
const handleToggleCam = useCallback((): void => {
setControllersConfig((prevState) => ({ ...prevState, cam: !prevState.cam }));
}, []);
return {
controllersConfig,
handleToggleMic,
handleToggleCam,
};
};
```
```tsx
const { controllersConfig } = useVideoConfControllers();
return (
<VideoConfPopup>
<VideoConfPopupHeader>
<VideoConfPopupTitle text={t('Calling')} counter />
<VideoConfPopupControllers>
<VideoConfController
active={controllersConfig.cam}
title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
icon={controllersConfig.cam ? 'video' : 'video-off'}
disabled
/>
<VideoConfController
active={controllersConfig.mic}
title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
icon={controllersConfig.mic ? 'mic' : 'mic-off'}
disabled
/>
</VideoConfPopupControllers>
</VideoConfPopupHeader>
</VideoConfPopup>
);
```
## Understanding the component and defining the scope
New components typically emerge from requirements put forth by the Product Design Team. The frontend engineer holds the responsibility of assessing the genuine necessity of such components. Due to the substantial effort involved in creating a new component, it is prudent to collaborate with product managers and designers. It is advisable to explore the feasibility of employing Complex Components as an MVP to validate concepts and user flows. After successful validation, the progression to developing a new Fuselage level component can be considered.
**How do I know my component should be part of the Fuselage library?**
Consider the `VerticalBar` component as a clear example. It began as a Complex Component for a single application but has now advanced to the Fuselage level. This shift is driven by its usefulness in multiple applications, like Rocket.Chat and Cloud Portal. This case demonstrates how components can grow from specific solutions to versatile tools with broader applications.
## Logical components
### Use child components to compose a logical complex component
Leverage the integration of child components to construct a unified and logical complex component:
```tsx
const OutgoingPopup = ({ room, onClose, id }: OutgoingPopupProps): ReactElement => {
const t = useTranslation();
const videoConfPreferences = useVideoConfPreferences();
const { controllersConfig } = useVideoConfControllers();
return (
<VideoConfPopup>
<VideoConfPopupHeader>
<VideoConfPopupTitle text={t('Calling')} counter />
<VideoConfPopupControllers>
<VideoConfController
active={controllersConfig.cam}
title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
icon={controllersConfig.cam ? 'video' : 'video-off'}
disabled
/>
<VideoConfController
active={controllersConfig.mic}
title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
icon={controllersConfig.mic ? 'mic' : 'mic-off'}
disabled
/>
</VideoConfPopupControllers>
</VideoConfPopupHeader>
</VideoConfPopup>
);
};
```
### Customization through variations
Provide users with the ability to customize a component's appearance or behavior by selecting from predefined variations or options. This approach enhances user experience and flexibility in adapting components to specific requirements.
```tsx
<VideoConfController
active={controllersConfig.mic}
title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
icon={controllersConfig.mic ? 'mic' : 'mic-off'}
disabled
/>
```
```tsx
const VideoConfController = ({ icon, active, secondary, disabled, small = true, ...props }: VideoConfControllerProps): ReactElement => {
const id = useUniqueId();
return (
<IconButton
small={small}
icon={icon}
id={id}
info={active}
disabled={disabled}
secondary={secondary || active || disabled}
{...props}
/>
);
};
```
### Avoid direct styles
Refrain from applying direct styles to components. By avoiding inline styling, the code maintains a cleaner structure and promotes better separation of concerns, enhancing maintainability and readability.
❌ Avoid:
```tsx
<VideoConfPopup>
<VideoConfPopupHeader>
<VideoConfPopupTitle text={t('Calling')} counter />
<VideoConfPopupControllers>
<Box display='flex' alignItems='center'>
<VideoConfController
width='50px'
height='50px'
active={controllersConfig.cam}
title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
icon={controllersConfig.cam ? 'video' : 'video-off'}
disabled
/>
<VideoConfController
active={controllersConfig.mic}
title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
icon={controllersConfig.mic ? 'mic' : 'mic-off'}
disabled
/>
</Box>
</VideoConfPopupControllers>
</VideoConfPopupHeader>
</VideoConfPopup>
```
### Don't write CSS styles in JS files
This approach separates your component's logic from styling, promoting better code organization and maintainability while avoiding inline CSS-in-JS styling.
Define your custom styling in an external CSS file:
```css
/* styles.css */
.customClass {
border: 1px solid black;
padding: 1.5rem;
}
```
Then, apply the class to your component:
```tsx
import './styles.css';
return (
<VideoConfPopup>
<VideoConfPopupHeader>
<VideoConfPopupTitle text={t('Calling')} counter />
<VideoConfPopupControllers>
<VideoConfController
className='customClass'
active={controllersConfig.cam}
title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
icon={controllersConfig.cam ? 'video' : 'video-off'}
disabled
/>
<VideoConfController
active={controllersConfig.mic}
title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
icon={controllersConfig.mic ? 'mic' : 'mic-off'}
disabled
/>
</VideoConfPopupControllers>
</VideoConfPopupHeader>
</VideoConfPopup>
);
```
### Use the states of the component
By using component states to conditionally render different complex components, you maintain a clear and organized structure in your code, enhancing readability and maintainability.
```tsx
if (isReceiving) {
return <IncomingPopup room={room} id={id} position={position} onClose={onClose} onMute={handleMute} onConfirm={handleConfirm} />;
}
if (isCalling) {
return <OutgoingPopup room={room} id={id} onClose={onClose} />;
}
return <StartCallPopup loading={starting} room={room} id={id} onClose={dismissOutgoing} onConfirm={handleStartCall} />;
```
Each state should render the proper Complex Component:
```tsx
const OutgoingPopup = ({ room, onClose, id }: OutgoingPopupProps): ReactElement => {
const t = useTranslation();
const videoConfPreferences = useVideoConfPreferences();
const { controllersConfig } = useVideoConfControllers();
return (
<VideoConfPopup>
<VideoConfPopupHeader>
<VideoConfPopupTitle text={t('Calling')} counter />
<VideoConfPopupControllers>
<VideoConfController
active={controllersConfig.cam}
title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
icon={controllersConfig.cam ? 'video' : 'video-off'}
disabled
/>
<VideoConfController
active={controllersConfig.mic}
title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
icon={controllersConfig.mic ? 'mic' : 'mic-off'}
disabled
/>
</VideoConfPopupControllers>
</VideoConfPopupHeader>
</VideoConfPopup>
);
};
```
## Visual components
Visual components are responsible for the appearance of a UI element. They define the element's style, layout, and other visual properties.
Adhering to the guidelines in Fuselage's componentization offers the value of modular, reusable, and maintainable UI components. This approach enables efficient development, ensures consistent behavior, and supports the evolution of solutions from specific contexts to broader applications. By encapsulating logic, avoiding direct styles, and leveraging API-driven customization, developers can create a streamlined and user-centered experience.

94
docs/frontend/i18n.md Normal file
View File

@@ -0,0 +1,94 @@
# Translations (i18n) in the client
Client-side conventions only. How keys are stored, named, and written — naming, interpolation, plurals, formatters, what the linter enforces — is in [docs/i18n.md](../i18n.md), and applies here too.
## Reading translations
Use `useTranslation` from `react-i18next`:
```tsx
import { useTranslation } from 'react-i18next';
const DeleteRoomButton = () => {
const { t } = useTranslation();
return <Button danger>{t('Delete_room')}</Button>;
};
```
❌ Not `useTranslation` from `@rocket.chat/ui-contexts` — it is `@deprecated` and re-renders every consumer of the translation context on language change. ~125 client modules still import it against ~890 for the `react-i18next` one; don't add to the count.
[`TranslationProvider`](../../apps/meteor/client/providers/TranslationProvider.tsx) bundles `en` statically and sets it as `fallbackLng`, so English keys resolve immediately. Only a non-English active locale is fetched over HTTP (`partialBundledLanguages: true`); until it arrives, and for any key it omits, the English string is used. The server, by contrast, holds all 68 locales in memory.
## Never build keys at runtime
`t()` does not reject unknown keys (see [docs/i18n.md](../i18n.md#types-are-generated-from-en)), so a computed key has nothing checking it — and it makes the key invisible to anyone grepping for it.
❌ Incorrect:
```tsx
{t(`App_status_${instance.status}` as TranslationKey)}
```
✅ Correct — map the value to a literal key:
```tsx
const APP_STATUS_LABELS = {
installed: 'App_status_installed',
updating: 'App_status_updating',
} as const satisfies Record<AppStatus, RocketchatI18nKeys>;
{t(APP_STATUS_LABELS[instance.status])}
```
The map is the thing that tells you a key is missing when a new status is added, and `satisfies` is what makes the map itself checkable. `as TranslationKey` appears about 70 times in the client; treat every one as debt rather than precedent.
## Rich text: the `Trans` component
When a sentence contains a link, bold run, or nested element, don't split it into several keys. Use `Trans` and put the markup in the translation:
```json
{
"Unique_ID_change_detected_learn_more_link": "Read the <a>documentation</a> before proceeding."
}
```
```tsx
import { Trans } from 'react-i18next';
<Trans i18nKey='Unique_ID_change_detected_learn_more_link' components={{ a: <ExternalLink to={links.go.fingerPrintChangedFaq} /> }} />;
```
Name your tags. Indexed placeholders (`<1>`, `<3>`) appear in older keys and are effectively unreadable for a translator, who can't tell what index 3 wraps:
❌ Avoid:
```json
{ "Limits_reached": "Your workspace reached the <1>{{val}}</1> limit. <3>Manage subscription</3>." }
```
✅ Prefer:
```json
{ "Limits_reached": "Your workspace reached the <bold>{{val}}</bold> limit. <link>Manage subscription</link>." }
```
The base locale is currently split about evenly between the two (~50 values each), so this is a direction, not an established majority.
Pass interpolation values through `values` and `t` options through `tOptions`:
```tsx
<Trans i18nKey='Airgapped_workspace_warning' values={{ remainingDays }} />
```
## Interpolated values are not HTML-escaped
`escapeValue` is `false`. That is safe only because React escapes strings when it renders them as text nodes — it is not a property of the translation itself.
So if a translated string is injected as HTML, sanitize it, as the two existing call sites do:
```tsx
<FieldHint dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t(provider.description)) }} />
```
Prefer `Trans` over injected HTML wherever possible — it needs no sanitizing because the components are real React elements, not parsed markup.

View File

@@ -0,0 +1,61 @@
# Migrating from JavaScript
## TypeScript is a superset of JavaScript
TypeScript is an extension of JavaScript, meaning that when transitioning from JavaScript to TypeScript, you can employ the identical syntax used in JavaScript. Errors flagged by the TypeScript compiler (`tsc`) and ESLint enforce best practices, and both gate CI — [`ci-code-check`](../../.github/workflows/ci-code-check.yml) runs `turbo run typecheck` and `yarn lint` — so resolve them rather than leaving them in place.
When a diagnostic is genuinely wrong about the code, suppress it explicitly and narrowly:
- `@ts-expect-error` with a short reason, rather than `@ts-ignore` — it fails once the underlying error is gone, so the suppression can't outlive its cause;
- `eslint-disable-next-line`, naming the specific rule rather than disabling the line wholesale.
## JSDoc
When `allowJs` is enabled in `tsconfig.json`, JSDoc comments can document types within JavaScript code. This is particularly useful during gradual migration when `tsc` struggles with type inference.
Example with `@typedef`:
```js
// module.js
/**
* @typedef {Object} Foo
* @property {string} bar
* @property {string} [qux]
*/
/** @type {Foo} */
export const foo = { bar: 'baz' };
foo.qux = 'quux';
```
Alternative using the `@type` tag:
```js
// module.js
/**
* @type {{ bar: string; qux?: string }}
*/
export const foo = { bar: 'baz' };
foo.qux = 'quux';
```
Both approaches help ensure TypeScript accurately recognizes the complete type structure.
## Declare a `*.d.ts` file
Creating a `.d.ts` declaration file is strongly recommended when migrating large JavaScript modules. It describes the module's public surface for type checking and is easier to read as a whole than JSDoc scattered through the implementation.
A declaration file is types only: it is erased at build time and creates no runtime code. Place it alongside the JavaScript module it describes, with the same basename, and declare only exports that module actually has — TypeScript will trust the declaration over the `.js`, so anything you declare that isn't really exported will typecheck at the import site and be `undefined` at runtime.
```ts
// hugeModule.d.ts — sits next to hugeModule.js
export function foo(): void; // hugeModule.js exports `foo`
export function bar(): void; // hugeModule.js exports `bar`
```
Writing the file is still a good way to plan the shape you're migrating toward, but it only records exports; it can't create or relocate them. If an export belongs in another module, move the implementation first, then declare it beside its new home.

258
docs/frontend/react.md Normal file
View File

@@ -0,0 +1,258 @@
# React
> Most of the recommendations here are based on Alex Kondov's [Tao of React](https://alexkondov.com/tao-of-react/).
## Prefer functional components
React initially introduced class components to leverage JavaScript class syntax for managing state and component lifecycles. However, class components have significant drawbacks:
- they tend to be verbose;
- they often involve a misuse of the inheritance mechanism through `extends` and `super`.
Hooks were introduced to provide an alternative approach to declaring state and effects. They maintain the core concept of the component's render function without the need for classes, streamlining the development process.
## Declare one component per file
While colocation is a commendable concept, it's not consistently followed for React components within a single file. The main reason is that we've noticed people misusing this approach before. It might start with something as straightforward as adding a basic modal component alongside a page component, but it can quickly lead to a confusing jumble of components that becomes difficult to manage.
## Name components
Failing to name a component is a common mistake that can lead to prolonged debugging efforts. It results in less informative error stacks and challenges while navigating components in React Dev Tools. There are two approaches to properly name a component:
1. By assigning the function straight to a binding. The arrow function is itself anonymous, but assigning it to a `const` makes JavaScript infer the name from the variable:
```jsx
const Foo = () => {
return <div>Foo</div>;
};
console.log('The component name is:', Foo.name);
```
2. By setting the `displayName` property. Once a wrapper call sits between the binding and the function, there is nothing for inference to attach to — the arrow is an argument to `memo`, and `memo` returns an object rather than a function:
```jsx
const Foo = memo(() => {
return <div>Foo</div>;
});
Foo.displayName = 'Foo'; // `Foo.name` is `undefined`
console.log('The component name is:', Foo.displayName);
```
## Use default export at the end of file
While named exports are often preferred, using default export enhances code readability, especially when dealing with Higher Order Components (HOCs) like `memo` and `forwardRef`, and it aligns neatly with code splitting using `lazy`.
```tsx
// Component.tsx
import { memo } from 'react';
type ComponentProps = {
name: string;
};
// Anonymous arrow, but the const assignment infers the name "Component"
const Component = (props: ComponentProps) => {
return <div>Hello, {props.name}</div>;
};
export default memo(Component); // the component name is preserved
```
```ts
// index.ts
import { lazy } from 'react';
const Component = lazy(() => import('./Component'));
```
The same example based on named exports is less readable:
```tsx
// Component.tsx
import { memo } from 'react';
type ComponentProps = {
name: string;
};
// Anonymous arrow in an argument position — no name is inferred
export const Component = memo((props: ComponentProps) => {
return <div>Hello, {props.name}</div>;
});
Component.displayName = 'Component'; // needed for React Dev Tools
```
```ts
// index.ts
import { lazy } from 'react';
const Component = lazy(async () => {
const { Component } = await import('./Component');
return { default: Component }; // you need to reconstruct the default export
});
```
## Extract helper functions
A drawback associated with the adoption of React Hooks is the tendency for individuals to define helper functions within the component. This is facilitated by the fact that there is no requirement to pass arguments; instead, variables from the encompassing scope can be directly accessed and used.
```jsx
const Component = () => {
const value = useMyHook();
const isValueOK = () => value === 'OK';
return isValueOK() ? <>OK</> : null;
};
```
Typically, effective helper functions adhere to the principle of being pure, which makes them simpler to debug. However, when you bind variables into the helper's scope, you're making it impure. Furthermore, with each rendering cycle, the function value gets redefined. While this process is efficient in terms of CPU and memory usage, it can lead to scenarios where you must rely on techniques like `useCallback` to prevent unnecessary re-renders of child components that receive your helper function as a prop.
Here's the ideal case:
```jsx
const isValueOK = (value) => value === 'OK';
const Component = () => {
const value = useMyHook();
return isValueOK(value) ? <>OK</> : null;
};
```
## Do not refer to the `children` prop explicitly
Children should always be actual children, not passed in as a prop. Use the `eslint-plugin-react` rule [`react/no-children-prop`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-children-prop.md) as reference.
❌ Incorrect:
```jsx
export const MyComponent = ({ children }) => <div children={children} />;
```
✅ Correct:
```jsx
export const MyComponent = ({ children }) => <div>{children}</div>;
```
## Declare props type explicitly
Each component named, for instance, `Component` must have an _exported_ type declaration for `ComponentProps`.
```tsx
export type MyComponentProps = {
// ...
};
const MyComponent = (props: MyComponentProps) => /* ... */;
export default MyComponent;
```
## Use generic types when appropriate
Generic types are a great way to ensure type safety once they enable us to not fall into the unsoundness of type assertions.
❌ Incorrect:
```tsx
// MyComponent.tsx
export type MyComponentProps = {
value: unknown;
onChange: (newValue: unknown) => void;
};
const MyComponent = ({ value, onChange }: MyComponentProps) => /* ... */;
export default MyComponent;
// MyApp.tsx
import MyComponent from './MyComponent';
const MyApp = () => {
const [value, setValue] = useState('');
const handleChange = (value: unknown) => {
setValue(value as string); // unsound type assertion, as unknown can be anything
};
return <MyComponent value={value} onChange={handleChange} />;
};
export default MyApp;
```
✅ Correct:
```tsx
// MyComponent.tsx
export type MyComponentProps<TValue> = {
value: TValue;
onChange: (newValue: TValue) => void;
};
const MyComponent = <TValue,>({ value, onChange }: MyComponentProps<TValue>) => /* ... */;
export default MyComponent;
// MyApp.tsx
import MyComponent from './MyComponent';
const MyApp = () => {
const [value, setValue] = useState('');
const handleChange = (value: string) => {
setValue(value);
};
// since `value` is string, `onChange` type will be inferred
// as `(value: string) => void`
return <MyComponent value={value} onChange={handleChange} />;
};
export default MyApp;
```
## Avoid naming identifiers with their value type/kind
This is a common programming convention, but it seems easier to violate when working with React concepts. In general, identifiers should not include information about the data structure they refer to.
```ts
// Examples of bad identifiers
const amountConst = 123; // instead of "amount"
let countNumber = 0; // instead of "count"
function logFunction(lineString: string) {
// instead of "log" and "line"
console.log(lineString);
}
```
Identifiers like this mimic the old-fashioned Hungarian notation. In React, this happens quite often:
```tsx
// Examples of bad identifiers in React constructs
function useThingHook() {
// ...
} // instead of "useThing"
const useThingContext = () => useContext(ThingContext);
// "ThingContext" already exposes itself as a context that provides a "Thing".
// "useThing" is good enough, since it doesn't need to expose its context dependency.
const MyComponent = () => <div />;
// Fine for code examples and components passed by props*, but in general
// capital cased function names used in JSX already communicate they are of
// "Component" kind
// * when "Component" is okay as a suffix
type MyPanelProps = {
panelComponent: ElementType<{}>;
};
function MyPanel({ panelComponent: PanelComponent }: MyPanelProps) {
// "PanelComponent" is fine here
return (
<div className='panel'>
<PanelComponent />
</div>
);
}
```

View File

@@ -0,0 +1,199 @@
# TypeScript general conventions
## Don't use CommonJS features
Avoid using CommonJS features such as `require` and `module` alongside ES module constructs like `import` and `export`. The preference is for ES modules due to their enhanced portability and user-friendly nature. However, keep in mind that synchronous conditional imports aren't possible in ES modules.
Example of CommonJS (not recommended):
```ts
// commonjs.ts
if (condition) {
const foo = require('foo');
module.exports = foo;
} else {
module.exports = {};
}
```
Invalid ES module pattern:
```ts
// esmodule.ts
if (condition) {
import foo from 'foo';
export default foo;
} else {
export default {};
}
```
## Prefer `import type` over `import`
Prioritize using `import type` for type-only imports. While a regular `import` works for both JavaScript and TypeScript, it can include module output code in the bundle. The `import type` construct is specific to TypeScript compilation and avoids unnecessary code inclusion.
```ts
// Foo.ts
export class Foo {
bar: string;
constructor(bar: string) {
this.bar = bar;
}
}
// Bar.ts
export class Bar {
foo: Foo;
constructor(foo: Foo) {
this.foo = foo;
}
}
// index.ts
import { Foo } from './Foo';
import type { Bar } from './Bar';
export const foo = new Foo('baz');
declare const bar: Bar;
// index.js (transpiled from index.ts)
import { Foo } from './Foo';
export const foo = new Foo('baz');
```
`Foo` is constructed, so its import is emitted; `Bar` is only ever a type, so `import type { Bar }` disappears entirely.
Note that TypeScript also elides a plain `import` whose bindings are only used in type positions, so writing `import` where `import type` belongs is usually not what bloats the output. `import type` is preferred because it states the intent, fails loudly if someone later uses the binding as a value, and is what tooling that transpiles file-by-file — `isolatedModules`, `verbatimModuleSyntax`, esbuild, SWC — needs in order to strip the import without consulting the type checker.
## Know the difference between `type` and `interface`
An `interface` serves as a type declaration similar to a class construct, while a `class` functions as an actual class declaration.
```ts
interface IThing {
prop: string;
method(): void;
}
class Thing implements IThing {
public prop = 'foo';
method(): void {
console.log('bar');
}
}
```
A `type` construct is similar to an interface but offers greater flexibility. The `type` construct permits declaration of union types:
```ts
type Foo = string | number;
```
An `interface` can declare generic types:
```ts
interface IFoo<T> {
prop: T;
}
```
A `type` also supports generics and conditional types, providing more adaptability:
```ts
type Foo<T> = T extends string ? { foo: number } : { bar: number };
```
## Avoid using classes as namespaces
Pattern to avoid:
```ts
// foo.ts
class Foo {
bar(): void {
// ...
}
}
export const foo = new Foo();
// index.ts
import { foo } from './foo';
foo.bar();
```
Preferred approach (when no state is needed):
```ts
// foo.ts
export function bar(): void {
// ...
}
// index.ts
import * as foo from './foo';
foo.bar();
```
Valid use case (when managing state):
```ts
// foo.ts
class Foo {
baz: number;
bar(): void {
// perform actions referencing and modifying `this.baz`
}
}
export const foo = new Foo();
// index.ts
import { foo } from './foo';
foo.bar();
```
Classes are reasonable when they encapsulate state and provide a controlled interface for modification.
## Avoid using the `any` type except when it's used as a constraint
Refrain from using `any` under most circumstances. Instead, follow this model:
- `unknown` serves as the universal type, encompassing all potential values;
- `any` should not be seen as an actual type, but rather as a means to disable TypeScript's type checking.
Using `any` is discouraged as it indicates unawareness regarding the type being manipulated. Working with `unknown` necessitates type narrowing, leading to more robust code.
```ts
// Avoid using any
declare const foo: any;
foo.bar(); // No compilation error
// Prefer using unknown
declare const bar: unknown;
bar.baz(); // Compilation error
const hasBaz = (bar: unknown): bar is { baz(): void } =>
typeof bar === 'object' && bar !== null && 'baz' in bar && typeof (bar as { baz: unknown }).baz === 'function';
if (hasBaz(bar)) {
bar.baz(); // No compilation error
}
```
Exception — generic type constraints:
```ts
type X<F> = F extends (x: unknown) => void ? true : false;
type Y<F> = F extends (x: any) => void ? true : false;
type A = X<(x: string) => void>; // `false`, because x is not `unknown`
type B = Y<(x: string) => void>; // `true`, because x is anything
```