00:00:00.120
Oh my gosh.
00:00:33.899
[Foreign]
00:00:47.899
For our next talk, let me introduce you to Joel.
00:01:04.440
Welcome, Joel! Hey, thanks, Monica. Thanks for having me. Thank you for joining us.
00:01:10.680
Well, today we have Joel Hawksley, directly from GitHub, the creator of ViewComponents.
00:01:16.740
And guess what? We're going to be talking about ViewComponents.
00:01:22.220
But the interesting part about this is not only about ViewComponents per se.
00:01:28.380
What I love is that you're going to talk about how to build a community around something new.
00:01:33.479
How do you nurture something like that? How do you open source a library of ViewComponents and actually scale an application to hundreds of them?
00:01:40.140
So, it's going to be not only about theory, but also practice, I guess, and how to bring ViewComponents to life for the whole community.
00:01:47.700
I'll leave the stage to you. Thank you so much again for being with us.
00:01:53.399
Wonderful! Thanks for having me, Monica. I really wish I was with you all in person in Italy, but this virtual experience will have to suffice.
00:02:00.420
Maybe next year we can all be drinking some wine together in some beautiful place in Italy.
00:02:07.740
Anyways, all right. Well, my name is Joel. Thanks for coming to this session.
00:02:12.900
Today, I'm going to share some lessons we've learned building ViewComponents at GitHub over the past year.
00:02:19.440
I'm an engineer on the design infrastructure team at GitHub. We're responsible for the Primer design system.
00:02:25.860
As you can see, it's really a system of systems. We have a CSS framework, React components, and a library of ViewComponents.
00:02:32.060
These are just a few of the pieces that you might expect in a modern design system these days.
00:02:37.560
My job on the GitHub design infrastructure team is to create consistent, accessible, resilient user interfaces in our Rails monolith.
00:02:43.379
We aim to make this an enjoyable experience, all at what I would call an incredible scale.
00:02:49.500
Whenever I give talks about our monolith, people always ask, 'How big is your application?'
00:02:55.860
So here's a few stats: The GitHub Rails app is over 13 years old and receives tens of thousands of requests per second.
00:03:03.480
We've extracted around two dozen services from the application into what many call a citadel architecture.
00:03:10.140
Most of our services are written in Go, addressing functional areas, but most of GitHub you interact with is still a Rails monolith.
00:03:16.670
It's a monolith that continues to grow very quickly.
00:03:23.220
To give you an idea, we have almost 600 models in our application, which has grown by about 33% over the past year.
00:03:28.620
We have almost 4,700 views, growing nearly 20%, and over 800 controllers, with a growth of almost 37% over the last year.
00:03:44.280
Most importantly for our design team, we have over 2,000 pages in the application that need to stay current and up to date with our design standards.
00:03:50.220
This scale creates interesting challenges; what may be a small annoyance in smaller Rails applications can turn into serious roadblocks for us.
00:03:56.459
I'm going to start by sharing some of those serious roadblocks.
00:04:03.660
When you have a body of work this large, patterns begin to emerge, and one challenge we encountered was identifying which template renders which part of a page.
00:04:10.620
For example, on a pull request page, if I want to edit the draft badge displaying the status of a pull request, how do I find the right place to make that change?
00:04:17.400
One way could be searching for specific class names on the element. However, since these class names aren't unique, it's not a very reliable approach.
00:04:25.140
In this case, if you search for these class names, you might receive dozens or even hundreds of results.
00:04:31.740
To address this issue, we decided to add HTML comments at the beginning and end of every template's output, including the path of the template file.
00:04:39.300
To accomplish this, we wrote a custom ERB compiler.
00:04:46.139
The ERB compiler defines a subclass of ActionView template handlers ERB and we define a call method that receives a template object.
00:04:51.479
The first thing we do is take the output of the superclass and assign it to a variable.
00:04:59.520
Then, we grab that template object and render HTML comments using the path attribute before and after the output of the template.
00:05:06.360
After that, we register the compiler as the handler for ERB files.
00:05:12.720
Now we can see which template rendered which part of a page. In this case, it's a ViewComponent we wrote to display the state of a pull request.
00:05:18.780
This annotation feature has been beneficial internally; we actually extracted this into Rails.
00:05:25.560
This is part of Rails 6.1. You can enable it with the configuration variable config.action_view.annotate_template_file_names.
00:05:32.760
If you create a new Rails application today, this annotation feature is enabled by default in local development.
00:05:39.360
Another issue we face at our scale is getting our application into the right state for visual changes.
00:05:46.320
For designers, unit tests often aren't sufficient for verifying visual changes.
00:05:53.100
A lot of these challenges arise from our citadel architecture with multiple services.
00:05:59.520
Although we have a Rails monolith and about two dozen external services, achieving smooth local reproduction can be challenging.
00:06:06.360
You might wonder why we don’t just use system tests. We’ve thought about this, but they can add significant time to our test suite.
00:06:13.740
We write plenty of controller tests, and after collaborating with our designers for a while, we identified a way forward.
00:06:20.520
What if we could convert our controller tests into system tests? That way, we could reuse the existing setup code, stubs, mocks, and fake services.
00:06:27.720
This would allow us to preview our application in a specific state.
00:06:34.440
Here’s what a controller test looks like: it calls 'GET' with a URL and makes an assertion against the response code.
00:06:42.240
To convert this into a system test, we could write a module called SystemTestConversion.
00:06:49.080
When this module is included, it registers basic configuration for ActionDispatch::SystemTestCase.
00:06:56.520
Next, we redefine the 'GET' method to visit the provided path and pause execution with a debugger.
00:07:03.240
Regarding the assert response, we can treat that as a no-op.
00:07:09.420
We then check for a runtime environment variable, which will be designated as running in a browser.
00:07:15.360
If so, we include that module into our base controller test case.
00:07:21.480
This means that we’re effectively converting a controller test into a system test at runtime.
00:07:27.840
Unlike template annotations, this feature hasn’t made it into Rails yet.
00:07:36.360
It’s dependent on our specific architecture, but it has served us well.
00:07:43.020
More generally, we've noticed that our seeds and test setup code have a lot of overlap.
00:07:49.260
I’m curious if there’s a better way we could bridge the gap between these conceptual domains, particularly with external services.
00:07:55.680
While working on these projects, we identified another source of friction.
00:08:02.880
When rendering a template at our scale, it can be difficult to know where it's used.
00:08:09.300
If you’re unaware of where a template is used, changing it becomes risky, particularly when we reuse partials frequently.
00:08:15.030
To better understand this issue, my colleague John Hawthorne created a diagram showing how our templates reference each other.
00:08:21.660
It’s like a massive ball of spaghetti, and navigating this render stack can be quite tricky.
00:08:28.830
This makes it daunting to make changes in these templates.
00:08:36.780
To get a handle on this problem, we built a tool called ViewFinder.
00:08:44.100
Here’s how it works: you pass in the path to a template, for example, the wiki show page.
00:08:52.800
If I made a change to the wiki show page and input the command, it starts by extracting the template string literal.
00:08:59.940
The string literal is 'Wiki show,' and we search for it in the code base.
00:09:06.240
This works similarly to a global text search in an editor.
00:09:12.600
For each of our search results, we load the resulting file with the Parser gem.
00:09:19.140
The parser returns what's called an Abstract Syntax Tree (AST) of the file.
00:09:25.740
For example, here's one of the matches; it happens to be a controller with part of its syntax tree.
00:09:32.639
This data structure represents how Ruby interprets the code we write.
00:09:41.160
RoboCop also works this way; it translates Ruby into these data structures for operations.
00:09:48.600
As you can see, we have our render call to 'Wiki/show,' and we’re doing this parsing step.
00:09:56.160
This confirms that those command-shift-F global search results are render calls to our specific path.
00:10:03.960
We continue this process recursively until reaching a controller action.
00:10:11.760
Once we reach the controller action, we query back up the tree to find the method definition.
00:10:18.840
In this case, we find the definition of the show method, meaning that we are in 'WikiController#show.'
00:10:25.740
From there, we look up the routes that render that controller action, which helps us locate it.
00:10:32.760
Here's the summary: we input 'bin viewfinder' followed by the path to our wiki show template.
00:10:39.960
In the end, we find two routes that route to that template.
00:10:47.520
Both the WikiController#show and the '/repository/wiki' URLs reuse this template.
00:10:54.840
This enables us to know where to go when we want to verify we haven’t broken something in our application.
00:11:01.740
On its own, this tool has been useful; we can see all the routes that rendered a specific template.
00:11:08.760
Moreover, we realized we could link these routes back to identify which controller tests rendered our template.
00:11:15.480
We started by finding all of the 'GET' calls from our controller tests.
00:11:22.740
Here's one that loads the wiki for a given repository.
00:11:30.120
We extract that path and use Rails' routing mechanism to recognize what controller action it maps to.
00:11:37.440
This returns a hash that includes the name of the controller and the action determined.
00:11:44.580
We repeat this process for every single controller test in our suite, creating a hash that we cache heavily.
00:11:52.320
This enables us to include which tests render a certain ViewComponent in our system test conversion tool.
00:11:58.380
We can verify changes to our template in a browser, allowing us to move from knowing the template’s name to providing a test.
00:12:06.840
We can automatically reproduce the page that renders without local setup in my environment.
00:12:13.680
Unfortunately, this only works with explicit render calls where the template name is passed to the render method.
00:12:22.560
This is enforced with a linter we use for other optimizations in the GitHub application.
00:12:30.540
Thus, this approach isn’t universally applicable across all Rails apps. It also doesn't account for conditionals.
00:12:38.820
Nevertheless, it's been extremely helpful, especially for designers making changes that need quick reproduction.
00:12:46.260
The biggest challenge we face as a design infrastructure team has been the missing abstraction in our view layer.
00:12:53.280
A common rule of abstraction is the Rule of Three.
00:13:02.340
One of my favorite Ruby books discusses the Rule of Three, suggesting that the first time you do something, just do it.
00:13:09.240
The second time, you notice the duplication but do it anyway. By the third time, you refactor.
00:13:16.080
When developers repeatedly create similar code without abstraction, it leads to inconsistencies and maintenance difficulties.
00:13:24.180
For us as a Design Systems team, the goal is to enable sweeping changes to designs across GitHub effectively.
00:13:31.920
This duplication makes it hard to have a broad impact.
00:13:38.760
In 2019, I had a crazy idea of using Ruby objects to render views, inspired by React.
00:13:46.440
We now call this ViewComponent, a framework for building reusable, testable, and encapsulated view components as a natural extension to Rails.
00:13:54.060
At its core, a ViewComponent is a Ruby file, often accompanied by a template.
00:14:01.320
The Ruby file contains an initializer that assigns an instance variable, such as a title.
00:14:08.640
The template may include HTML that utilizes the title as an attribute.
00:14:15.060
To render it, we instantiate the component and pass it to the render method.
00:14:22.200
This ability to pass objects to render was a vital change we made to Rails for supporting this framework.
00:14:29.340
We can also pass in content blocks; for example, we can say 'Hello World' in a block.
00:14:37.500
When we render this, we get a span with the title attribute assigned and the block content returns inside.
00:14:44.460
To test the component, we write a unit test that renders the component, asserting against the output.
00:14:51.060
Using matchers from Capybara, these tests are extremely fast.
00:14:57.840
In our code base, they run about 100 times faster than our controller tests.
00:15:04.740
We’re talking about a quarter of a second compared to six seconds for a controller test.
00:15:11.640
This capability has been a game changer for us.
00:15:18.360
So since adopting this pattern, the GitHub application has grown significantly.
00:15:24.240
Last year, while the application grew by 25%, our ViewComponents grew by over 15 times.
00:15:30.180
We’ve learned a lot through this incredible growth.
00:15:37.980
One key insight is how ViewComponents can enforce consistency in our application.
00:15:44.380
Our Design Systems team aims to help developers build consistent UIs.
00:15:50.400
For instance, last summer, one of my colleagues built a ViewComponent for counters used throughout the application.
00:15:58.260
These counters are rendered in about a hundred different places.
00:16:05.520
Let’s examine one of his pull requests. Here, he replaced one counter’s amount with a formatted version.
00:16:12.660
This change led to subtle visual inconsistencies on the page in edge cases.
00:16:20.280
With his update, we pass the raw count to our Primer counter component.
00:16:27.780
Now, the component handles formatting and displaying it consistently.
00:16:35.220
Another advantage of ViewComponents is helping developers write performant code.
00:16:42.660
A common issue encountered is unintentionally querying the database.
00:16:48.900
For example, with permission checks in our views, we might verify whether to show certain elements.
00:16:55.140
These checks can trigger queries if not batched properly ahead of time.
00:17:01.740
To help developers understand how the view code interacts with the database, we redefined the render inline helper.
00:17:08.340
This helper accepts a number of allowed queries, defaulting to zero.
00:17:14.820
When rendering a component, it will fail if it generates more queries than allowed.
00:17:22.740
This approach has fundamentally helped our team understand the side effects of our code.
00:17:29.640
It has also prevented numerous N+1 query issues. Recently, we saved 100 milliseconds per request on the checks page.
00:17:37.620
We've started applying a similar approach to prevent regressions regarding memory allocations.
00:17:44.640
For instance, we specify allowed allocations when refactoring code to avoid increasing memory usage.
00:17:52.560
The same principle applies to performance: we want to ensure our components maintain low memory usage.
00:18:00.960
Another advantage of ViewComponents is their ability to manage complexity, especially in mailers.
00:18:08.280
HTML emails can be challenging to write, often requiring inline HTML.
00:18:15.480
Most of our mailers utilized heavy-handed inline HTML that is hard to maintain.
00:18:23.880
So, when a designer requested adding columns to a mailer, excitement was not the reaction we had.
00:18:30.600
My colleague Mike built mailer-specific components for rows and columns to abstract away complexity.
00:18:38.040
These components enabled reliable column layouts and reduced the need for understanding legacy HTML.
00:18:46.320
Creating a few components has simplified the process of building mailers significantly.
00:18:53.880
If you think about it, it parallels how Active Record operates with SQL.
00:19:01.740
ViewComponents aim to make building UI the default approach, not the exception.
00:19:08.820
Another feature making building UIs easier is slots. Slots allow multiple content blocks to pass to a single component.
00:19:16.320
Here’s an example from our design system: a box component with a header, body, rows, and footer.
00:19:24.840
The HTML for this component can become complex, especially with multiple rows.
00:19:32.520
But when we implement it as a ViewComponent, it simplifies the process.
00:19:41.220
We define named slots for the header, body, and footer, as well as for multiple rows.
00:19:48.960
Utilizing slots, we can pass in specific blocks without worrying about the order.
00:19:56.880
The component implementation can then format the output correctly with the necessary structure.
00:20:03.960
Slots eliminate the burden on developers of managing template integrity.
00:20:11.520
Alongside components, we've also experimented with Storybook, an open-source tool for writing components in isolation.
00:20:19.680
It’s mostly used in React applications but now supports ViewComponents thanks to community contributions.
00:20:27.120
This tool allows us to preview components in action, helping to visualize changes pre-implementation.
00:20:34.320
Over the past summer, we open-sourced our Primer ViewComponents library, which now consists of 40 components.
00:20:41.520
We aren’t alone in open-sourcing our components; the UK government also has several ViewComponents in their design system.
00:20:48.720
Common questions from the community touch on how we're rolling out and ensuring proper usage of components.
00:20:56.160
On the technical side, we create ViewComponents primarily by refactoring existing view code into components.
00:21:03.060
Before ViewComponents, we used a presenter-like pattern called view models.
00:21:11.040
Here’s an example view model using a repository's status to display enabled or disabled.
00:21:19.080
We would pass this object into a template that accesses its values.
00:21:27.000
When rewritten as a ViewComponent, the only significant change was using ViewComponentBase.
00:21:33.240
The tests don't change much; we still perform assertions on the rendered output.
00:21:40.440
However, unlike before, we now need to assert what is presented to users.
00:21:48.000
This shift grants confidence that our view code works well from the customer's perspective.
00:21:56.260
We also write new ViewComponents from scratch when necessary; this generally happens when a complex view has varying states.
00:22:03.660
In these situations, it makes sense to use a ViewComponent for unit testing all those possibilities.
00:22:10.920
Communication has been essential when bringing ViewComponents into our organizational workflow.
00:22:18.500
We focus on documentation for our open-source Primer ViewComponents using YARD.
00:22:25.800
At the top of each component class, we write comments explaining its function and provide ERB examples.
00:22:33.960
These annotations maintain up-to-date docs alongside our code, allowing us to sync both.
00:22:41.560
We also use these YARD annotations to generate our entire documentation site.
00:22:47.520
A rake task parses the project, loads the output into an undocumented object called the registry store.
00:22:55.020
From there, we can render the documentation page.
00:23:02.460
This process helps maintain consistency and accessibility in documentation; it’s consumable through code comments and published files.
00:23:10.740
Also, any examples are executed, catching errors before they become committed.
00:23:18.000
Another way we communicate about ViewComponents is through linters.
00:23:26.880
GitHub uses linters extensively; it’s one of the main mechanisms for managing application scale.
00:23:35.460
For instance, we have a bot that reviews pull requests to encourage the use of ViewComponents.
00:23:41.760
This bot detects outdated methods and provides documentation links for alternatives.
00:23:48.840
We also employ custom rubocop rules, which help discourage using view models in favor of ViewComponents.
00:23:57.180
Linters can recognize class syntax tree nodes, issuing alerts for non-compliance.
00:24:04.680
Given our scale, automation is essential to drive consistent adoption of best practices.
00:24:12.060
Weekly updates summarize project progress and highlight improvements attributed to our approach.
00:24:18.780
These updates help company leadership communicate the value of our work and secure resources.
00:24:25.800
We formed a team of engineers to focus on building and rolling out new ViewComponents this summer.
00:24:32.640
The results were dramatic; we quadrupled the number of component usages in our code base.
00:24:40.440
The momentum continued even after that team concluded its work.
00:24:47.220
Having created a critical mass of examples helped developers copy and paste them into new interfaces.
00:24:54.960
However, we need to ensure the examples used are correct.
00:25:02.520
We achieve this with linters, which help maintain a maximum number of allowed non-component implementations.
00:25:11.040
As the new component is rolled out, we reduce the count of exceptions for legacy implementations.
00:25:18.840
Reporting our progress helps create accountability for using the recommended patterns.
00:25:25.800
For example, when we see 492 usages of a ViewComponent, it indicates our approach is successful.
00:25:32.760
Now, let's discuss lessons from open-sourcing our project.
00:25:39.480
This is my first significant open-source project.
00:25:46.200
We've received a lot of engagement—a hundred developers have contributed code, only a dozen of whom worked at GitHub.
00:25:53.520
This experience has taught me empathy; each interaction holds an opportunity for learning.
00:26:00.960
If someone misunderstands documentation, we can improve it; if an error message fails to clarify an issue, we need to adjust it.
00:26:09.120
People want to contribute; we must enable easy, high-quality contributions.
00:26:16.440
In a growing, complex library, accommodating various Rails versions becomes critical.
00:26:23.520
Using matrix builds helps run the test suite across different Ruby and Rails combinations.
00:26:30.840
This verifies changes work across all versions while maintaining test coverage.
00:26:38.580
Achieving this confidence allows us to accept community contributions safely.
00:26:45.600
Working on this project has highlighted the power of Rails conventions.
00:26:52.800
Aligning the project with Rails conventions makes it intuitive for contributors.
00:26:59.520
For instance, contributors have implemented ViewComponent previews based on Action Mailer previews.
00:27:06.720
Now, it's important to touch on challenges. One issue is the fragmentation in how views are created.
00:27:14.640
We now have two ways of writing views in Rails applications, each with a different approach.
00:27:22.020
I doubt Rails will provide truly native built-in support for this pattern as it currently exists.
00:27:29.700
Yet, there are lessons from this project that could bring improvements to Rails.
00:27:37.680
An example is the experimental gem called Nice Partials, which provides an API similar to ViewComponents.
00:27:46.200
This could be integrated into Rails, easing the transition and improving usability.
00:27:53.220
The long-term focus for this project is enhancing the Rails view layer based on real experience.
00:28:01.560
One glaring issue is the lack of support for caching within this framework.
00:28:09.840
Improving this aspect requires collaboration, possibly from you or others in the community.
00:28:18.360
More broadly, it’s vital to think about innovation in Rails and Ruby.
00:28:25.740
The Innovator's Dilemma outlines this tension between addressing current needs and adopting new technologies.
00:28:32.520
Organizations can do everything right yet still lose market leadership to unexpected competitors.
00:28:40.080
This dilemma speaks to the relevance of our ecosystem.
00:28:47.520
GitHub’s future hinges on Rails and Ruby remaining relevant, as we’ve built immensely upon them.
00:28:55.020
Rewriting our codebase isn't practical; thus, we must innovate to stay current.
00:29:01.800
We must fend for our ecosystem's viability, be proactive, and continuously contribute.
00:29:09.420
All technologies we use are open-source, built by those within our community.
00:29:16.920
We benefit from others' prior contributions but require more involvement from you.
00:29:23.760
We know the inherent shortcomings of these technologies because we’ve worked across countless projects.
00:29:30.600
Identifying pain points is crucial for our companies' survival.
00:29:38.280
Choosing a framework, a technology, or language is pivotal since rewrites are rare.
00:29:45.960
The world will continue to evolve regardless, and it's our responsibility to ensure relevance.
00:29:53.400
Thank you.
00:29:56.820
Thank you, Joel. Yes, it's definitely up to us.
00:30:03.000
That was a beautiful message at the very end. Thank you so much for sharing your story and the story of your components.
00:30:09.360
There was a question in the chat by Marcom asking about the styling of the component.
00:30:15.600
Is it in the global page style, or is there a clever way to load it if it's used in a template?
00:30:22.500
Oh man, that’s a very future-looking question! It’s a problem we're currently examining.
00:30:29.700
We've been deciding how to incorporate successful practices from the React ecosystem.
00:30:36.300
One area of focus is CSS encapsulation and delivering Styles that align well with ViewComponent's architecture.
00:30:43.500
There’s a pull request open in the ViewComponent repository discussing a potential architectural solution to this.
00:30:51.000
My team is poised to take on this task, given we have around 500 kilobytes of custom CSS.
00:30:58.560
We struggle with delivering appropriate Styles at the right places.
00:31:06.000
I might return next year with an update on how this has evolved.
00:31:14.520
There are many people expressing their appreciation for using ViewComponent in production.
00:31:22.320
It seems we are on the right track, and it's exciting to is.
00:31:30.480
I recall the early prototypes, which, despite being hastily produced, surprisingly functioned.
00:31:38.040
Aaron Patterson, Tender Love, and I created the initial prototype together at GitHub.
00:31:45.480
It was an exhilarating experience to see an idea take form and function.
00:31:52.680
And now, it has grown and evolved successfully.
00:31:59.880
Someone asked if we considered using views in an object-oriented manner before going with components.
00:32:08.520
The components originated from an early approach where we had inline templates.
00:32:16.320
It led to complications, particularly with file syntax highlighting.
00:32:24.000
We saw problems with nesting both visually and syntactically.
00:32:31.020
Also, as one of the Ruby core team members noted, inline templates would complicate future Ruby 3 typing work.
00:32:38.580
It quickly became clear that maintaining a singular syntax per file presents a more streamlined experience.
00:32:51.480
While there are extensions to write templates to components, at GitHub we prefer separated implementations.
00:32:57.780
Ultimately, it’s open source, and people can tailor it to their preferences.
00:33:05.280
We still have eight minutes left for Q&A.
00:33:12.300
I have to ask you about other ways you think the Rails view layer could be improved.
00:33:19.920
I'm particularly interested in the modern version of the asset pipeline.
00:33:27.720
Deciding whether to adapt to webpack's architecture has been challenging.
00:33:35.760
It's vital to ensure that traditional benefits remain while also remaining contemporary.
00:33:43.560
The goal is to maintain convention over configuration while accommodating modern demands.
00:33:50.220
It's a challenging endeavor where we strive for balance.
00:33:58.020
I hope that this conversation might kick off further discussions in the community.
00:34:05.160
Thank you, everyone.
00:34:07.560
Thanks once more for being with us, Joel.
00:34:12.600
We look forward to hearing more about your work next year.
00:34:18.300
Enjoy the rest of your day!
00:34:25.020
You too! Thank you!
00:34:26.880
[I am indeed extremely grateful!]
00:34:28.560
This has been a wonderful experience!
00:34:30.900
Returning next year sounds like a great plan! Thank you very much!