React Universal Component 2.0 & babel-plugin-universal-import
Whatup Reactlandia!
Today I want to introduce something powerful. You can now do the above.
Click like, click share, tweet. Thanks for reading.
WHAT PEOPLE HAVE TO SAY ABOUT UNIVERSAL:
- “@faceyspacey. Should say, U are making SSR codesplitting easier /w each release. Solves most of our use-cases magically 🍻🔥” @aga5tya
- “first I want to say fantastic work on all of your libs for react + ssr + code splitting — doing myself a deep-deep dive the last several days to get something correctly started, and I came across all of your stuff today and it’s been a godsend.” @eudaimos
- “Whoah thats cool as fuhhhhhhhhh” @Ken Wheeler
- “Been great watching you fly dude. Firing out awesome efforts on many fronts!” @controlplusb (author of react-async-component)
- “This has gotten waaaay too little attention: https://medium.com/faceyspacey/code-cracked-for-splitting-ssr … Nice work @faceyspacey. I just embedded that stuff in our stack. 👌🤘” @thefubhy
TRUE UNIVERSAL RENDERING & CODE SPLITTING
<UniversalComponent />
as used in the Redux-First Router CodeSandBox.To the SPA crowd being able to use a variable within a dynamic import isn’t anything new (aka “dynamic expression”). And even though we haven’t had a general component that allows us to pass the name of the module to import()
as a prop, that isn’t what’s significant either. This is about being able to dynamically import/require as part of a true universal component.
True universal rendering has been impossible for the simple fact that webpack hasn’t supported it.
Here’s what a true universal component must do:
- call
import()
on the client on user navigation to new pages/components - call
require.resolveWeak
(a webpack-specific method) on the server to render synchronously - call
require.resolveWeak
on the client on initial load to also render synchronously (i.e. without triggering additional requests to the server, and without checksum mismatches) - insure additional dependencies aren’t put in the parent bundle through synchronous requires (as that defeats the point of code-splitting)
Existing loadable components required you to create many such universal components as they didn’t support more than one static import at a time. They could only do this: import('./Foo')
.
What this is about is being able to use dynamic expressions synchronously when the modules have been delivered to the given environment, without creating new dependencies.
To render universal components synchronously requires calling require.resolveWeak('./Foo')
or require.resolveWeak(`./${page}`)
.
The latter of which hasn’t been supported by webpack until now.
WEBPACK NOW SUPPORTS WEAK RESOLVES /w DYNAMIC EXPRESSIONS
Last week I finalized a PR to add this feature to webpack. The feature revolves around “context.” Context is a capability unique to webpack that enables it to extend how imports and requires work in the context of bundling and code-splitting (different use of the word “context”). When you create a dynamic import such as:
import(`./asyncComponents/${page}`)
it creates a representation of all the possible modules you can import in the asyncComponents
folder. That’s what “context” is. What it is most notably used for is triggering the creation of every possible bundle/chunk that could come from that folder, where each file becomes the entry point to an additional chunk. It also supports nested directories — that’s a lot of chunks!
It’s all possible. But unlike import(), require.resolveWeak
was missing this feature. It had been a lesser-used feature until the pursuit of universal code-splitting necessitated it.
By the way, it’s called “weak” because the way webpack works is it marks the imported/required/resolved module as a “weak dependency.” What that means is the parent module that imported/required/resolved it does not expect the module included in the same chunk. It just expects that it somehow is manually made available. In fact, the primary purpose of
resolveWeak
is just to see if the module is there so that you don’t require an unavailable module and throw an error.resolveWeak
unlikeimport
andrequire
just gives you themoduleId
, which you can then use to key into the hash of modules available and see if it’s available before actually requiring it.
When you do finally require it you have to use webpack’s internal function for requiring, which also will insure a new dependency isn’t created:
__webpack_require__(moduleId)
Now that I think of it, webpack really should just have a method called requireWeak.
Something to the tune of:
You can’t in fact do that because the webpack compiler transforms resolveWeak
in a special way where it creates that context we talked about for the static initial parts of the module path.
But theoretically it could be added to webpack, and today many of you reading this wouldn’t be wondering what "resolve”
means.
The bottom line is we haven’t had a feature-complete way to keep modules out of a parent chunk — should we desire — when requiring modules.
So without further a due, here’s the conclusion to the resolveWeak
PR story: Within a day Tobias Koppers (@ sokra) merged the PR. I was expecting at least a week or two — I guess he wanted it as badly as I did. A few other things were added in fact. For example you can create a weak context via require.context
as well, and use a “magic comment” to weakly import, i.e. like this:
import(/* webpackMode: ‘weak’ */ ‘./Foo’)
You can read the PR comments or hear from Tobias Koppers in this week’s “webpack freelancing logbook” to learn exactly what was added and what it does.
However, it hasn’t been published to NPM yet. That’s the catch.
HERE’S HOW TO USE IT TODAY
UPDATE (August 8th 2017): THE PR HAS BEEN PUBLISHED. YOU NO LONGER HAVE TO DO THIS. JUST USE NORMAL WEBPACK.
Don’t let the following installation detour you. Doing such things is how you become a bad-ass developer who won’t stop at anything:
yarn add --dev webpack-universal module-alias
# add this to your package:"_moduleAliases": {
"webpack": "./node_modules/webpack-universal"
},
# paste this at the top of your server-side entry script: require('module-alias/register')
Yea, use a fork until the next webpack release, which seems to be like every week and a half. OR, try out the super awesome brand new demo in the react-universal-component repo. More on that below.
Everything is documented in the corresponding Universal repos for this approaching future. If you’re already using react-universal-component
, it won’t break existing usage as existing usage didn’t even rely on dynamic imports.
🍾 Here’s where you should go next:
THE WAY IT WAS BEFORE
When things become frictionless, platforms evolve. Just yesterday I helped someone with the also awesome code-splitting package, react-async-component (by @cntrlplusb).
The issue they had was that without dynamic imports you have to create a hash of all the split point components, guarded by functions. Then when needed, pick one from the hash to call.
The following is an example to the problem in the context of react-universal-component
, which supports synchronous rendering via the resolve
option (unfortunately react-async-component
does not):
To seasoned code-splitting experts this is par for the course. However, the hash-based solution is a key gotcha that has stagnated the potential for code-splitting for a long time now.
And again to make matters worse, as soon as you could do it, it became clear you couldn’t also do so as part of the universal synchronous rendering mechanism: require.resolveWeak
(i.e. what the PR solves). This is the precise issue that person was having with react-async-component
. Stackoverflow and github is littered with people having this quandary.
But there’s still more to the equation: notice the resolve
options above. It’s small, but still not hitting the mark. Developers are smart; they wonder what things do; that’s stress and cognitive overhead that can and should be averted. Having to do anything more than import()
leaves things in inaccessible territory.
NOTE: if you have a Babel server by the way, you have to provide yet another option:
path: path.join(__dirname, './Foo')
. Then when it comes to flushing the modules on the server (as my webpack-flush-chunks packages does), even more work was required.
To make things completely accessible and 100% frictionless, requires creating a babel plugin to eliminate these additional options. And unfortunately older solutions which didn’t have such options didn’t universally render.
Introducing: babel-plugin-universal-import
I initially began not really caring about whether a babel plugin saved a line or 2, but after I did babel-plugin-dual-import (which means we already had a babel plugin in play), and after building support for a universal component that can utilize dynamic expressions, and after doing the dynamic resolveWeak
PR to webpack to facilitate it, it finally occurred to me that universal(props => import(`./${props.page}))
in all its frictionless glory was what would finally make universal code-splitting accessible. And it had to be done.
So without further a due, I introduce babel-plugin-universal-component:
What it does is the combination of babel-plugin-dual-import and the elimination of the extra options (obviously it generates them for you based on the argument to import()
). However, most importantly it automatically generates the names of chunks for you.
“Magic Comments” are so magical they have disappeared.
If you’re not familiar with babel-plugin-dual-import, it’s a plugin I released several weeks ago in the Webpack’s import() will soon fetch JS + CSS — Here’s how you do it today article.
The short of it is webpack’s master plan for CSS includes import()
retrieving two files: the js chunk and a stylesheet “chunk.” This is better for cacheability, and more importantly keeps the amount of CSS you serve to a minimum in the same way JS chunks do — you only serve what you need.
I won’t go into it in depth, but since CSS plays a central role in completing the vision here, I’ll digress for a sec: Basically this approach gives the latest CSS-in-JS solutions a run for their money, with the one exception being Emotion, which is on track to generate static stylesheets via their “extract mode.” Which means they would play very very VERY nice together! Though I wouldn’t recommend using it now, since it doesn’t yet support IE11+ (and to solve it requires a solution that comes at the problem from a completely different angle).
As these strategies become common place, the Emotion team is in a good position to join the fray in true universal rendering. I look forward to that happening. I think they (and certainly other CSS-in-JS packages) simply came at the problem at a time where generating multiple stylesheets per call to import()
was an impossibility. My suggestion to them is to look into how extract-css-chunks-webpack-plugin
(another package in the Universal family) generates multiple stylesheet chunks now:
CSS-in-JS solutions currently suffer from sending the javascript description of your styles in addition to “critical render path css”, even though they pitch themselves as serving the most minimal amount of CSS. They may send the most minimal CSS, but they send the most amount of style-related bytes in total. And your stylesheets aren’t cached. And your render functions are doing more work they don’t have to on both the server and client (however minimal). Emotion has potential to be a game changer here. I’m also in love with Glamorous. I would love to see them take the universal code-splitting approach to CSS. It can be done in combination with what they are doing. We can have it all. To do this at the highest level requires making dynamic css-in-js static in a preval phase, for all css except the css dependent on props. Kent Dodd’s babel-plugin-preval is the key.
THE SIGNIFICANCE OF FRICTIONLESSNESS
These incremental evolutions may seem small, but they are not. To understand why they are not, you must be familiar with the startup application development wisdom of “the significance of frictionlessness.”
This can take many forms, but let’s take the simplest concrete examples from the “product” level of the game:
- uber makes ordering a taxi pressing a button
- instagram makes sharing photos pressing a button
- twitter makes world-wide connectedness typing 140 characters (and pressing a button)
- XYZ on-demand startup, fulfills that demand by pressing a button
The metaphor can also be applied to advanced systems and algorithms. In my favorite (lesser known) Bret Victor piece, Ladder of Abstraction, Bret describes how once certain algorithms become fast enough (frictionless), it opens the doors for the next evolution of the system.
Just imagine your debugger is debugging your code line by line — well now, imagine it debugs your code as if it was evaluated with a 10,000 different input variables simultaneously (similar to Wallaby.js’ continuous test runner in fact), and provides visual tools to jump between all the different ways your code can be executed in a context-relevant way, instead of debugging line by line.
Well, we can’t really do that because your computer isn’t fast enough. When our systems — aka computers — evolve to be fast enough, many opportunities to explore our code (aka “observability”) will open up.
Observability into what our code is doing is one of Bret’s core tenets described in that one main article of his. Google him if this is your first time hearing of him. You won’t regret it.
When things become cheap, we can do that time-consuming thing a near-infinite number of times and focus on the next level of the game, whatever that is.
For Reactlandia, there’s a lot more we have on our to-do list for universal rendering. And rest assured you’ll be hearing about it from me. With all this finally frictionless, it opens the doors for:
- component-level data-fetching and re-hydration on the server
- fetching + rehydration along side code-splitting
- prefetching both chunk imports and said data
Put another way: once you can do something previously hard with supreme ease, it results in people finding new ways to apply and evolve the medium. (I look forward to what’s next in Reactlandia)
As universal code splitting becomes more commonplace, there will only be more developers innovating and finding solutions to problems we didn’t even know we had. In other words: evolving the React platform.
Lastly, just to drive home how challenging this has been for those that haven’t attempted, read this quote from the React Router docs on code splitting:
While the aspects that must be handled by the server is certainly the more challenging part, every bit of efficiency we can squeeze anywhere is key to this strategy reaching the inflection point where mass adoption becomes a reality.
NOTE: today we’re only talking about the component aspect. However, the crux of the Universal offering is really the webpack-flush-chunks package, which provides things you can’t get anywhere else:
webpack-flush-chunks is essentially the glue of the Universal family that brings everything together to insure the client has the precise chunks it needs to synchronously render on initial load (no less, no more). It’s a one-of-a kind package that all who have struggled with simultaneous SSR + splitting must check out.
Look forward to its next big release that consolidates all these packages: CLUE: it will be renamed to ‘universal-render’
.
WHY SO MANY PACKAGES
Transparency. Deploy early and often. Learn. Those are the reasons. I’ve grown several products on and offline, and it’s become a personal mission of mine to not give into fears of perception and get things into peoples’ hands sooner. And to constantly be improving this process.
I’ve found it to pay off in the long run as it leads you to building the right things (common startup wisdom). There’s already many merged PRs to Universal from the community, and directions changed based on what’s been learned.
That said, I realize it can be confusing having so many related tools floating around. Expect everything to consolidate and become easier.
I must say — given the additional confusion —it’s far exceeded my expectations how many people these tools have resonated with and how quickly, especially as a newcomer to open source. It’s very inspiring hearing so many developers ecstatic that these problems have finally been solved.
Once things consolidate, expect things to take off. But don’t let that hold you back from trying these things out. I chose to take this time to focus on open source because A) I didn’t want to go another project without these tools (i.e. actual needs, which in my opinion is a base requirement), and B) because of how exciting community-oriented iteration is. It’s been great interacting with you all — make sure to come say hi on Reactlandia Chat.
Development is time-consuming and to take things to completion can be a real test of your character. When you work in silos, your only fuel is money (if you’re lucky) for a very long time. So I’m honestly loving the feedback cycle. Provided you have your bare financial needs met, the fuel of inspiration runs far deeper. Super helpful community-focused developers like Kent C. Dodds, Dan Abramov, Gajus Kuizinas (to name a few) have taught us this.
So as for consolidating the space and simplifying things, it relies on you. To try the tools, star the repositories so other developers know they have value, and perhaps even contribute. Don’t wait for things to become even easier, even though it’s my mission to do so. And it’s more than just helping me grow the popularity of these packages. It’s about you, which brings us to today’s conclusion:
CONCLUSION: THE FRAMEWORKLESS APPROACH
The frameworkless approach relies on you being savvy with package adoption. The biggest draw of javascript is the ecosystem (despite how many people complain about the language).
The language could suck (which is no longer true) and it wouldn’t matter.
Therefore the most important skill right now is your ability to review packages and try them out swiftly, not just coding. You benefit greatly from the skills of being able to know which package is a true gamer changer, how mature it is, and if and when it makes sense for you to adopt it. And of course you can learn tons from analyzing the source code.
The reason we want the frameworkless approach is because it gives us the most flexibility as our apps grow. The difference between Next.js and what we can do now without a framework is negligible, and by the time we’re done, it will be non-existent. Well, in fact, it will be the other way around 😎
So that is all to say that once these tools achieve the aimed for level of simplicity, they will inevitably explode in popularity. Developer skill level is like a pyramid with the least amount of developers but with the most skills on the top. That’s why frameworks quickly become so popular. There’s just a larger developer audience.
The downside though as we’ve seen with Meteor and as we’re now seeing with Next.js is soon their key features are commoditized outside of their walled-gardens, and soon you’re no longer doing things the best way, as you can do them just as easily but with far more flexibility.
Of course I’m speaking a bit preemptively, but developers paying attention have a sense of what’s about to go down. The coming weeks will bring answers for “power users” that want the best of Next.js minus the walled garden, and answers for “up and comers” who are informed enough to desire a frameworkless approach, but perhaps for whom — without these tools — it would be a mistake to take on for a project with a deadline.
As of today, SSR + Splitting is finally in reach for general consumption. I hope you give Universal a try, share your experience and tell the world.
1 love [javascript]. Long live frameworkless development in Reactlandia.
@ faceyspacey
EPILOG
New Features in React Universal Component 2.0:
onBefore
,onAfter
,onError
callbacks as props. These are especially important now that your<UniversalComponent />
can switch between multiple components. They allow you to display loading indicators elsewhere in your UI, not just the placeholder for the imported component.- No more need to provide
resolve
,path
orchunk
options asbabel-plugin-universal-import
generates them. - Stylesheet chunks and automatic importing of them along with your js (“dual imports”), again via
babel-plugin-universal-import
. - New options:
alwaysDelay
,loadingTransition
. See the docs for info. - Static hoisting:
MyComponent.doSomething()
becomesUniversalComponent.doSomething()
. - docs overhaul for all packages in the
Universal
family - Plenty of bug fixes and enhancements to:
webpack-flush-chunks
,extract-css-chunks-webpack-plugin
,babel-plugin-universal-import
andreact-universal-component
itself. Many thanks to the community for your PRs!
Oh, and if it’s not clear, you now only need one universal component to show all these imported components:
You’ll find the above demo in the react-universal-component repo. Make sure you try it.
Where to go next:
⭐ If you’re excited about the latest evolutions in Universal Rendering, tweet, heart and star so other developers know 🚀
> For more idiomatic javascript in Reactlandia, read:
Find me on twitter @faceyspacey
Want to stay current in Reactlandia? Tap/click “FOLLOW” next to the FaceySpacey publication to receive weekly Medium “Letters” via email 👇🏽