Talks

Refactoring Volatile Views into Cohesive Components

Refactoring Volatile Views into Cohesive Components

by Jeremy Smith

In the presentation "Refactoring Volatile Views into Cohesive Components," Jeremy Smith discusses the complexities and volatility within the view layer of web development, particularly in Ruby on Rails. He highlights that the view layer, often seen as simpler than the back-end, has grown increasingly complicated due to factors like accumulation, variation, and proliferation of UI elements. Jeremy emphasizes the challenges developers face due to frequent changes, insufficient test coverage, and high complexity associated with traditional views while advocating for the use of view components to enhance organization and stability.

Key points discussed:
- Volatility in Views: Jeremy defines volatility in the context of web views as having high complexity and high churn, where churn refers to the frequency of change in UI elements and complexity arises from accumulation, variation, and proliferation of functions within views.
- Dimensions of Complexity: He outlines various factors contributing to view complexity such as browser compatibility, email client intricacies, multi-tenancy theming, SEO implications, execution context, and testing affordance, which can complicate the development process.
- Introduction to View Components: Jeremy presents view components as Ruby objects with their own templates, contrasting them with traditional views that might reference multiple objects. He promotes the use of the ViewComponent library to address view layer challenges.
- Categories of View Components: Examples are provided for utility components (like buttons and alerts), layout components (such as page structures), and model-specific components (tied to application domains), illustrating their practical implementations using Ruby classes and templates.
- Refactoring Example: A detailed case study is presented concerning the development of a navigation bar for a CRM application. Jeremy demonstrates how initial ERB templates became complicated with changing requirements over time and how refactoring into view components helped organize the code, enabling clearer structure and flexibility.
- Comparison with Partials and Helpers: He discusses the limitations of using traditional partials and helpers in achieving maintainable designs and advocates for the abstraction layers that view components offer, aiding in better collaboration and design clarity.

Main Takeaways:
- Monitor view complexity and recognize volatility signs to refactor efficiently into view components as needed.
- Use components to simplify testing and enhance collaboration amid changing requirements.
- Leverage libraries like ViewComponent to adapt development practices and promote stability within the view layer, paving the way for better user interfaces in applications.

00:00:08 Welcome to "Refactoring Volatile Views into Cohesive Components." My name is Jeremy Smith. This is my avatar and handle in most places online. I am a product-focused Rails developer, running a tiny one-person web studio. Someone asked me the other day if that was just freelancing. Yes, it is essentially that, although I've been doing this for ten years. I've realized that one of the essential aspects of running a business is thinking like a business.
00:00:26 I'm also the co-host of the Indie Rails Podcast with my good friend Jess. We just released our 36th episode earlier this week, where we discuss independent business and Rails. Last year, I organized the Blue Ridge Ruby Conference in the mountains of Western North Carolina, which was one of my biggest accomplishments and one of the most gratifying things I've ever done.
00:00:54 One of the things I've learned from that experience is how hard it is to put on a conference. I have a tremendous respect for conference organizers and understand how important they are for community building. I enjoy being there at the beginning, at the birth of a conference, so it's great to be here with you today.
00:01:31 Today, I want to talk about views. To be clear, I don’t mean views in the sense of scenic views or even the views you might find in a gallery. I'm talking about views in web development—the view layer where we design user interfaces.
00:01:45 My entrance into web development and programming was through the view layer, starting with learning HTML from sites like Web Monkey. Does anyone remember Web Monkey? A few hands—great! I began building web applications with tools like FileMaker Pro and its template language. For many years, I worked as a hybrid designer-developer, creating layouts in Photoshop, slicing them into HTML and CSS, and building WordPress themes in PHP.
00:02:03 Eventually, I found Ruby on Rails, learned what object-oriented programming was, and expanded further into server-side development while keeping one foot in the client-side arena. If your journey is anything like mine, starting from front-end and moving to back-end, you may have developed a certain bias—thinking that the view layer is simple or entry-level compared to the back-end, which is often considered the tough part.
00:02:32 However, I believe this perspective is misguided. The view layer may have once been easy, but it has become quite intricate and, dare I say, volatile. When I refer to volatility in views for the context of this talk, I’m borrowing and loosely reinterpreting concepts from Michael Feathers to say that a view component can be classified as volatile when it exhibits high complexity and high churn.
00:03:03 Let’s take a moment to consider these two concepts. Churn typically refers to the frequency of change associated with something. In the context of views, it might be more pertinent to consider churn at the level of the UI element, which can involve multiple files including markup, styling, and behavior.
00:03:45 Complexity in UI elements, on the other hand, can manifest in several ways. My observations categorize this into three areas of growth: accumulation, variation, and proliferation. Accumulation refers to the growing attributes and responsibilities of an element; variation involves additional options or differences in those attributes; and proliferation means an increase in the instances of that element in the system.
00:04:03 To illustrate, accumulation leads to more functionality, variation results in multiple forms of the same functionality, while proliferation dictates more pervasive use. I've also observed various dimensions of complexity that can emerge within the view layer. Here's an exhaustive list of dimensions as I've come to understand them.
00:04:36 Some factors are more obvious than others. For instance, consider browser compatibility. In the earlier days, this was a more significant concern, as we dealt with CSS hacks and JavaScript shims for everything. Today, it's somewhat less of an issue, but email clients remain particularly tricky to work with. Additionally, theming and white-labeling are not always at the forefront of our minds, but they can increase complexity when handling multi-tenant applications.
00:05:06 For example, allowing for user-specified themes or creating an app to be rebranded by other users can complicate the nuances of colors and styles used in the interface. Another complexity arises with SEO and Open Graph considerations. You may have heard stories about redesigned websites suffering significant SEO losses due to various factors related to how content is rendered.
00:05:28 There are other factors to consider, such as execution context in rendering views, especially when determining if they’re being rendered in the context of a request-response cycle or within a background job, which doesn’t have access to certain variables like the current user. I’ve also encountered a concept I call 'testing affordance.' This pertains to when changes in design inadvertently break existing feature specs or system tests, not necessarily because components have disappeared but due to slight changes like renaming IDs or classes.
00:05:59 Given the extensive code found in views, the multitude of responsibilities, and complexities I've pointed out, plus the tendency for code to change frequently and often lacks sufficient test coverage, it's no surprise that the humble template can struggle under pressure. In light of this chaos, I want to direct our focus to view components.
00:06:32 So, what are view components? To put it simply, traditional views are templates that can reference any number of objects, variables, or methods. In contrast, a view component is a Ruby object representing itself with a template representation.
00:06:55 We'll be using the ViewComponent library, which Hans introduced at the OSS Expo yesterday, but there are other Ruby alternatives that I'll outline in the list of resources at the end of this talk. Personally, I care less about which library you choose than that you find a view component library to experiment with and discover a solution that works for you.
00:07:33 All the code I will share during this talk can be found in this repository, so feel free to follow along or check it out later if you wish. My examples will be centered around a fairly vanilla Rails application, heavily leaning on Action View helpers alongside Tailwind for CSS and Stimulus for JavaScript.
00:08:10 Let’s delve into a few examples of how I perceive components falling into three categories: utility, layout, and model-specific. Utility components generally include buttons, cards, breadcrumbs, and banners—elements typically found in UI kits like Tailwind UI and design systems such as GitHub's Primer.
00:08:59 Here, we have four examples of an alert component, which varies its color style based on the alert type. It requires a message and can also include an optional title and icon. The Ruby class for this alert component, as you can see, initializes with a title and optionality for title and icon. It renders one message as a slot in the view component, allowing for easy naming and rendering of multiple blocks.
00:09:30 The class also sets the constant color styling corresponding to each alert type. The template representation of this Ruby class shows how the tag method helps structure the functionality. Besides the render call for the icon, all other calls reference an instance variable or method relating to the alert object.
00:09:57 It's straightforward to see that the icon and title are rendered conditionally. Now let’s explore how we can invoke this component from the view. The examples I've presented earlier can be constructed using whichever method you prefer to pass the message slot, whether by string argument or as a block. Moving on to layout components, they serve to structure your elements on a page and may sometimes go unnoticed.
00:10:36 For instance, when considering a layout component, the logic of the overall layout plays a critical role, even if it seems invisible. In the context of these components, we can identify two instances that dictate a clear structure: a header with a title, an optional parent link, a main action, and a body underneath.
00:11:02 Here is the Ruby class for that page layout component, which requires a title and an optional parent link, rendering a single action and body. The template for this page layout component illustrates how to manage layout while incorporating style and functionality effectively.
00:11:31 In another example, I’ve maintained the CSS classes within the component template, but you can adopt your preferred convention, whether using utility-style frameworks or adhering to your own front-end methodologies.
00:12:08 The readability of the template can suffer if you entangle extensive HTML attributes. Let's look at how this page layout component is called in the view, where the action and body calls integrate effectively within the layout.
00:12:55 Finally, the third category includes model-specific components, which typically relate tightly to domain concepts within the application.
00:13:06 For instance, a contact card tied directly to our CRM's contact domain model presents a unique situation. Designing with care is critical; if this is the only instance of the component type, I would advise against an attempt to make it more generic until other cases emerge.
00:13:28 Consider the Ruby class for our contact card component, which accepts an instance of the contact model. It can integrate with an avatar component to decide if it should display the contact's avatar or a styled circle reflecting the contact's first initial. The corresponding template is designed to minimize duplication without sacrificing clarity.
00:14:08 In our scenarios, we might find opportunities to optimize templates further but should approach this carefully on the first pass.
00:14:19 Next, let us illustrate how complexity can escalate in developing views and how we might refactor this into components. Suppose we’re part of a dev team for a CRM product tasked with constructing a tab navigation bar containing links to the user dashboard, contacts, companies, and tasks.
00:15:00 Initially, we might create a new ERB file with a nav element housing four links. The product team opts for this, appreciating the swift turnaround. However, the designers soon request icons beside each tab.
00:15:34 After implementing the requested icons, they further ask for a visual indicator for the current tab using a bold text style and a red bottom border. Subsequently, we introduce a negative margin to create the visual effect. Users express satisfaction with this result, yet the product team soon requests a counter displaying task records.
00:16:12 When additional complexity is implemented, such as conditional styling for a 'task' tab based on the total number of records, the design still seems to suffice. Yet, soon after, we face a request to disable a tab for accounts not on the Pro Plan, providing an explanatory tooltip.
00:16:55 This progression of requirements leads us back to our ERB template, where we check if the current account is on the desired plan and render the appropriate link or span element that mimics a link. Despite it appearing to please everyone, users still struggle to discover app configuration options.
00:18:07 As a result, we introduce another tab that reveals various settings pages but ensures visibility for users with an admin role. Incorporating conditional checks within the template becomes necessary, ultimately requiring additional wrappers and styles to implement the desired drop-down functionality.
00:19:08 Following this, the product team suggests adding a search box at the far-right end of the navigation bar, which upon conditions being met, leads us to perform numerous updates in positioning and realizing that the search feature functionality might alter their needs in the future.
00:20:04 Having coded multiple ERB templates and experiencing higher volatility, it is time to refactor this navigation system into view components. We can start by creating a component specifically for the navigation bar itself. This component would render multiple tabs and account for the additional area where the search box might appear.
00:21:15 After identifying there are three distinct types of tabs—link, disabled, and drop-down—we would leverage polymorphic slots to enable flexibility within our tab component structure. This realization emphasizes the idea that had we started structuring our components early on, we could have circumvented the complexity accumulated during development.
00:22:05 Nonetheless, to facilitate better organization and clarity, we should implement a shared base tab component that encompasses common functionality, subsequently creating specific subclasses to handle unique behaviors for each tab type. This design brings clarity and clean logic into the navigation structure, minimizing redundancy and offering built-in flexibility.
00:23:06 In the view component structure, the navigation bar's rendering could utilize these polymorphic types efficiently composed of slots accepting multiple types of child elements while retaining a clear understanding of each component’s responsibilities.
00:23:58 Some may question whether partials and helpers could suffice for this type of component logic. While that can indeed be a feasible starting point, I strongly advocate for the additional abstraction layers that view components provide. The distinctions shared by view components compared to traditional partials give rise to a more explicit and manageable design structure and enhance collaboration with designers and other developers.
00:24:58 To summarize, I recommend implementing designs using traditional templates initially, monitoring for signs of volatility through changes in complexity, and ultimately extracting these components when necessary to restore stability. Testing becomes more efficient within components and can be done in a manner aligned with your existing confidence levels.
00:25:58 Finally, I wish to extend my gratitude to Casper for his invaluable feedback, as well as the conference organizers for their support and for this opportunity to discuss view components with all of you. Thank you.