Ctrl + K

Best Practices

Best Practices of the framework

Best Practices

Best practices in Pulsar emerge from its core philosophy: clarity over cleverness, explicit over implicit, and composition over complexity. These patterns help you write maintainable, testable, and predictable code.

Component Design

Keep render() Pure

The render() method should be a pure description of structure. All transformation, computation, and conditional logic belong in getters or methods.

// ❌ BAD - Transformation in render()
@override
Morphic render() {
  return Ul()([
    users.map((user) => UserCard(user)).toList(),  // ❌ map() in render
  ]);
}

// ✅ GOOD - Transformation in getter
List<UserCard> get userCards =>
  users.map((user) => UserCard(user)).toList();

@override
Morphic render() {
  return Ul()([...userCards]);  // ✅ Pure description
}

Store Components as Fields

Components with identity should be stored as fields, not created in render(). This preserves identity and state across re-renders.

// ❌ BAD - Creating in render()
@override
Morphic render() {
  return Div()([
    StatsWidget(),  // ❌ New instance every render
  ]);
}

// ✅ GOOD - Stored as field
final StatsWidget stats = StatsWidget();

@override
Morphic render() {
  return Div()([stats]);  // ✅ Same instance always
}

Make Dependencies Explicit

Pass dependencies through constructor parameters. Avoid hidden dependencies like global state or implicit context.

// ❌ BAD - Hidden dependency
final class UserCard extends Component {
  @override
  Morphic render() {
    final user = globalState.currentUser;  // ❌ Hidden
    return Div()([user.name]);
  }
}

// ✅ GOOD - Explicit dependency
final class UserCard extends Component {
  final User user;
  
  UserCard(this.user);  // ✅ Explicit

  @override
  Morphic render() {
    return Div()([user.name]);
  }
}

State Management

Keep State Close to Usage

Place state as close as possible to where it is used. Only lift state when multiple components truly need to share it.

// ✅ GOOD - Local state
final class SearchBox extends Component {
  String query = '';  // ✅ Only used here
  
  void handleInput(Event e) {
    final target = e.target as HTMLInputElement;
    query = target.value;
  }

  @override
  Morphic render() {
    return Input().value(query).onInput(handleInput)([]);
  }
}

// ✅ GOOD - Lifted state (multiple components share)
final class SearchPage extends Component {
  String query = '';  // ✅ Shared by SearchBox and ResultsList
  
  @override
  Morphic render() {
    return Div()([
      SearchBox(query, onQueryChange: (q) => morph(() => query = q)),
      ResultsList(query),
    ]);
  }
}

Use morph() for All State Changes

Never mutate state outside of morph(). This ensures renders are triggered and keeps state changes explicit.

// ❌ BAD - Direct mutation
void handleClick(Event e) {
  count++;  // ❌ No render triggered
}

// ✅ GOOD - Mutation via morph()
void handleClick(Event e) {
  morph(() => count++);  // ✅ Render triggered
}

Use Domain Objects for Complex Logic

Extract complex state logic into domain objects. This keeps components clean and makes logic testable.

// ✅ Domain object encapsulates logic
class ShoppingCart {
  List<CartItem> items = [];
  
  double get subtotal => 
    items.fold(0.0, (sum, item) => sum + item.price * item.quantity);
  
  double get tax => subtotal * 0.1;
  double get total => subtotal + tax;
  
  void addItem(CartItem item) {
    items.add(item);
  }
  
  void removeItem(String id) {
    items.removeWhere((item) => item.id == id);
  }
}

// ✅ Component uses domain object
final class CartComponent extends Component {
  final ShoppingCart cart = ShoppingCart();
  
  void handleAdd(CartItem item) {
    morph(() => cart.addItem(item));
  }

  @override
  Morphic render() {
    return Div()([
      P()(['Total: $${cart.total.toStringAsFixed(2)}']),
      // ...
    ]);
  }
}

Event Handling

Pass Method References, Not Calls

// ❌ BAD - Calling the method
Button().onClick(handleClick())(['Click'])  // ❌

// ✅ GOOD - Passing reference
Button().onClick(handleClick)(['Click'])  // ✅

Use onInput Without morph for Text Inputs

void handleInput(Event e) {
  final target = e.target as HTMLInputElement;
  inputValue = target.value;  // ✅ No morph - keeps focus
}

void handleSubmit(Event e) {
  morph(() {
    // Process inputValue
  });
}

Name Handlers by Intent

// ❌ BAD - Named by event type
void handleClick(Event e) { ... }

// ✅ GOOD - Named by intent
void handleSubmit(Event e) { ... }
void handleDelete(Event e) { ... }
void handleToggleMenu(Event e) { ... }

Routing

Use Builders for Route Components

// ✅ GOOD - Builder creates component
Route(
  path: '/users/:userId',
  builder: (params) => UserPage(params['userId']),
)

Store Stateful Route Components as Fields

final class App extends Component {
  // ✅ Stateful components as fields
  final DashboardPage dashboard = DashboardPage();
  
  late final Router router = Router(
    routes: [
      Route(path: '/', builder: (_) => HomePage()),
      Route(path: '/dashboard', builder: (_) => dashboard),  // ✅ Returns field
    ],
  );
}

Use RouterComponent for Nested Routing

// ✅ GOOD - RouterComponent handles routing
final class DocsPage extends Component {
  late final Router docsRouter = Router(routes: [...]);

  @override
  Morphic render() {
    return Main()([
      RouterComponent(
        router: docsRouter,
        layout: (section) => DocsLayout(section),
      ),
    ]);
  }
}

// ❌ BAD - Manual routing in render()
@override
Morphic render() {
  final path = window.location.pathname;
  Component content;
  if (path == '/docs/intro') content = IntroSection();
  // ... ❌ Impure render()
}

Styling

Use Enums, Not Strings

// ❌ BAD - String variants
ButtonStyles(variant: 'primary')

// ✅ GOOD - Enum variants
enum ButtonVariant { primary, secondary, success }
ButtonStyles(variant: ButtonVariant.primary)

Extract Complex Styling to Domain Objects

// ✅ Domain object encapsulates styling logic
class AlertStyles {
  final AlertType type;
  final bool dismissible;
  
  String get classes { ... }
  String get iconClasses { ... }
  
  static const success = AlertStyles(type: AlertType.success);
}

// ✅ Component uses domain styling
Alert('Success!', styles: AlertStyles.success)

Use Static Constants for Common Configurations

class ButtonStyles {
  static const primary = ButtonStyles();
  static const success = ButtonStyles(variant: ButtonVariant.success);
  static const largePrimary = ButtonStyles(size: ButtonSize.large);
}

// ✅ Clean usage
Button('Submit', styles: ButtonStyles.success)

Code Organization

Single Responsibility per Component

Each component should have one clear responsibility. Break large components into smaller, focused ones.

// ❌ BAD - Component does too much
final class DashboardPage extends Component {
  // User management
  // Analytics
  // Settings
  // Navigation
  // ... 500+ lines
}

// ✅ GOOD - Focused components
final class DashboardPage extends Component {
  final UserSection userSection = UserSection();
  final AnalyticsSection analytics = AnalyticsSection();
  final SettingsPanel settings = SettingsPanel();

  @override
  Morphic render() {
    return Div()([
      Navbar(),
      userSection,
      analytics,
      settings,
    ]);
  }
}

Co-locate CSS with Components

// ✅ CSS lives in web/styles near component
final class Card extends Component {
  @override
  List<Stylesheet> get styles => [
    css("styles/components/card/card.css"),
  ];
}

// File structure:
// lib/components/card/card.dart
// web/styles/components/card/card.css

Testing and Maintainability

Test Domain Objects Independently

// ✅ Domain object is testable
class ShoppingCart {
  List<CartItem> items = [];
  double get total => /* ... */;
  void addItem(CartItem item) { /* ... */ }
}

// Test without UI
test('cart calculates total correctly', () {
  final cart = ShoppingCart();
  cart.addItem(CartItem(price: 10.0, quantity: 2));
  
  expect(cart.total, 20.0);
});

Document Complex Logic

/// Manages pagination state for large lists.
/// 
/// Handles page navigation, bounds checking, and item range calculation.
/// State updates do not trigger renders - the component must call morph().
class PaginationState {
  int currentPage = 1;
  int itemsPerPage = 10;
  int totalItems = 0;
  
  /// Total number of pages based on items and page size.
  int get totalPages => (totalItems / itemsPerPage).ceil();
  
  /// Move to next page if available.
  void nextPage() {
    if (hasNext) currentPage++;
  }
}

Performance

Precompute in Getters, Not render()

// ✅ Computation happens once per state change
List<UserCard> get activeUsers =>
  users.where((u) => u.isActive).map((u) => UserCard(u)).toList();

@override
Morphic render() {
  return Div()([...activeUsers]);  // ✅ Already computed
}

Avoid Recreating Components

// ❌ BAD - New instance every render
@override
Morphic render() {
  return ExpensiveChart(data);  // ❌ Recreated
}

// ✅ GOOD - Stored as field
final ExpensiveChart chart;

MyComponent(List<Data> data) : chart = ExpensiveChart(data);

@override
Morphic render() {
  return Div()([chart]);  // ✅ Same instance
}

Core Principles Summary

  • Clarity over cleverness — Write code that is obvious, not clever.
  • Explicit over implicit — Make dependencies and data flow visible.
  • Composition over complexity — Break problems into smaller pieces.
  • Pure render() — Keep render() a pure description of structure.
  • Identity matters — Store components as fields when identity is important.
  • Domain-driven — Isolate logic into domain objects.
  • Type-safe abstractions — Use enums and objects, not strings.

Philosophy: Best practices in Pulsar emerge from treating code as a craft. Every abstraction should earn its place. Every pattern should solve a real problem. The goal is not to write less code, but to write clearer code.