| name | ionic |
| description | [Applies to: **/*.{js,jsx,ts,tsx}] This guide provides definitive best practices for developing high-performance, maintainable, and accessible Ionic applications using modern TypeScript and web standards. |
| source | cursor_mdc |
ionic Best Practices
Ionic Framework enables powerful cross-platform applications. Adhering to these guidelines ensures your codebase is robust, performant, and aligned with modern web development standards.
1. Code Organization and Structure
Adopt a consistent, component-centric file structure for clarity and maintainability.
1.1 One Component Per Directory
Each Ionic component (or Stencil component it wraps) must reside in its own directory, co-locating all related files.
❌ BAD: Scattered files
// components/my-button.tsx
// components/my-button.css
// components/my-card.tsx
// components/my-card.css
✅ GOOD: Component-per-directory
├── components
│ ├── ion-button
│ │ ├── ion-button.css
│ │ └── ion-button.tsx
│ └── ion-card
│ ├── ion-card.css
│ └── ion-card.tsx
1.2 Naming Conventions
Use descriptive, noun-based names for components, prefixed for uniqueness.
- HTML Tag: Use a unique, brand-specific prefix (e.g.,
ion-). Names should be nouns.
- TS Class: No prefix for the ES6 class name, as classes are scoped.
❌ BAD: Verb-based or un-prefixed tags
@Component({ tag: 'animating-component' })
export class AnimatingComponent {}
✅ GOOD: Noun-based, prefixed tags
@Component({ tag: 'ion-animation' })
export class Animation {}
1.3 Component File Structure (Newspaper Metaphor)
Organize component class members logically, from high-level summaries to detailed implementations.
@Component({
tag: 'ion-example',
styleUrls: {
ios: 'ion-example.ios.css',
md: 'ion-example.md.css',
},
})
export class IonExample {
private internalValue: number = 0;
@Element() el!: HTMLElement;
@State() isActive: boolean = false;
@Prop() text: string = '';
@Prop() disabled: boolean = false;
@Watch('disabled')
disabledChanged(newValue: boolean, oldValue: boolean) {
console.log(`Disabled changed from ${oldValue} to `);
}
() ionClick!: <>;
() { }
() { }
() { }
() { }
() { }
() { }
(, { : })
() {
(event. === .) {
..();
}
}
()
() {
..();
}
() {
.++;
}
() {
(
);
}
}
2. TypeScript Best Practices
Leverage TypeScript for type safety, improved tooling, and maintainability.
2.1 Enforce Strict Typing
Always enable noImplicitAny and strictNullChecks in your tsconfig.json. Avoid any unless absolutely necessary.
❌ BAD: Implicit any
function processData(data) {
console.log(data.value);
}
✅ GOOD: Explicit typing
interface Data {
value: string;
}
function processData(data: Data): void {
console.log(data.value);
}
2.2 Use private for Internal Members
Mark internal class properties and methods as private to enforce encapsulation and aid dead code detection.
❌ BAD: All public by default
export class MyComponent {
helperMethod() { }
}
✅ GOOD: Encapsulated internals
export class MyComponent {
private helperMethod() { }
}
2.3 JSDocs for Public API
Document all @Prop(), @Event(), and @Method() decorators with JSDoc comments to generate documentation and improve editor experience.
❌ BAD: Undocumented public API
@Prop() value: string;
@Event() ionChange: EventEmitter<string>;
✅ GOOD: Documented public API
@Prop() value: string = '';
@Event() ionChange!: EventEmitter<string>;
3. Common Patterns and Anti-patterns
Adopt patterns that leverage Ionic's strengths and avoid common pitfalls.
3.1 Always await Asynchronous Native Plugin Calls
Capacitor/Cordova plugin calls are asynchronous. Always await their resolution to prevent race conditions and unexpected behavior.
❌ BAD: Fire-and-forget native calls
import { Camera } from '@capacitor/camera';
async takePhoto() {
Camera.getPhoto({ });
console.log('Photo request sent');
}
✅ GOOD: Await native calls
import { Camera, CameraResultType } from '@capacitor/camera';
async takePhoto() {
const photo = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri
});
console.log('Photo taken:', photo.webPath);
}
3.2 Prefer Ionic Components for UI
Leverage Ionic's rich set of UI components for adaptive styling, performance, and accessibility. Avoid custom implementations where an Ionic component exists.
❌ BAD: Custom button for navigation
<button onclick="navigateTo('/home')">Home</button>
✅ GOOD: Ionic button with router link
<ion-button routerLink="/home" routerDirection="forward">Home</ion-button>
3.3 Theming with CSS Variables
Customize your app's look and feel using Ionic's CSS Variables for consistent, platform-adaptive theming.
❌ BAD: Hardcoded styles
.my-component {
background-color: #3880ff;
}
✅ GOOD: CSS Variables for theming
.my-component {
background-color: var(--ion-color-primary);
}
4. Performance Considerations
Ionic is built for performance. Follow these to maintain a fast, responsive app.
4.1 Optimize DOM Updates
Avoid direct, heavy DOM manipulation. Let the framework (Angular, React, Vue, Stencil) manage DOM updates efficiently.
❌ BAD: Manually updating styles on scroll
this.el.style.transform = `translateY(${scrollTop}px)`;
✅ GOOD: Use CSS transforms or Ionic's built-in scroll events
4.2 Leverage Built-in Optimizations
Ionic components often include hardware-accelerated transitions, lazy loading, and tree-shaking. Ensure your build process enables these.
- Tree-shaking: Use modern bundlers (Webpack, Rollup) and ES Modules.
- Lazy Loading: For routes and components, especially in Angular/React/Vue.
5. Common Pitfalls and Gotchas
Be aware of these common issues to prevent bugs and performance regressions.
5.1 Ignoring Accessibility (a11y)
Ionic components are built with accessibility in mind, but developers must use them correctly. Run automated accessibility audits as part of CI.
❌ BAD: Missing aria-label or semantic elements
<ion-icon name="menu" (click)="openMenu()"></ion-icon>
✅ GOOD: Accessible elements
<ion-menu-button auto-hide="false"></ion-menu-button>
<ion-icon name="menu" aria-label="Open menu" (click)="openMenu()"></ion-icon>
5.2 Using Deprecated TSLint Configurations
TSLint is deprecated. Migrate to ESLint with @stencil-community/eslint-plugin and eslint-config-ionic for modern static analysis.
❌ BAD: Relying on tslint.json
{
"extends": "tslint-ionic-rules/strict"
}
✅ GOOD: Using ESLint
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@stencil-community/recommended',
'@ionic/eslint-config/recommended'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', '@stencil-community'],
};
6. Testing Approaches
Implement a robust testing strategy for reliability and maintainability.
6.1 Unit Testing
Use Jest for unit testing individual components, services, and utilities. Focus on isolated logic.
import { MyService } from './my-service';
describe('MyService', () => {
let service: MyService;
beforeEach(() => {
service = new MyService();
});
it('should return the correct value', () => {
expect(service.calculate(2, 3)).toBe(5);
});
});
6.2 End-to-End (E2E) Testing
Use Playwright or Cypress for E2E tests to simulate user interactions across your application.
import { test, expect } from '@playwright/test';
test('should navigate to home and display title', async ({ page }) => {
await page.goto('/home');
await expect(page.locator('ion-title')).toHaveText('Home');
});