Talks

Component Driven UI with ViewComponent

wroc_love.rb 2024

00:00:03.930 (Applause) Hello, everybody. Is this on? Okay, great!
00:00:11.920 Hello! I'm Radoslav Stankov. You can call me R. I come from Bulgaria.
00:00:17.520 I have a newsletter called 'Tips at arankof.com', where I share everything. All my slides are uploaded on Speaker Deck already, as I tend to include a lot of content in my slides. I present quite quickly, and people often try to take pictures while I switch the slides, so don't worry about it—everything will be online.
00:00:30.080 I used to be the CTO of Product Hunt, where we had an architecture that I really liked. I was quite happy with it.
00:00:37.760 We had a Rails backend integrated with a GraphQL layer that connected to Apollo and Next.js. It fit our needs exceedingly well. While working at Product Hunt, I began a side project called 'Angry Building'. I jokingly said that the goal of the company was to rename it to 'Happy Building' because everyone who uses it should feel happy after their experience. This is essentially an ERP system for facility management.
00:01:11.880 As I was starting this system during my weekends and spare time, I put a great deal of thought into how to architect it. I knew I wanted to use Ruby on Rails because I'm proficient with it. I enjoy JavaScript but didn’t want to write a lot of it since it's harder to test. I also didn’t want to write much CSS, even though I like it. Instead, I wanted to try Tailwind CSS and focus on extensive end-to-end tests because it's essential to ensure that everything works smoothly from start to end.
00:01:37.960 The primary focus of the project should be the domain itself. I didn't want to reinvent the wheel or build unnecessary technologies. So, I love Rails, but there is always a 'but' in the story.
00:01:45.480 The first thing is that in a Rails project, there are two folders we always see—'helpers' and 'views'. When I think about these folders, I feel overwhelmed by how complex it can get. You start pulling a partial, go to a helper, pull another partial, and it quickly becomes messy. About eight years ago, before I started doing a lot with React, I began to think about building UIs in terms of components.
00:02:25.000 Fortunately, GitHub released a gem called 'ViewComponents', which provides a system for building components in the view layer. It's very aptly named. If you think about a component, say, a 'field set' for entering a bank account number, your component consists of a title and content.
00:02:40.000 In your Ruby code, you can create a new instance of your field set component that includes a form with the necessary inputs. This is how a view component looks: you inherit from 'ViewComponent::Base', accept the title as an instance variable, and create a neat ERB template for your field set.
00:03:10.000 The content for the field set comes from the block passed to the view component. This allows you to keep things simple. Since we are in the Ruby on Rails world, we're fortunate to have fantastic tools, and ViewComponents have a feature called previews. Previews allow you to render your component in a safe space where you can visually see how it looks, similar to how you create and preview mail templates.
00:04:00.000 If you add a gem called 'Lookbook', you can display all your components with their previews. You can check everything, ensuring that stuff works as it should. After this, you’ll feel like an expert in ViewComponents, and at this point, my job could be considered done. However, let's dig a bit deeper; there's more to explore regarding ViewComponents.
00:04:58.360 While using ViewComponents, the devil is always in the details. You’ll have questions about what belongs in helpers, what belongs in views, what constitutes a partial, and what defines a ViewComponent. I personally dislike opening a view only to be confronted with a wall of partials—it's frustrating and slow. If someone says, 'Now we're going to start using ViewComponents', but ends up with the same mess, it's not a useful transition.
00:06:00.920 To manage this, I have a mental checklist I refer to when deciding whether to use a ViewComponent or not. Every tool has its proper place, and we as engineers need to know when to employ each solution. To use ViewComponents effectively, I consider the following: if I find a group of code that is used across multiple controllers, I move that code into a component.
00:06:59.200 If I'm working with a view helper that generates HTML longer than two or three lines, I also move it to a ViewComponent. Additionally, for complicated if-else logic that gets confusing, I transition that logic into a ViewComponent where it can be tested more easily. If I find myself copy-pasting code frequently and just changing a few lines, I opt to use ViewComponents as well.
00:07:29.680 When JavaScript comes into the mix, such as needing a particular component to have JavaScript functionality, it often makes sense to wrap it in a ViewComponent for easier maintenance. However, there are still places where I use partials, particularly in underscore forms. For instance, I have a partial that I use for forms, and it suffices because it's not a performance bottleneck.
00:08:04.559 I also use about 20 view helpers that are essentially simple functions, like one that formats money. There are some instances where I don't extract ViewComponents from messy HTML because it can be harder to address later. I prefer to isolate messy code until I have a better understanding of how to fix it, so I can address it when I'm ready.
00:09:00.597 So, my mindset towards ViewComponents is this: consider the helpers we use inside them. ViewComponents should represent domain components, drawing a parallel between view components and domain components. I can have a helper that formats money while still having a money component that, based on its state, formats its appearance—positive being green, negative being red, and zero being blue.
00:09:53.479 Moreover, I can have a product price component that represents the instance of the product and displays its price. It can also incorporate discount codes or other relevant information. What I don't want to see is a header component, as it’s generally not reusable; it's specific to one page. This layered approach to components is how I structure my code.
00:10:55.360 At this point, though I’ve covered quite a bit in theory, let's see some real code in action. For example, I really don't like typing 'render field set component title' as it's too verbose, so I created a helper called 'component', where you just pass a symbol, leading to a simpler interpolation for rendering.
00:11:37.119 Below is a page from my application—half of it is structured this way since it functions as an ERP system, effectively rendering Excel spreadsheets with enhanced styling. If we break this down, the app essentially consists of several components including navigation, page header, filter form, stats, and table components—these are all reusable components.
00:12:26.079 The organization of the components keeps everything readable and consistent. Let’s zoom in on a specific section of the code which presents statistics. The stats component fetches and displays various statistics.
00:12:39.960 Its code uses my component helper, employing a simple syntax reminiscent of a domain-specific language. Within the stats component, I define a list of smaller stats, where every stat can be presented as its own stats number component. While I have only implemented stats numbers presently, this could evolve to include stats strings or more advanced variations in the future.
00:13:37.919 The stats component merely renders multiple stats number components, which is the entirety of its implementation. The goal is to keep everything clean and efficient. I utilize such helpers like 'with_number' to simplify rendering, creating a more readable implementation.
00:14:41.320 Beyond this, another essential component involves the filter form, which has a range of inputs that I’ve built around a custom UI. I refer to this as the Builder pattern. The form component elements closely resemble the form helpers in Rails. This component accepts two attributes: the action URL and an array of inputs.
00:15:27.480 This was developed before I learned about slots in ViewComponents. In this case, the block only executes when requested, as opposed to slots, which execute right away. I have multiple methods for rendering various input types which could easily be adapted into more sophisticated selections in the future. Regardless of whether I switch to more advanced UIs, I can change the implementation of the input types without having to overhaul the entire system.
00:16:29.439 For instance, I include a date range input, constructed simply by looping through elements, displaying labels and inputs without unnecessary complexity. Such organization in my code streamlines maintenance and provides the opportunity to adapt as necessary without extensive rewrites.
00:17:09.679 The page header component is another key aspect that incorporates logic, encapsulated within its own component. Here, I manage breadcrumbs for navigation. The header itself accepts various objects, distinguishing responsibilities between breadcrumbs and actions on the page.
00:18:08.560 When building this component, I incorporated slots, allowing for flexible inclusion of breadcrumb items and action slots. I also deploy further helpers for internalization among various languages. This consideration enhances accessibility as I tailor text elements for display based on language context.
00:18:34.440 Before rendering, I check if a specific title hasn’t been provided and try to inflect it based on built names and resources in translation tables. Should an explicit title not be provided, I derive one based on the resource context. This logic not only simplifies the title management but ensures consistency across pages.
00:19:30.679 Moving on to the core component of my application—the table component—its evolution over the years reflects a blend of personal necessity and observed best practices. Originally a rendering helper, this component grew into a view component that accepts tables and columns for dynamic data display.
00:20:02.239 Users can define the column and apply formatting easily through a simplified interface. I’ve added complex data integration into the table component, ensuring it can accommodate datasets ranging from arrays to active record associations.
00:21:01.719 The table maintains simple, readable logic, where all components can be efficiently rendered based on the specified column definitions and types. Overrides can also cater to unique cases, facilitating tailored interactions while safeguarding the overall structure.
00:22:00.639 Overall, it is important to remember that through utilizing ViewComponents, I can create a more structured, layer-based approach that lends itself to better testing and maintenance. This layered abstraction model allows for swift modifications as business needs evolve.
00:22:45.159 Now that we've explored the theory and abstract applications, let’s take a moment for questions. I have experience with React, and while both React and ViewComponents have their unique strengths, their differences are ultimately rooted in the use cases for which they were designed. Particularly, consider the interactivity needs of your application and how those trade-offs will shape your technology choices.
00:38:40.119 If your application thrives on dynamic user interactions, you may lean towards React. However, if the backend functionality is central to your needs, utilizing ViewComponents in Rails can streamline your development workflow effectively.
00:39:00.000 Moreover, while ViewComponents offer promising testability through the use of previews, I typically indulge more in smoke testing rather than unit tests. Having these previews set up allows me to check component behavior without diving deep into unit testing specifics.
00:39:37.239 These considerations ensure that your application functions correctly without getting bogged down in unnecessary minutiae. Thanks for your attention, and I'm ready for any questions you may have!
00:39:56.719 (Applause) Thank you!