Single-Responsibility Component Design: Learning Abstraction and Reuse from message-scroller

How the message-scroller component demonstrates single-responsibility design, abstraction, and reuse in front-end engineering.
Using the message-scroller component as a case study, this article explores the Single Responsibility Principle in front-end design—covering SOLID origins, virtual scrolling internals, the Rule of Three, the evolution from Mixins to Hooks, and how thoughtful API boundaries and abstraction pay off over the long term.
Component Design Philosophy Through message-scroller
In front-end development and interface engineering, designing a component that is both focused and reusable has always been a core challenge that engineers must weigh repeatedly. Recently, a developer shared insights on a social platform about building a message-scroller component: this component only handles scrolling logic and does nothing else. He admitted that he invested considerable effort to get the abstraction just right, but in terms of long-term returns, that investment was well worth it.
Behind this brief share lies an enduring principle in software engineering—the Single Responsibility Principle (SRP). It seems simple, yet it is the true dividing line between excellent architecture and bloated code. It's worth noting that the value of single responsibility manifests not only at the level of code organization but also profoundly affects a developer's cognitive load. Cognitive load theory originated from research by educational psychologist John Sweller and was later introduced into software engineering: when a module carries too many responsibilities, the developer must simultaneously hold multiple logical threads in mind while reading, debugging, or modifying code, rapidly exhausting the brain's working memory capacity, with error rates rising accordingly. A single-responsibility component is the opposite—the developer only needs to understand one thing, the comprehension cost is minimal, and the risk of changes is correspondingly reduced. This perspective reminds us that good component design is, at its core, a kind of considerate response to the cognitive limitations of humans.

Single Responsibility: Do One Thing, Do It Well
The Historical Origins of the Single Responsibility Principle
SRP was first systematically articulated by Robert C. Martin (a.k.a. "Uncle Bob") in his book Agile Software Development: Principles, Patterns, and Practices, and became the core idea represented by the initial "S" in the SOLID principles. SOLID is a collection of five principles of object-oriented design, covering Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
The SOLID principles were born in the historical context of the early 2000s as the object-oriented programming paradigm matured, representing the software industry's systematic response after experiencing numerous large-scale system failures. At the time, enterprise Java and C++ projects frequently fell into the trap of "code rot"—every requirement change felt like navigating a minefield, where touching one thing affected everything. Martin's contribution was to distill the tacit knowledge scattered across design pattern literature and engineering practice into five actionable, teachable concrete guidelines, enabling teams to establish a common language in code reviews and architecture discussions. Notably, these five principles are not independent of one another but mutually supportive: following SRP often naturally leads to interface segregation, while dependency inversion provides the technical foundation for implementing the open/closed principle. Understanding this historical background helps us realize that SOLID is not dogma conjured out of thin air, but rather the crystallization of painful engineering experiences—each principle corresponds to real system collapses and maintenance disasters that actually happened in the real world.
The classic definition of SRP is: "A module should have only one reason to change." Here, the "reason to change" essentially refers to different business stakeholders or usage scenarios—when a module must be modified due to requirement changes coming from multiple different directions, it carries too many responsibilities, and any single change may unexpectedly affect other unrelated functional paths.
What "Only Handles Scrolling" Means
The developer explicitly stated that message-scroller was designed to "only handle scrolling, and do nothing else." This means it doesn't care how message content is rendered, doesn't care how data is fetched, and doesn't intervene in other details of user interaction. Its sole mission is to manage scrolling behavior.
Such restraint is uncommon in real development. Many components continuously absorb new features during iteration, eventually evolving into "do-everything" yet unmaintainable mega-modules. Extracting scrolling logic into its own component means that any scenario requiring scrolling capability—chat windows, log panels, infinite scroll lists—can directly reuse the same polished implementation without reinventing the wheel.
One of the core challenges of extracting scrolling logic into an independent component is how to design a clear API boundary. The interface a component exposes determines its usability and maintainability: an overly broad API forces the caller to shoulder unnecessary implementation details, while an overly closed API restricts the flexibility of usage scenarios. In front-end component design, the choice between "controlled" and "uncontrolled" patterns is especially critical—the controlled pattern hands state ownership back to the caller, suitable for scenarios requiring fine-grained control; the uncontrolled pattern lets the component manage its own internal state, lowering the barrier to use. An excellent message-scroller often supports both modes simultaneously, or provides an "escape hatch" mechanism (such as exposing a ref or callback function) that lets the caller intervene in low-level behavior when necessary, without breaking the component's encapsulation boundary. This deliberate thoughtfulness about API design is precisely the concrete embodiment of "spending a lot of effort getting the abstraction right."
It's worth adding that API boundary design also involves the long-term commitment issue of semantic versioning. Once an API is publicly exposed, any breaking change imposes upgrade costs on callers. Therefore, mature component libraries typically follow the principle of "subtract before you add"—the initial API should be as minimal as possible, exposing only the interfaces currently known to be needed; as usage scenarios accumulate, it can then be gradually extended in a backward-compatible manner. This "evolutionary API design" is one of the key reasons why long-lived component libraries in the open-source ecosystem (such as Lodash and date-fns) can be maintained continuously for decades. For a foundational component like message-scroller, thinking ahead about the stability boundaries of the API is just as important as thinking about the functionality itself.
Why Scrolling Deserves Its Own Abstraction
Scrolling seems simple, but in reality it conceals many complex details: auto-scrolling to the bottom, pausing auto-scroll when the user manually scrolls up, smooth transitions when new messages arrive, remembering and restoring scroll position, and virtual scrolling in performance optimization scenarios.
Before diving into the implementation of virtual scrolling, it's necessary to understand the underlying mechanisms of browser scrolling behavior. The rendering pipeline of modern browsers typically consists of the following stages: JavaScript execution → Style → Layout → Paint → Composite. Among these, the composite stage is handled by a dedicated compositor thread that runs in parallel with the main thread. Chrome's Blink engine and Safari's WebKit engine both move scrolling operations to the compositor thread as much as possible, to avoid blocking main-thread JavaScript execution and style computation, thereby achieving the effect that "even when the main thread is busy, page scrolling remains smooth."
However, when JavaScript implements custom scrolling logic by listening to scroll events and modifying the DOM, if handled improperly—such as not using the passive: true event listener option, or triggering forced synchronous layout in the scroll callback (i.e., first reading layout properties like offsetHeight in JS and then modifying styles, forcing the browser to repeatedly perform layout calculations within the current frame)—it will forcibly pull scrolling logic back to the main thread, causing dropped frames and stuttering. The seemingly trivial passive: true API option is motivated by the following engineering rationale: when the browser receives a touchstart or wheel event, by default it must wait for the JS event handler to finish executing before it can determine whether preventDefault() is needed, and this wait itself introduces latency; after declaring passive: true, the browser can start compositor-thread scroll handling in advance, without waiting for the JS execution result, thereby reducing scroll latency from tens of milliseconds to single-digit milliseconds. This is precisely why scrolling logic deserves to be carefully encapsulated by a specialized component: it requires a deep understanding of the browser rendering pipeline to strike a balance between correctness and performance.
Virtual scrolling is an important technique in modern front-end performance optimization. Its core idea is that when rendering an extremely long list, only the items within the user's current visible area are actually rendered in the DOM, rather than mounting all data onto the DOM tree. Its underlying implementation is quite ingenious: the component must continuously listen for scroll events and dynamically calculate "which range of data should be rendered" based on the scrollTop value and each item's height, while simultaneously setting top padding on the container or transform offsets to simulate the physical placeholders of vanished items, so that the browser scrollbar's proportion and position always accurately reflect the height of the full dataset. This process involves precise mathematical calculations and frequent DOM operation scheduling, and the slightest carelessness can cause scroll jitter or blank flashes.
When items in the list have variable heights (such as message bubbles containing images or collapsible content), the implementation complexity of virtual scrolling increases further. In this case, the component must maintain a dynamically updated "item height cache table," and after an item is rendered, asynchronously measure its actual height via the ResizeObserver API and write it back to the cache, while recalculating the offsets of affected items. ResizeObserver is an efficient element-size monitoring mechanism provided by modern browsers; compared to earlier approaches that polled offsetHeight via setTimeout, it only triggers a callback when an element's size actually changes, making it both accurate and energy-efficient. This two-phase strategy of "estimate first, correct by measurement" is the industry's mature approach to handling variable-height virtual lists, and is also one of the core design ideas behind libraries such as react-window's advanced fork react-window-dynamic and @tanstack/virtual.
To further reduce performance overhead, mature virtual scrolling implementations also introduce a "render buffer (overscan)"—rendering a few extra items above and below the visible area, in exchange for a smoother visual experience while scrolling. Behind this seemingly trivial optimization lies a deep understanding of the browser rendering pipeline: when the user swipes rapidly, if only the precisely visible items are rendered, the time lag between the scroll frame rate and the data calculation frame rate will cause momentary blank areas to appear; the buffer is equivalent to "pre-loading" at the data level, trading a small amount of extra memory for a perceptible improvement in smoothness. Libraries such as react-window and react-virtualized in the React ecosystem, and vue-virtual-scroller in the Vue ecosystem, are all mature implementations of this idea, hiding all the above complexity behind a clean API. For scenarios such as message lists and log panels that may contain thousands or even tens of thousands of records, virtual scrolling can reduce memory usage and rendering time by several orders of magnitude.
If this logic were coupled with business code, every place that uses scrolling would repeatedly step on the same pitfalls. Encapsulating these details into a dedicated component is precisely the classic practice of "isolating complexity within a boundary." The caller only needs to care about "I want to scroll," without worrying about "how to scroll correctly."
The Cost and Return of Abstraction
"Spending a Lot of Effort Getting the Abstraction Right"
The developer particularly emphasized that finding the right level of abstraction "took a lot of effort." This statement reveals a often-underestimated truth in engineering practice: good abstraction is not achieved in one go, but is the result of repeated refinement.
Regarding when to extract an abstraction, one of the most widely circulated practical guidelines in software engineering is the "Rule of Three" proposed by Martin Fowler and Kent Beck: the first time you write a piece of logic, just implement it directly; the second time you encounter a similar requirement, resist the urge to abstract and accept the duplication; only the third time you encounter it should you set out to extract a reusable abstraction. The logic behind this rule is that a pattern appearing twice may just be coincidence, whereas a pattern appearing three times allows you to identify a real, stable common structure with relative certainty. Premature abstraction is just as dangerous as premature optimization—it introduces unnecessary layers of indirection based on imagined future needs, actually increasing cognitive burden and maintenance cost. The Rule of Three is essentially a form of epistemological humility: before we have enough samples, our judgment about "what is truly common" is often unreliable, and once a wrong abstraction is established, the cost of removing it is often far higher than the maintenance cost of the duplicated code that would have existed without the abstraction.
Echoing the Rule of Three is a sharp observation from software engineer Sandi Metz: "Duplication is far cheaper than the wrong abstraction." This statement directly challenges many engineers' instinctive preference for "eliminating duplication." A wrong abstraction is so costly because it establishes a "lie" in the codebase that many places depend on—all callers believe they are using the same thing, but that "thing" does not actually reflect their common needs. Over time, to make the abstraction fit more and more edge cases, developers are forced to keep adding parameters, conditional branches, and special-handling logic to it, ultimately making the abstraction itself harder to understand than the duplicated code it was meant to simplify. From this perspective, the message-scroller developer's "spending a lot of effort" is precisely a serious reckoning with the hidden cost of "avoiding the establishment of a wrong abstraction."
The difficulty of abstraction design lies in getting the "degree" right. Over-abstraction introduces unnecessary complexity and layers of indirection, making simple things cumbersome; under-abstraction fails to truly isolate change, leading to responsibility leakage. Making a component both general enough to cover multiple scenarios and focused enough to stay simple requires a deep understanding of usage scenarios and multiple rounds of validation and refactoring.
The Returns Are Emerging
"It's paying off" is the most crucial signal in this share. The returns from good abstraction are often delayed—early on, you pay an extra design cost; but in subsequent reuse, maintenance, and extension, that investment continues to pay dividends in the form of fewer bugs, faster iteration, and clearer code structure.
This "hard first, sweet later" pattern has a corresponding concept in economics: the opposite of technical debt, namely technical credit. When Ward Cunningham first proposed the metaphor of technical debt, he was describing design compromises made for short-term delivery speed—like taking out a loan, you gain liquidity in the short term but must pay interest (i.e., extra maintenance cost) in the long term. Good abstraction design is the reverse operation: proactive investment up front, in exchange for continuous "interest income" later—every reuse of the component is a way of enjoying the returns of the up-front investment. Understanding this metaphor helps engineering teams more accurately assess the input-output ratio of infrastructure work in project management, rather than simply treating it as a "pure cost that generates no business value."
This also explains why senior engineers are willing to endure "hardship first, sweetness later" on abstraction. When the second and third features requiring scrolling appear, the efficiency advantage of directly reusing an existing component becomes obvious.
Lessons for Developers
Split Responsibilities, Isolate Complexity
The first lesson this case brings to developers is: identify the recurring, logically independent concerns and distill them into dedicated components. Scrolling behavior, date formatting, form validation, request retries—these cross-cutting concerns are often the best candidates for polishing into reusable abstractions.
Cross-cutting concerns are an important concept in software architecture, referring to system-level concerns that span multiple modules and cannot be cleanly encapsulated within a single module, such as logging, permission checks, transaction management, and error tracking. Aspect-Oriented Programming (AOP) is precisely the programming paradigm born to solve such problems, stripping cross-cutting logic out of business code via "aspects." The core mechanism of AOP is "weaving"—injecting cross-cutting logic into the execution flow of target methods at compile time, class-loading time, or runtime, without modifying the original business code. The @Transactional annotation in the Spring Framework is AOP's most widely known application: developers only need to add an annotation to a method, and the framework automatically weaves transaction start, commit, and rollback logic before and after method execution, while the business code itself is completely unaware of the transaction's existence.
In the front-end domain, patterns for handling cross-cutting concerns have gone through a clear evolutionary history, which is itself a history of exploration about "how to better reuse logic." Early React used the Mixin mechanism, allowing multiple components to mix in shared logic, but Mixins brought serious problems such as naming collisions, implicit dependencies, and opaque origins, and the React team eventually marked them as an anti-pattern. The subsequently popular Higher-Order Components (HOCs) injected cross-cutting logic by wrapping components in functions, solving some of Mixins' pain points but introducing new problems such as "wrapper hell" and props name shadowing—viewing a deeply nested tree of HOC components in React DevTools sometimes reveals a dozen layers of Providers and Enhancers stacked on top of the actual business component, making debugging extremely painful. The arrival of React Hooks was an important milestone in this evolution—custom Hooks encapsulate stateful cross-cutting logic into independently testable, freely composable functional units without changing the component's hierarchy, fundamentally solving the age-old ailment that "logic reuse necessarily leads to component hierarchy bloat." Vue 3's Composables share the same origin, likewise replacing inheritance and wrapping with function composition. If message-scroller were implemented in a React project, it would most likely exist in the form of a custom Hook, encapsulating scroll state and behavior into useMessageScroller()—this is precisely the concrete manifestation of this idea in the domain of scrolling.
It's worth mentioning that the design of the Hooks model itself also embodies the spirit of the single responsibility principle: a custom Hook should encapsulate only one category of related state and behavior, rather than trying to become an "all-purpose toolbox." useMessageScroller() focuses on scrolling, useWebSocket() focuses on connection management, useVirtualList() focuses on virtualized rendering—these Hooks can be freely composed when used, independently validated when tested, and swapped without affecting one another. This "function-granularity single responsibility" is the natural extension of front-end componentization thinking at the logic layer.
Anchor Abstractions in Real Needs
The second lesson is: abstractions should stem from real, recurring needs, not premature speculation. The developer invested effort in abstraction design only after clearly identifying the concrete pain point of "needing to handle scrolling." This path of "pain point first, abstraction later" is more reliable and easier to implement than speculative over-engineering. As the Rule of Three reveals, only after needs genuinely recur will the distilled abstraction possess enough stability and representativeness.
This principle also has an important corollary in practice: good abstractions often come from "distilling from concrete implementations," rather than "designing downward from abstract concepts." That is, first implement the same logic separately in several concrete scenarios, accumulate enough comparative samples, and then identify commonalities, distill interfaces, and eliminate differences. Compared to "top-down" design starting from a theoretical framework, this "bottom-up" abstraction path produces APIs that better fit real usage habits and fall less often into the abstraction trap of "beautifully designed but hard to actually use."
Accept Up-Front Investment, Reap Long-Term Dividends
Good engineering practice requires accepting a certain up-front cost. As this developer said, getting the abstraction right takes effort, but that effort continues to pay off throughout the project's lifecycle. Between short-term convenience and long-term maintainability, mature engineers typically choose the latter.
Summary
message-scroller is a tiny yet typical sample of engineering practice. It reminds us that excellent software is not composed of do-everything mega-modules, but is built through the collaboration of countless small components with clear responsibilities and well-defined boundaries. From the theoretical foundation and historical origins of the SOLID principles, to the underlying implementation mechanisms of virtual scrolling (including the ResizeObserver strategy and two-phase rendering approach in variable-height scenarios) and the engineering trade-offs of the render buffer, to the Rule of Three and Sandi Metz's warning that "the wrong abstraction is more costly than duplication" jointly guiding the timing of abstraction, and the evolutionary trajectory of front-end patterns from Mixins to Hooks along with the front-end application of AOP thinking—these pieces of engineering wisdom all point to the same conclusion: focus on doing one thing well, and encapsulate complexity within a reasonable abstraction. At the same time, understanding the browser compositor thread mechanism and the passive event listener option reminds us that even the single thing of "just doing scrolling" internally holds a deep grasp of low-level system behavior; the economic metaphor of technical credit tells us that up-front design investment is a long-term asset with compound-interest effects; and attention to cognitive load tells us that good component design is ultimately an act of kind consideration for the user's brain. This restraint and patience are precisely the foundation of high-quality component design, and the necessary path for front-end engineering to reach maturity.
Key Takeaways
- The Single Responsibility Principle is the dividing line between excellent architecture and bloated code, and its value manifests in two dimensions simultaneously: code organization and reducing cognitive load
- Good abstraction requires repeated refinement: the Rule of Three and "the wrong abstraction is more costly than duplication" both point to the same conclusion—restrain the impulse toward premature abstraction, and let real needs provide the anchor for abstraction
- A deep understanding of the browser rendering pipeline is a prerequisite for implementing high-performance scrolling components: mechanisms such as the compositor thread,
passiveevent listeners, and forced synchronous layout determine the performance gulf between "doing scrolling correctly" and "doing scrolling carelessly" - Virtual scrolling boosts the rendering performance of extremely long lists by several orders of magnitude by rendering only the DOM nodes within the visible area; the ResizeObserver strategy in variable-height scenarios is an important detail of its implementation
- Front-end logic reuse patterns evolved from Mixins to HOCs to Hooks/Composables, and each evolution is a better practice of the engineering principle that "logic reuse should not come at the cost of component hierarchy bloat"
- API boundary design is just as important as functional implementation: the choice of controlled/uncontrolled patterns, the provision of escape hatch mechanisms, and considerations of semantic versioning commitments jointly determine whether a component can stand the test of time
Related articles

Kimi K3 Deep Dive: 2.8 Trillion Parameter Open-Source Model Closes the Gap with Closed-Source Frontier for the First Time
Moonshot AI releases Kimi K3 open-weight model with 2.8T parameters and 1M token context. Our deep dive covers coding, 3D dev, agent capabilities, and safety concerns.

How to Choose an AI Agent Platform for Your Team: 5 Evaluation Dimensions and a Decision Framework
Choose the right AI Agent platform by evaluating model flexibility, observability, tool integration, security compliance, and total cost. A complete decision framework to help technical leaders avoid vendor lock-in.

Legora's Legal AI Practice: How Vertical AI Empowers Law Firms and Corporate Legal Transformation
Explore Legora's legal vertical AI practice, covering AI model selection strategies, enterprise legal transformation challenges, and the path to deploying vertical AI in the legal industry.