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
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:
480
docs/frontend/building-components.md
Normal file
480
docs/frontend/building-components.md
Normal 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
94
docs/frontend/i18n.md
Normal 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.
|
||||
61
docs/frontend/migrating-from-javascript.md
Normal file
61
docs/frontend/migrating-from-javascript.md
Normal 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
258
docs/frontend/react.md
Normal 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>
|
||||
);
|
||||
}
|
||||
```
|
||||
199
docs/frontend/typescript-conventions.md
Normal file
199
docs/frontend/typescript-conventions.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user