← Back to principles
UI
Predictable, Explicit Behaviour
Statement
Minimize magic and maximize clarity. State management, side effects, and data flow must be understandable from code, not guessed from hidden behaviour. Avoid surprising defaults.
What this means in practice
Code should communicate its intent and behaviour on the surface. A developer reading a component, function, or module should be able to understand what it does, what it depends on, and what it changes — without tracing through hidden mechanisms.
State is managed explicitly and owned in clearly defined locations, not scattered across implicit global stores or framework internals.
Side effects (API calls, subscriptions, DOM mutations, timers) are visible where they occur, not buried inside seemingly pure abstractions.
Data flow is traceable: it should be obvious where data enters a component, how it is transformed, and where it goes next.
Defaults are sensible, documented, and unsurprising — a developer should never be caught out by behaviour they did not opt into.
Naming reflects purpose: function and variable names describe what they do, not how they are implemented.
Configuration and conventions are explicit rather than relying on magic strings, implicit file-based routing, or framework-specific naming conventions that are not immediately obvious.
Why this matters
Implicit behaviour is the enemy of maintainability and debugging.
Developers waste significant time tracing bugs through hidden state mutations, unexpected re-renders, and undocumented side effects.
Magic (auto-wiring, implicit subscriptions, convention-over-configuration without visibility) creates a steep learning curve and makes onboarding harder.
Surprising defaults lead to subtle production bugs that are difficult to reproduce and diagnose.
When behaviour is explicit, code reviews are more effective because reviewers can assess correctness from the code itself.
Explicit code is easier to test because dependencies and effects are visible and can be controlled.
Teams move faster when they can trust what they read without needing deep framework-specific knowledge to fill in the gaps.
Practices that meet this principle
Declare state close to where it is used, with clear ownership — avoid deeply nested or globally shared state unless genuinely required.
Make side effects explicit: use clearly named hooks, service methods, or dedicated layers rather than embedding effects inside computed properties or getters.
Prefer props and parameters over implicit context or ambient state for passing data between components and functions.
Avoid framework features that rely on naming conventions or file structure to trigger behaviour unless the convention is well-documented and widely understood by the team.
Use TypeScript (or equivalent type systems) to make data shapes and function contracts explicit at the code level.
When introducing a significant new pattern or abstraction, include a doc explaining the "why" — especially if the mechanism is non-obvious.
Treat
useEffectas a last resort, not a default tool — it is a side effect and should be used sparingly. Prefer derived state, event handlers, and framework primitives before reaching for an effect. EveryuseEffectshould have a clear justification for why the work cannot be done elsewhere.Favour straightforward control flow (early returns, flat conditionals) over cleverness (nested ternaries, short-circuit chains, implicit coercion).
Validation
A project meets this principle when:
A developer unfamiliar with a module can read the code and correctly describe its state, data flow, and side effects without running it or consulting someone.
State ownership is documented or immediately apparent from the code structure.
Side effects are isolated in clearly identifiable locations, not interleaved with rendering or pure logic.
Default values for props, configuration, and library options are documented and reviewed for safety.
Code reviews consistently confirm that behaviour is understandable from the code alone, without relying on framework magic or tribal knowledge.
New team members can understand the data flow of a feature without requiring a walkthrough from an existing developer.
Potential blockers
Framework conventions that encourage implicit behaviour by design (e.g. auto-imports, magic file-based routing, implicit dependency injection).
Existing codebases with deeply embedded implicit patterns that are costly to refactor.
Pressure to move fast leading to "it just works" shortcuts without documenting why or how.
Over-reliance on framework-specific abstractions that obscure the underlying data flow.