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
If you havenât checked it out, above is the <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.
hereâs what youâll see in the demo
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):
hash-based dynamic importsâââu dont have to do this anymore; no resolve option either
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
- pre fetching 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:
âGodspeed those who attempt the server-rendered, code-split appsâ #ssr #splitting #godspeed
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 :
faceyspacey/webpack-flush-chunks
đ© webpack-flush-chunks: use this shit!
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:
rendered synchronously on the server, synchronously on the client on first load, and asynchronously on navigation
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:
Server-Render like a Pro /w Redux-First Router in 10 steps
Redux-First Router data-fetching: solving the 80% use case for async Middleware
Pre Release: Redux-First RouterâââA Step Beyond Redux-Little-Router
Find me on twitter @faceyspacey
Want to stay current in Reactlandia? Tap/click âFOLLOWâ on the FaceySpacey publication on Medium to receive weekly âLettersâ via email