Joel is a software engineer at GitHub. He has worked on projects including 2FA reminders and the Content Attachments API. He now leads Project Paper Cuts, incorporating feedback from members of the community into GitHub.View the profile
About the talk
RailsConf 2019 - Rethinking the View Layer with Components by Joel Hawksley
While most of Rails has evolved over time, the view layer hasn’t changed much. At GitHub, we are incorporating the lessons of the last decade into a new paradigm: components. This approach has enabled us to leverage traditional object-oriented techniques to test our views in isolation, avoid side-effects, refactor with confidence, and perhaps most importantly, make our views first-class citizens in Rails.
All right. Good morning, everyone. My name is Joel. And I work and get him. The creativity is the ability to imagine something new. It is not the ability to create something out of nothing but to generate new ideas by combining changing or reapplying existing ideas. Today we're going to do just that. We're going to take ideas from react. And reapply them in my house. And in doing so we're going to take a template that is hard to test thoroughly. Is it possible to audit with code coverage tools? Makes it difficult to reason about data flow. Enfield basic Ruby code standards
3 factored into a new experimental addition to rails called action view component. That is tested thoroughly and isolation. Is audited with code coverage tools? Only received it out and needs. unpause the code standards of the Ruby language It also happens to be over 200 times faster to past. The first what even is a view? user functions the input data and returned HTML So how is the rails relayer evolved over the years? The reality is that has been pretty stable Real Steel ships with ARB like it in 2005. In 2012 rails for at a turbo links
What is it like to work on music app? So I recently work on adding sticky headers to poor request an issue Pages the ship's I believe in January. So part of that project, I got to know this little piece of our user interface that we called issue badge really well. That we use that she bout to display the status of issues and poor class. We use it about a dozen or two places throughout the application. It's part of our design system that we call primer, which you can think of is basically our own version of bootstrap. Before we dig into that too much. Let's talk about our data model.
So when we're talking about issues and pork West a pork last it's just an issue with an Associated pork West object. So all pull requests are issues, but not all issues are poor. What are the issue bad for this parcel? I'll give you a second to read it. So depending on the state of the poor Quest or issue. We're under an icon label and color. These together display the state of the issue of power Quest. Just wanting to know more about the behavior of this view. I figured why don't we just delete it and see what happens? satisfied
and I pushed it up to RCI. Annabelle cast so how could this be? The reality is that things are I mean if she's a special saw aliens talk yesterday a little different in the way we use rail to get hub. You have is a rails monolith. They just turned 11 years old. foot talk about scale we have over 200 controllers not including or API. We have over 550 models not including concerns of which there's about 1,500. 8 / 3700 views so how might the scale affect our approach to testing reviews? So right now our main way of exercising review code is through controller tests that
we set up the rent a car reviews. And then our test weed it takes 6 seconds to run one of these not including any setup. Set another way that's one minute to run a suite of 10 cases. So does anyone here know how long the Jeopardy theme song is? The 30 seconds. There we go. So imagine listening to it twice every time you want to run a suite of 10 cases that are scale. This just isn't sustainable. I believe this problem is a symptom of several shortcomings in the rails. We liar. When I ask my
local Ruby group this question, the number one response is data flow. A common data flow error. We're probably all familiar with is a good all done + 134. We accidentally generate an expensive query in a few. Example code also have some data flow issues. Super Bowl request an issue. We need from each object. These are active record objects. We'd be touching their entire set of attributes when we met and Tackle. I need one or two for each object. In addition, it's unclear where the poor request in issue variables are coming from making it difficult
to reuse this partial with any amount of confidence. Another problem is that unit testing fuse is in a common practice and rails rails encourages us to test our views through integration in system tests, which are expensive. This is especially painful for partials. Like our issue badge is they often end up being tested for each of the views. They're included in which leads to duplication of tests. And I would say really cheap is the benefit of reusing a partial in the first place. another promise measuring code coverage either simple Cub or coveralls
supportive Yukon this combined with the friction of writing tests put their views in a real blind spot compared to the rest of her coat. Another weakness is the lack of a method signature. I'd like a map to Declaration on an object needs to not expose the values are expected to receive. pokebattler example What data does this mean you need to render? Does it need a pork roast? Does it need an issue? Should I be able to passing both? What about neither? What is value passing as
locals, or do they come from helper? Rihanna interviews regularly fail even the most basic standards of code quality. We expect out of our birthing classes. So it's taking a look at that example code. So this was a method on a class what aspects might be objective and I thought of you. Besides it being a super long method and I can think of a couple. Where is octagon defined? Where does the class attribute value come from it kind of feels like a magic spring? Where
are poor quest in issue coming from? The reality is that we regularly do things in our templates that we would never do in a ruby class. Surgery cap rails views are difficult to test and those tests are impossible to audit with code coverage tools out. There. They are. And make it difficult to reason. Data flow. an implicit method signatures and often feel basically because standards the reality. Is that the existing rails view layer. Is a second-class citizen these days? So with all these shortcomings perhaps it isn't
much of a surprise, but a new way of building views to taken hold in our community. react three components A component encapsulate a piece of user interface making easy to reuse. They just one way of writing hello world in a react component. three components at a minimum employment or under method that returns HTML And then argument passed to a component are assigned to the props object, which is accessible within methods on the component. But here's an example of what the issue badge might look like as react component.
Like our template the component renders an icon and label wrapped in a state-specific CSS class. Another dimension of Architecture is types. The proptypes library allows react components to express some expectations about the data they receive So in this case, we are expecting an issue with the is closed bouillon to always be provided and a pork roast is sometimes be provided. And if so would that is closed is merged and is draft bullion's. What's great about this is that we can then reference that is closed
Boolean on issue without fear Hazard type check will guarantee that it is present. Another advantage of react as how simplifies data flow. Bypassing values interviews instead of objects reacting to urges us to write functions without side effects. Another great thing about react. That's how it usually components can be tested in isolation. Futaba logo sample component Act Here's an example test that renders the component directly and then it starts against the output. What's great is that the test runs without touching the database for the
controller layer, which means that it's really really fast. Set a recap react as components that render HTML. types that give us confidence in our inputs simplify data flow I might wait texting and isolation. Which is really too bad because not compatible with our Progressive enhancement architecture that we used to get high. But what if there is a way we can incorporate some of the benefits of react and giraffes? So before we start doing some more factoring, that's right,
because you got to make sure that we're not going to break anything. So it made it look like to test review. Tony's case we're doing three things said in class meme icon and label. Put a start. Let's have some traditional controller tops for each state will start with a test for the open issue batch will certainly of The Craft class name icon and label. And I love you like I did earlier and see what happens. Repelling text which means we can actually do something Factory.
So what might a component look like in the real world? And I think would make it it would make sense to make it a class like everything else in Ruby. foot caught badge inside the issues module And how might we call it interview? The rails way would be to use the existing render syntax. So see if we can get our first test to pass. The first one method chart component that returns to open issue badge from our partial. Call HTML is render that react uses is a loaded word in action View and will run or test.
This is interesting. It looks like action view render doesn't like being passed or component imagine that. footage of how to handle it now short of 14 rails and changing the original definition of axon view base render. naked Dora monkey patch will redefine render. Symphony pops are component because their components is Kim out method. undefined method octagon remember I could have you comment about about not knowing where that occupy Medicaid from now or code
is asking us the same question. So back in a component, let's tell it where to find it. Go Runner Taxi. Now it looks like it can't find the CSS we're looking for. I wonder what are component is rendering at this point. It looks a little something like this the skates HTML. So I might be something you something like HTML safe here. I think it might be a better idea to just try and reuse the rails rendering Pipeline and all the safety guarantees it against us. So first, let's move our template into a method called template.
And then in our HTML method will run our template through action view zrb. Template Handler this effectively mirrors how regular Ruth templates are compiled and then executed in the be later today. Search for an artist again. Time to ship it, right? black people the next test this work clothes issue. We should have a red background a closed icon and close the label. Flotrack I can't find the red CFS class as we haven't handled this case yet. Let's go back to our template. Already passing an issue here.
The first we're going to need to update our monkey patch to take the argument for passing in to render and passing them into an initializer on our component. Memo needs to find component in this case. If you can remember back to the slide about the data model we're going to let pork recipe now is not all tissues have chloroplasts. Now that we have an issue. We can render it. We can reference it in our template. Sophie Runner new text screen something interesting just happened there.
We gave ourselves an interface for a view. Which means no more implicit arguments. And just like that. We're making even more progress on our coat of you. The next let's take the rest of our partial and just drop it into our component and see how the rest of the controller Test 2. We're still green. But you know something doesn't feel quite right about the pamphlet the first 2/3 handle various pull request States or the last third candle issue state. It really seems that we have two components here. Not one.
So since the view that calls are component knows whether it is dealing with a pork roast or an issue. How about we split this out into two components and let the view pick which one to use You know the time so bad. Based on whether the issue has a poor Quest we can render either the poor request badge or the issue patch. We're going to do a couple things. We need to update our conditional. Look for both components. Gloucester countertops They're still green.
Circle back to Arkadelphia Remember that comment about magic strings? both our issue badge andar por Casa badge remembering that same state UI element from our design system. So why do we make that a component? Source it UIL really just has one option color. So if it was a component. That would be a single argument. football. What state within inside the primer module? Enemalta need to update our monkey patch to handle yet another component. Do you know this is getting a little weird? Perhaps are missing an abstraction here. Or really trying to say with this line of code is MI dealing with one of
these new components. I think it might be time for a parent class. McCall action view component So take a new parent class and update our existing components to inherit from it. Then we can simplify the conditional in a monkey patch to instead just check if the argument is a subclass of action view component. Now that we have a parent class for a components. We talked to do some reducing duplication. So an easy Canada is are HTML method, which has doesn't have any component specific logic. Let's move that to ask if you can come in.
I was checking artist we're still green. Go back to build a new component. You know, this one's a little different. We're passing a content is a block. So stop by running a test. Now we could probably just rely on our existing controller test for this. Would it be nice if we could touch this new nasty component directly by itself? The react Artest were able to render a component directly and then insert against the resulting age came out and ideally we'd be able to do the same and
Rouse render the component directly and start against the resulting age came out. All we would need is a way to render template in line. Which is pretty easy to do with application controller. We can render our template in the same code path is normal view in the park the result in nokogiri, which will make those assertions a lot easier. Search Runner cast now. Looks like something that expects the class is receiving a hash. And it looks like our test helper is actually the blame here passing a hash into render. So is it turns out action views render method except
a couple different types of arguments? Three new go back to our monkey patch and update our conditional to make sure we're dealing with with a class. And then what's 300 * that's a lot closer to what we looking for. We are expecting our content to be rendered. We just got an empty string. So think about how we might make this work. When were passing content into our component, what were effectively saying is render this block in the context of the current View and then wrap the result in the component. So how made it look?
The first we're going to need to update our monkey patch took up the block argument. And then we need to update our vendor step. The first instantiate the component and then if a block has been passed render in the context of the current View and we'll do this using actions use capture Helper and then we'll find a result to an accessor on the component. At that point our component will know about the content so we can render it. Now that we're expecting all components to have this content accessor. Let's go back to action view component and declare it there. Just a
matter of taking our component. And updating the template to render the value of that content accessor. Post tracking artist go back to green. But I don't have contact. What about selling the color? So let's start by adding a color argument initializer. What values do we need to handle? If you look at our design system documentation. It looks like we can specify three green red and purple. Otherwise the component defaults to cry, but she probably seen as our new feature draft poor
class. Flakka back to work on it. Let's capture those relationships in a constant. This gives us a clear mapping between the default value and not applying CSS class and the color values and their respective CSS class. What's great? Is that the keys of our hash also represent the entirety of the values? We should allow for the color argument. So, how come you enforce this in our component? Let's start with a test. So we'll start that when passing a color. We're not expecting chartreuse. An
error will be raised and it will be raised with a message in a format. That should be suspiciously familiar top. fliptroniks Homemaker fails so how many intercolor is one of the expected values? Where in rails so this is a problem and it's called active model validations. Sabaton or component? We can use an inclusion validation to check that color is one of the keys in our concept. Make this work will need to find an attribute reader. An action view component needs include active model validation. And then that's all that's left is to go back to a monkey pouch. And a
deceptive. They are compounded before I render it. soprano Jessica Now it's out of tests to make sure make sure we're saying the right CSS class based on the color. NFL previously, we just had the CFS class hard-coded in or template. But now that we can be sure that color is one of the keys are hash. We can safely use the hash look up the correct CSS class. And use it in our tablet. Or I can bring. Ghostek note to look at that documentation. I think we might have missed something here. We're supposed to have a title attribute.
The reality is that for most of the components in our design system CSS isn't the only interface there's a lot of things that we have to take into account like first for accessibility reasons, for example But this is something original partial never accounted for so let's make sure that doesn't happen again. It'll do that with the past. The past is in an empty title and expect a validation error. I will make sure it fails. Then it's just a matter of adding and presents validation for the title attribute and right back to green. associate Harkin tour tots are doing
missing keyword title. I think we might have just caught a regression. Never updated our consumers of this new primer State component to pass on that new required title argument. So that the title attribute? Go back to green. Go to come to dataflow you're mainly concerned with arbues unintentionally queering or database. But what if we could avoid passing an active record objects all together that would eliminate that rest, right? food start with a shoe patch Right now we're passing an issue
object, which is active record. The only thing we're doing with it is calling the clothes predicate method. Since you can probably imagine our issues are issue models interface is much more than just this one method. But yeah, we're passing an entire object just to get this one value. Sophie look at the very abbreviated implementation of issue. You can just see that this close predicate method is just checking whether the value of state is closed. What minor component look like if we pass in the state value instead of the whole issue object?
So first, we need to update the initialize method to accept Estate Value instead of issue object. And out of validation for the possible values of State. But I'm looking at her template what if we could extract eat branch of this to just be derived from the value of a state. We can still Express the relationship between the state and the combination of color icon name and label. And then we can take our template. Extract the values from the constant instead of having nearly duplicate branches of our template. So, it's Runner test. We're still green.
But what about our poor cost component? I might be a couple if I could record. Looking at the template we're relying on three predicate methods merged closed and drafts. So can we pass an estate value like we did for the issue component? Looking at another abbreviated model things aren't so simple as they were for the issue model while we do have a state value whether the poor Quest is a draft or not is independent of that value. In fact, it's just stored as a Boolean in our schema. Which means we're going to need both of these values to rent
or component correctly. So start with an x. What's lucky luckily enough for ice when you have any easy way to unit Pastore components? Send his first one will start that when you pass in those State and is draft values that we're under the correct label title attribute in icon. What's my name? It looks like our component is still expecting the old argument. Let's go updated. The first one need to update the initializer to accept the state and is draft values instead of the poor request object. Then let's take our template. Extract the title
color Arctic on name and label into methods and I'll spare you the details of those right now. It was interesting here to take a good look at this component. Is that it's really similar to a reactant walk up. It's almost uncanny. 50 heart health fair back to green So remember how we act encourage simple data flow minimizing side effects. Bypassing values into are components instead of objects for seeing similar benefits. Remember how we couldn't we were unable to get code coverage
reports for abuse? Are dental lab here now has a perfect score in simple cuff and perhaps more importantly if we dig into the report we can see that all the branching exercise and those methods I spared you. The details have is being exercised. So what are you waiting for? Let's ship it crazy, right? So as it turns out we already have these components that we bring today have been in production since March. And as of last week, we're also rendering repository topics and language badges with action to come out. What do we learn from using his architecture
in production? As we Implement these components in numerous places places throughout our views be exposed several cases. We forgot to set that title attribute. Much like the test failure we've been to earlier. Rihanna is that by using standard Ruby constructs, like required arguments. We've been able to enforce the interfaces of our components and as a result not have a single standardized reusable implementation. What is UI components that makes it easier for engineers to work with our design system. What about performance?
Start at Sweet Arkansas road test take about 6 seconds for loading a page and asserting against this content. So what about these new unit test they clocked in at around twenty-five milliseconds run in the same Suite. But / 240 times faster. Set another way AR test that used to take to Jeopardy theme song to run now barely makes it past the first note. The creativity is the ability to imagine something new is not the bility to create something out of nothing but to generate new ideas by
combining changing or reapplying existing ideas. By taking ideas from react and incorporating them into rails to take a note template that was hard to test efficiently. Was impossible to audit with code coverage tools. Made it difficult to reason about data flow. Enfield basic Ruby code standards and created a new way of thinking about the view layer and rails that enables us to write efficient isolated test. That are audited the code coverage tools. only work with the values they need and follow the code standards
of the Ruby language. All these things give us a higher confidence interview layer and perhaps most importantly make it a first-class citizen in rounds. Thank you. I have six minutes for questions if anyone doesn't anyone talk about Funny story didn't see Eileen's talk yesterday. Yeah, so we have a rule I get Hub at sorry. The question the question was do we have any plans to push this into rails? So first of all, whenever we have at this point, it is the default rule of the company that you have to plan. Streaming. That is especially true if you're running a monkey past
its running a production. got Where are the push 2 Upstream? So if you go to my website, there's a demo repo that actually has the exact source code as it's running in production right now for everything. We're up today. There's some small performance tweaks that I didn't have time to cover today. They're very minor though. Sure. So the question was we have thousands of views. Where do you see the pattern making sense vs. Cracked? So I think the most obvious application is using a design system like we have or if
you're using something silly bootstrap something that use over and over and over again that I don't know we care deeply about accessibility. So I didn't consistency in the way rui is rendered is really important in that regard. So we started with our design system. But we're working to actually for the pretty specific goal, which is we have kind of elements throughout our application that are reused and the one we're working on right now is what we call the repo list item. We do it 27 different ways right now. It's literally like if you see a list of repositories that thing so we're
building our way up into that has points. That's why you see us doing repository Topics in the language pouch. We're probably about a month away from rendering all those the same way with this everywhere and they have like complexity with the pattern. There's things on the reactant side that we have yet to try and apply in that regard. I think we'll start to see that as we build the reaper last item though. I'm excited to see what we can discover as we go. Cashing
he looked into what the bulk of our six second run time is over headed for a test. We have somewhat I think the the short answer to that is that I really try to focus this talk on things that are universally applicable. There's a lot of things if you saw aliens talk yesterday that we do internally that are unique to our code base that are more relics then things that are Benny interest that is part of the overhead. I know part of it is just some specific dependencies we have Call Father grammar check.
Buy this talk
Access to all the recordings of the event
Buy this video
With ConferenceCast.tv, you get access to our library of the world's best conference talks.