Rendering Khan Academy’s Learn Menu Wherever I Please

Jordan Scales
5 min readSep 14, 2016

--

The combination of React and CSS-in-JS just saved me about two days of work.

Over the next few months we’ll be running some A/B testing on Khan Academy’s logged out homepage. One such experiment involves placing the subjects menu beneath the main site banner:

Here’s a tale of how I implemented this change to one of our most important pages quickly and confidently using some modern frontend tooling.

What we’re working with

Currently, the learn menu sits in the site-wide navigation bar. You have to click “Subjects” at the top left to open it.

So all we need to do is take that thing, and put it in the markup of the logged-out homepage. Easy stuff, right? In 2015’s Khan Academy tech stack this entails:

  • Finding the handlebars template that renders the learn menu
  • Extracting just the stuff we need (the 5 columns of links, not the dropdown) into a separate template
  • Rendering the new template from the logged-out homepage
  • Crying because the styles aren’t applying to it
  • Finding out that the Less code for the learn menu is all nested within a single “#global-nav” rule which is not present when I render the learn menu elsewhere on the page
  • Removing the top-level “#global-nav” nesting
  • Smiling because the styles are now applied
  • …and crying again because the line heights are now messed up
  • Crying some more because the top-level #global-nav gave us just enough specificity to overrule the line-height rule for the fifth paragraph on the page
  • A combination of smiling and crying as I spend two afternoons refactoring the learn menu CSS to be more component-friendly
  • Double-checking that I typed class=”learnMenu__section--last” correctly and crossing my fingers that I didn’t break anything else

Two days later I now have the learn menu rendering in two different places, so I can sit back and relish in my success before realizing I broke the text alignment in the site-wide footer. (How was I supposed to know, who scrolls down that far anyway?)

But, this is 2016, and in 2016 we do things a little differently.

The LearnMenu Component

In 2016 we ditched the old handlebars template and created a <LearnMenu> component. It’s quite nice — it accepts a prop specifying all the sections and links within those sections. It then renders a few other shared components like <Link> and <DropdownButton> to keep the learn menu aligned with our style guidelines.

So I sat down and extracted just the stuff I needed (again, just the 5 columns and none of the dropdown business) into a separate component.

There’s nothing too exciting about this: your framework has components too. Also we can refactor with handlebars just fine — the learn menu is pretty static and doesn’t (yet) have any fancy callbacks or lifecycle methods that made this refactoring especially interesting or delightful.

The LearnMenu component does have one particularly useful thing within it, though:

return <ul
className={css(
styles.domains,
this.props.hasFiveColumns && styles.domainsFiveColumns
)}
>
{this.props.domains.map(this.renderDomain)}
</ul>;

There’s a fancy “css()” function, and two references to “styles.” If we scroll down we can see what those are about. (Bear with me, here)

domains: {
...sharedStyles.defaultAlignment,
paddingBottom: sectionPadding - gapBetweenStackedDomains,
columnCount: 4,
},
domainsFiveColumns: {
[mediaQueries.xl]: {
columnCount: 5,
},
},

That stuff is an ordinary JavaScript object, where the keys resemble CSS properties (except camelCased, so “padding-bottom” becomes “paddingBottom”). This is a library we wrote at Khan Academy called Aphrodite.

When I call the “css()” function on something like “domains,” I’ll get back a whacky class name like “domains_o3z00m-o_O-domainsFiveColumns_1xgyot8” that I can set as the “className” of the React element. This seems unwieldy (CSS in JavaScript oh no why!?), but has a few immediate benefits:

It’s unique to this component. In 2015 we might have written a “.domains {}” CSS rule. Someone else may come along and use “.domains {}” in a separate feature. Someone else may come along and change “.domains {}” for that second use-case, unaware that the original usage of “.domains {}” is now broken. We might fix this by scoping “.domains {}” to something like “#global-nav .domains {},” and then 2016 Jordan comes along and gets really sad and writes a blog post.

It travels with the component. When I render <LearnMenu> on a page, I don’t need to worry about also including “learn-menu.css” (or whatever package its contained in). I get precisely the styles I need, exactly where I need them. Aphrodite works perfectly with server-side rendering, so our CSS loads on the client just as quickly as it used to.

It still uses shared properties. The “CSS” above contains a few constants (sharedStyles.defaultAlignment, sectionPadding, and mediaQueries.xl) which are shared both internally and externally throughout our codebase. At Khan Academy we maintain a style guide full of media queries, colors, and alignment properties that we can use from our components. This is identical to the “variables.less” you have in your codebase, except instead of sharing it with some specific syntax like “@import” we use JavaScript’s “require” — it works just fine!

One Happy Developer

Using this tooling, we can now confidently render the learn menu on any page we want. If in the future we want to move the learn menu down, or put it under a video, we can do so quickly and with confidence — that’s quite the feat!

We can even render the learn menu on pages like Khan Academy’s “react-sandbox” which allows us to see what a component looks like with various combinations of props.

Still, at this point I’m not actually doing anything that I couldn’t do by hand. Techniques like OOCSS and BEM were built to help developers write perfect CSS to accomplish many of the items I listed above.

The difference is, I’m letting JavaScript figure out when and where I should load my stylesheet.

I’m letting JavaScript handle all my constants and shared mixins.

I’m letting JavaScript write the perfect CSS for me.

“Separation of concerns” you may chant. Well, I’ve separated my concerns for 10 years, and now I’m too scared to touch my CSS.

We’re still figuring out cool patterns with CSS-in-JS, and the landscape is rapidly (and to many, frustratingly) changing, but in 2016 we’re finally capable of removing some of the most annoying parts of frontend web development — and that’s something to embrace.

For more information on Aphrodite, check out the GitHub page and Kent C. Dodds’s awesome video series.

Be sure to follow me on twitter for rants like these in fewer characters.

We do lots of open source at Khan Academy, so come join us if that sounds awesome to you. 👋

--

--

Jordan Scales

JavaScript clickbait enthusiast. Giving you superpowers.