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.