Why are React component libraries so complicated compared to Svelte?

By Michael Lucht
Keywords: React, Svelte, create-react-library, TypeScript
January 25, 2021

Have you ever tried to share a freshly designed UI-component as a library? Recently, I was creating some ripple-effect animations in Svelte and decided to publish them in a library. It is neatly compiled with all the runtime, so it can be used without Svelte. To target the large audience of React developers, I also wrote a convenient React wrapper component for it. At first, I thought this would be an easy task, but the build process turned out to be harder than I thought. So, I want to share my experience. I will show a simpler example here, so let' go:

The project

Let's assume, we want to share a simple component, which wraps Html content into a

Rocket

The rocket is just a div, which has the borders styled in an unusual way with simple css. It is just a toy example, but I want to do this with React and Svelte, so let's start with the basic steps in Svelte:

Svelte

To start, create a folder svelte-div-rocket and in it run

npm init

and answer the questions to generate a package.json file. As "main" choose "Rocket.svelte" and then go ahead and create this file. For this example, I do not bother creating subfolders, for larger projects this would not be wise. Here is the content I put in the "Rocket.svelte":

<!-- @component ### Div Rocket Wrap anything into a marvellous rocket! - @param direction string: The rocket can fly "left", "right", "up" or "down". --> <script> export let direction = "left"; </script> <style> .uplookingrocket { border-width: 50px 6px 30px 6px; border-style: solid solid groove solid; border-color: grey grey red grey; border-radius: 100% 100% 0 0; } .leftlookingrocket { border-width: 6px 30px 6px 50px; border-style: solid groove solid solid; border-color: grey red grey grey; border-radius: 100% 0 0 100%; } .downlookingrocket { border-width: 30px 6px 50px 6px; border-style: groove solid solid solid; border-color: red grey grey grey; border-radius: 0 0 100% 100%; } .rightlookingrocket { border-width: 6px 50px 6px 30px; border-style: solid solid solid groove; border-color: grey grey grey red; border-radius: 0 100% 100% 0; } </style> <div class={ direction==="right" ? "rightlookingrocket" : direction==="up"?"uplookingrocket" : direction==="down"?"downlookingrocket" : "leftlookingrocket"}> <slot/> </div>

Let me explain: first, we have a comment bracket <!--... --> where the component is documented. Then there is the script, where we export a single prop direction. In the style element, we declare 4 CSS classes, one for each facing direction. And finally, the Html section has a div, where the class is picked based on the direction prop, and we got a slot so that we can add children into the rocket.

What else is to say about the component library? Not much, actually. We could add more information to the package.json, a README.md, and a license file, if we wanted to, or we could already publish this to npm. In a Svelte project, we can (after installation, obviously) use the component with

// Somecomponent.svelte <script> import Rocket from 'svelte-div-rocket'; </script> <Rocket> some text </Rocket>

That's it for Svelte.

React

Let's try the same process. Create a folder react-div-rocket, run npm init, answer questions with "main":"Rocket.jsx", create this file and put this in:

// Rocket.jsx import React from 'react'; import './rocket.css' const Rocket = (props) => { const { direction, children } = props; return ( <div className={ (direction === "up") ? "uplookingrocket" : (direction === "down") ? "downlookingrocket" : direction === "right" ? "rightlookingrocket" : "leftlookingrocket"}> {children} </div> ) } export default Rocket;

Also, create a file rocket.css where the 4 classes from the Svelte style tag are placed. When I try to use it from a project started with create-react-app the following error appears:

>

../react-div-rocket/rocket.jsx 8:8
Module parse failed: Unexpected token (8:8)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org

It tells me, you can't have .jsx files in a library unless you have an extra webpack loader. The loader would have to be part of the app project, not the library, so it is no option to demand the users of our rocket library to install it. Instead, we have to transpile the jsx into plain js.

In this case, we could do this by hand or with the Babel online transpiler, since we just need to arrive here:

//Rocket.js import React from 'react'; import './rocket.css'; const Rocket = props => { const { direction, children } = props; return /*#__PURE__*/React.createElement("div", { className: direction === "up" ? "uplookingrocket" : direction === "down" ? "downlookingrocket" : direction === "right" ? "rightlookingrocket" : "leftlookingrocket" }, children); }; export default Rocket;

In general, this is a tedious process, so let's start over and try a streamlined version. First, delete the package.json. Then run

npx create-react-library

and answer all the questions. Depending on your computer and internet connection speed, you'll have time to grab some coffee. Small recap, the ready-to-use Svelte library has 2 files and uses ~2 kB disk space, and has zero dependencies. To be fair, it does not have a test environment, an example app, or TypeScript linting and transpiling. The React folder has all that now. It takes about 397 MB (420 MB with TypeScript template) of disk space and the node_modules folder has 1087 (1096 with TypeScript) subfolders. It has no "normal" dependencies, but 19 dev-dependencies (29 with TypeScript) and 1 peer-dependency (react).

It should be easy enough to adjust the template with our rocket component. Or is it not? Copy over the Rocket.jsx code to src/index.js, copy rocket.css into the src folder and build the library with the npm run build command. Then import the component into some app. And et voilà, nothing happens. Well, at least if the React version of the library and your project matches. If not, you get an error.

>

Quick fix: update both React versions or simply delete the React folder in the node_modules folder in our rocket library.

Assuming that there are no more errors, why are we not seeing any rockets? First, note that our CSS classes end up in dist/index.css but they are renamed during the build process. If we want to style something with our class 'uplookingrocket', we would need to write something like this:

import styles from './rocket.css'; <p className={styles["uplookingrocket"]} />

Adjust our code accordingly aaaand..... still nothing. You can find the reason by looking into the Example/App.js. There is the following line:

import 'react-div-rocket/dist/index.css'

That means, in the app, we have to import the component and the CSS file. Maybe that makes sense from a technical perspective since React/Webpack wants to bundle CSS into a single file, but from a user perspective, this is rather inconvenient. For our little project, it even questions, whether it makes sense at all. The "magic" happens in CSS, so we could only publish the CSS file in a library and let the user apply it to her Html elements.

Comparison and reasons

Both libraries work, at last. When you only look at the final results which are delivered to the consumer, dist/index.js and dist/index.css for React and Rocket.svelte for Svelte, there is no big difference in size and content. Why is the process to get there so different?

React is much older than Svelte. It has a long history and along the way, it redefined itself and with it the way we look at web apps. The most popular way today is probably to create a bundled app with create-react-app. It was not always clear that this would be the preferred operation mode by developers. It is also possible to run React completely from CDNs. This has the advantage that the React library is downloaded only once for all pages. This option explains why our library cannot contain jsx files: we can't know if the library is processed further or if it is directly called in the browser, and the browser does not understand jsx.

The Svelte developers on the other side could observe how the landscape of web development changed over time. Developers today transpile and bundle their apps anyway, so why not try to put the heavy lifting into the compile step? So, Svelte is a compiler. It does not have a heavy legacy backpack to carry around (yet?). It gathers all information when the final code is generated. Thus, we don't need to pre-process our library. Svelte condenses a lot that is good in React, but of course, it is much more. It has its own syntax and file type, it gets rid of the virtual DOM and instead redefines reactivity, and adds a lot of little helper gimmicks that make the life of a developer much easier.

Perhaps React shows the way, that Svelte might also go one day: Commit after commit new Ideas are added and more and more use-cases are covered. At the same time, the codebase grows, and complexity increases. Cutting back in features or supported use-cases is hard. In this case, the CDN workflow makes it complicated, But also, I showed above that only little code changes are necessary, to make the React code work, basically just changing the angle brackets syntax <.. /> into React.createElement(...). Yet, create-react-library employs the powerful Babel toolchain. Babel's most magical trick is to transform modern code into something the "beloved" Internet Explorer understands. Identifying the code part whose sole purpose is this would be a tricky endeavor. Perhaps no unpaid volunteer or Facebook employee will ever tackle this problem. New features are sexier than cleanup.

Conclusion

Finally, the simple rocket component is shareable with both React and Svelte. The final bundle size for both packages is small and there are only peer dependencies to React and Svelte. The Svelte component is more convenient to use since we need two imports in React.

I wanted to share my experience, because I was really surprised, how many hoops I had to jump through to get a React component library up and running. I am not saying, that there are no pain points with Svelte (especially when you need to work on the rollup process) but the contrast in this situation is quite amazing. Many developers noticed before, that the dependency tree with React projects grows to an uncontrollable level. I'm certainly not the first or the last one. Having nearly half a gigabyte of disk space blocked for the development of a simple library feels absurd. Memes about the node_modules folder are spreading. There is always a trade-off between flexibility and streamlining. React may have reached the point, where it is so widely used, that you cannot cut down the supported use-cases to the most popular ones, eg. employing a 'create-react-app`-like build process, without upsetting your userbase. It might be easier to start from scratch with something new.

This is where a framework like Svelte steps in. Developers look at these not only because they are shiny and new, but they provide value. In this case, it reduces the complexity. I did not think that people should be able to use my little React library via CDN, or that it would be better to export the css file separately. From my usual use-case of React, the additional steps I had to take to make this library work don't make much sense. Since Svelte has only one way you can use it, I need to understand less.

Update January 26. 2021: I added some morecontent to the "Comparison and reasons" section and the "Conclusion"

Bonus: Add TypeScript to the Svelte Rocket

Maybe you already read my blog post about Svelte and TypeScript. It is easier than ever to create TypeScript declaration files for our Svelte library with svelte-types-writer. Just run:

npx svelte-types-writer ./*.svelte

This generates a nice declaration file Rocket.d.ts:

import { SvelteComponentTyped } from 'svelte'; /** * ### Div Rocket * Wrap anything into a marvellous rocket! * - @param direction string: The rocket can fly "left", "right", "up" or "down". */ export default class Rocket extends SvelteComponentTyped<RocketProps, RocketEvents, RocketSlots> { } declare const _RocketProps: { direction?: string; }; declare const _RocketEvents: { [evt: string]: CustomEvent<any>; }; declare const _RocketSlots: { default: {}; }; export declare type RocketProps = typeof _RocketProps; export declare type RocketEvents = typeof _RocketEvents; export declare type RocketSlots = typeof _RocketSlots; export {};

and a simple forwarding file Rocket.js. Update "main":"Rocket.js", in the package.json and maybe make _RocketProps in the d.ts. file more user-friendly:

... declare const _RocketProps: { /** The facing direction of the rocket. Default: "left".*/ direction?: "left"|"right"|"up"|"down"; }; ...

Enjoy!

Rocket

Share this article on twitter - Vote on next topics - Support me

This might also be interesting for you:

Support me:

or directly

Design & Logo © 2020 Michael Lucht. All rights reserved.