Anti-Patterns
Anti-Patterns onf Pulsar
Anti-Patterns
Anti-patterns are common mistakes that violate Pulsar's core principles. Recognizing these patterns helps you avoid subtle bugs and maintain code clarity.
Each anti-pattern represents a departure from clarity, explicitness, or composition. Understanding why these patterns are problematic helps build intuition for writing better code. Component Design ❌ Impure render()
Using transformations, map(), where(), or fold() directly in render() violates the Pure Description principle. impure_render.dart Copy
// ❌ ANTI-PATTERN @override Morphic render() { return Div()([ users.map((user) => UserCard(user)).toList(), // ❌ Transformation in render items.where((item) => item.isActive).toList(), // ❌ Filtering in render ]); }
// ✅ CORRECT
List
List
@override Morphic render() { return Div()([ ...userCards, ...activeItems, ]); }
Why this is bad: Transformations in render() make the component harder to test, harder to read, and violate the principle that render() should be a pure description. All computation should happen in getters. ❌ Creating Components in render() create_in_render.dart Copy
// ❌ ANTI-PATTERN @override Morphic render() { return Div()([ StatsWidget(), // ❌ New instance every render ChartWidget(), // ❌ Identity broken, state lost ]); }
// ✅ CORRECT final StatsWidget stats = StatsWidget(); final ChartWidget chart = ChartWidget();
@override Morphic render() { return Div()([stats, chart]); }
Why this is bad: Creating components in render() breaks identity. Each render creates new instances, losing state and making components unpredictable. ❌ Hidden Dependencies hidden_deps.dart Copy
// ❌ ANTI-PATTERN final class UserProfile extends Component { @override Morphic render() { final user = AppState.currentUser; // ❌ Hidden global dependency return Div()([user.name]); } }
// ✅ CORRECT final class UserProfile extends Component { final User user;
UserProfile(this.user); // ✅ Explicit dependency
@override Morphic render() { return Div()([user.name]); } }
Why this is bad: Hidden dependencies make components hard to test, hard to understand, and tightly coupled to global state. Dependencies should be explicit. State Management ❌ Mutating State Without morph() no_morph.dart Copy
// ❌ ANTI-PATTERN void handleClick(Event e) { count++; // ❌ No render triggered items.add(newItem); // ❌ UI won't update }
// ✅ CORRECT void handleClick(Event e) { morph(() { count++; items.add(newItem); }); }
Why this is bad: Mutations outside morph() don't trigger renders. The UI becomes out of sync with state, leading to subtle bugs. ❌ Premature State Lifting premature_lift.dart Copy
// ❌ ANTI-PATTERN - State lifted "just in case" final class App extends Component { String searchQuery = ''; // ❌ Only SearchBox uses this bool menuOpen = false; // ❌ Only Navbar uses this int currentTab = 0; // ❌ Only TabPanel uses this }
// ✅ CORRECT - State where it's used final class SearchBox extends Component { String searchQuery = ''; // ✅ Local to SearchBox }
final class Navbar extends Component { bool menuOpen = false; // ✅ Local to Navbar }
Why this is bad: Lifting state before it needs to be shared creates unnecessary coupling and makes components harder to reuse. Keep state local until sharing is needed. ❌ Using morph() in onInput for Text morph_in_input.dart Copy
// ❌ ANTI-PATTERN void handleInput(Event e) { final target = e.target as HTMLInputElement; morph(() => inputValue = target.value); // ❌ Loses focus }
// ✅ CORRECT void handleInput(Event e) { final target = e.target as HTMLInputElement; inputValue = target.value; // ✅ No morph - keeps focus }
Why this is bad: Using morph() in onInput for text inputs causes the input to lose focus on every keystroke, making typing impossible. Event Handling ❌ Calling Handlers Instead of Passing References calling_handlers.dart Copy
// ❌ ANTI-PATTERN Button().onClick(handleClick())(['Click']) // ❌ Calls immediately
// ✅ CORRECT Button().onClick(handleClick)(['Click']) // ✅ Passes reference
Why this is bad: Calling the handler immediately executes it during render, not when the event fires. This breaks event handling completely. ❌ Anonymous Functions Everywhere anon_functions.dart Copy
// ❌ ANTI-PATTERN @override Morphic render() { return Div()([ Button().onClick((e) => morph(() => count++))(['+']), // ❌ Button().onClick((e) => morph(() => count--))(['-']), // ❌ Button().onClick((e) { // ❌ e.preventDefault(); morph(() => submitted = true); })(['Submit']), ]); }
// ✅ CORRECT void increment(Event e) { morph(() => count++); }
void decrement(Event e) { morph(() => count--); }
void handleSubmit(Event e) { e.preventDefault(); morph(() => submitted = true); }
@override Morphic render() { return Div()([ Button().onClick(increment)(['+']), Button().onClick(decrement)(['-']), Button().onClick(handleSubmit)(['Submit']), ]); }
Why this is bad: Anonymous functions in render() make code harder to test, harder to read, and clutter the render method. Use named methods instead. Routing ❌ Manual Routing in render() manual_routing.dart Copy
// ❌ ANTI-PATTERN @override Morphic render() { final path = window.location.pathname;
Component content; if (path == '/home') { content = HomePage(); } else if (path == '/about') { content = AboutPage(); } else { content = NotFoundPage(); }
return Div()([content]); // ❌ Impure render }
// ✅ CORRECT late final Router router = Router( routes: [ Route(path: '/home', builder: () => HomePage()), Route(path: '/about', builder: () => AboutPage()), ], notFound: () => NotFoundPage(), );
@override Morphic render() { return Main()([ RouterComponent(router: router), ]); }
Why this is bad: Manual routing in render() violates Pure Description and creates unnecessary complexity. Use RouterComponent for routing logic. ❌ Creating Stateful Routes in Builders stateful_in_builder.dart Copy
// ❌ ANTI-PATTERN Router( routes: [ Route( path: '/dashboard', builder: (_) => DashboardPage(), // ❌ New instance, state lost ), ], )
// ✅ CORRECT final class App extends Component { final DashboardPage dashboard = DashboardPage();
late final Router router = Router( routes: [ Route( path: '/dashboard', builder: (_) => dashboard, // ✅ Returns field ), ], ); }
Why this is bad: Creating new instances in builders loses component state on every navigation. Store stateful components as fields. Styling ❌ Using Strings for Variants string_variants.dart Copy
// ❌ ANTI-PATTERN Button('Submit', variant: 'primary') Button('Cancel', variant: 'primry') // ❌ Typo at runtime
// ✅ CORRECT enum ButtonVariant { primary, secondary }
Button('Submit', variant: ButtonVariant.primary) Button('Cancel', variant: ButtonVariant.primry) // ✅ Compile error
Why this is bad: Strings are error-prone and provide no type safety. Typos become runtime bugs. Use enums for variants. ❌ Complex Class Logic Inline inline_class_logic.dart Copy
// ❌ ANTI-PATTERN @override Morphic render() { return Div().classes( 'card ${elevated ? "card-elevated" : ""} ' '${hoverable ? "card-hover" : ""} ' '${bordered ? "card-bordered" : ""} ' 'card-$size card-$theme' )([...]); }
// ✅ CORRECT class CardStyles { String get classes { /* logic here */ } }
@override Morphic render() { return Div().classes(cardStyles.classes)([...]); }
Why this is bad: Complex class logic in render() is hard to read and hard to test. Extract to domain styling classes. Architecture ❌ God Components god_component.dart Copy
// ❌ ANTI-PATTERN final class DashboardPage extends Component { // 50+ fields // 30+ methods // 500+ lines of render() // Handles: users, analytics, settings, notifications, etc. }
// ✅ CORRECT final class DashboardPage extends Component { final UserSection users = UserSection(); final AnalyticsSection analytics = AnalyticsSection(); final SettingsPanel settings = SettingsPanel(); final NotificationCenter notifications = NotificationCenter();
@override Morphic render() { return Div()([ users, analytics, settings, notifications, ]); } }
Why this is bad: Components with too many responsibilities are hard to understand, hard to test, and hard to maintain. Break them into focused components. ❌ Premature Abstraction premature_abstraction.dart Copy
// ❌ ANTI-PATTERN - Creating abstractions before they're needed abstract class StyleProvider { String getButtonStyles(); String getCardStyles(); }
class MaterialStyleProvider implements StyleProvider { ... } class TailwindStyleProvider implements StyleProvider { ... }
// ✅ CORRECT - Abstract when pattern is clear // Start simple: Button().classes('btn btn-primary')([])
// Abstract when complexity emerges: class ButtonStyles { static const primary = 'btn btn-primary'; }
Button().classes(ButtonStyles.primary)([])
Why this is bad: Abstractions created before their need is clear add complexity without value. Start simple, abstract when patterns emerge. Common Themes
Most anti-patterns share common characteristics:
Hidden complexity — Logic buried in render() or anonymous functions
Broken identity — Creating components in render() or route builders
Implicit dependencies — Global state instead of constructor parameters
Type unsafety — Strings instead of enums
Unclear ownership — State lifted too early or too late
Core principle: Anti-patterns emerge when code prioritizes brevity over clarity, or cleverness over explicitness. Pulsar values code that is obvious, testable, and maintainable. When in doubt, choose clarity.