Porting React to Svelte

Porting a React library 1:1 to Svelte - It might be easier than you think

By Michael Lucht
Keywords: Svelte, React, Virtualized
February 19, 2021

Svelte lures developers with the promise of better performance and smaller bundle size than competitors like React. But, since it is new, its eco-system is small. Let's change that!

There are many differences between Svelte and React. Svelte is a compiler, while React lives in the browser. Svelte components are written into '.svelte' files, while React uses jsx/tsx. React has the virtual DOM while, Svelte doesn't. So, are these frameworks so different, that we have to write every library from scratch?

No.

I took a React library, which I like, and translated it to Svelte. It is a good exercise to see what the real differences between React and Svelte are, and to see which concepts are related. The library I chose is react-virtualized-auto-sizer. It has unimpressive 295 stars as a standalone library at GitHub, but it belongs to the react-virtualized library, which is currently at 21 thousand stars. For comparison, the highest-rated Svelte-specific library besides Svelte and Sapper is svelte-material-ui with 1.5k stars. The auto-sizer allows your component to grow or shrink to the available space on your page.

An image of a port, with a ship and a crane.

Porting Auto-Sizer

>

Here is a link to the finished library: svelte-virtualized-auto-sizer

The library react-virtualized-auto-sizer has only one exported component, so it is quite a low-hanging fruit to port. It is older, so it is implemented as a React class component. React components have more lifecycle methods than in Svelte, so it may be hard or impossible to reproduce the same behavior. In this case, it will be no problem.

AutoSizer component

There are only two files of interest in the library. The first file is src/vendor/detectElementResize.js and it contains only code, that is independent of React. So, I could simply copy it into my project. The other file is src/index.js. The author used Flow for static type checking, so it starts with a lot of type definitions. Other than that, the file just holds the component source code.

In the root of my new project svelte-virtualized-auto-sizer I create a file AutoSizer.svelte.

Props

First, in the scripts part I've put all props of the React component, except children and style. Both of these are handled a bit differently in Svelte. Some of the props have a default, so I copied that over:

/// react-virtualized-auto-sizer/src/index.js export default class AutoSizer extends React.PureComponent<Props, State> { static defaultProps = { onResize: () => {}, disableHeight: false, disableWidth: false, style: {}, }; ...

This becomes the following:

/// svelte-virtualized-auto-sizer/index.js <script> export let onResize = () => {}; export let disableHeight = false; export let disableWidth = false; export let className = ""; export let defaultHeight = undefined; export let defaultWidth = undefined; export let nonce = undefined; ... </script> ...

State

The React component defines a state variable and a few class member:

/// react-virtualized-auto-sizer/src/index.js ... state = { height: this.props.defaultHeight || 0, width: this.props.defaultWidth || 0, }; _parentNode: ?HTMLElement; _autoSizer: ?HTMLElement; _detectElementResize: DetectElementResize; ...

which both become simple state variables in Svelte:

/// svelte-virtualized-auto-sizer/index.js ... let height = defaultHeight || 0, width = defaultWidth || 0; let _parentNode, _autoSizer, _detectElementResize; ...

componentDidMount and componentWillUnmount

The code from the componentDidMount and componentWillUnmount member functions is put into Sveltes onMount and onDestroy callbacks respecitvely.

/// react-virtualized-auto-sizer/src/index.js ... componentDidMount() { const {nonce} = this.props; if ( this._autoSizer && ...

The variable unloading (const {nonce}...) is deleted and also all this-s are also removed (thank you vs-code find-and-replace function :) ):

/// svelte-virtualized-auto-sizer/index.js ... onMount(() => { if ( _autoSizer && ...

_onResize

The "_onResize = () => {..." member becomes a const function "const _onResize = () => {...". Again, props unloading is deleted. The React component uses width and height again, so I had to rename the new consts to width_ and height_. And the setState part is changed:

/// react-virtualized-auto-sizer/src/index.js ... if ( (!disableHeight && this.state.height !== newHeight) || (!disableWidth && this.state.width !== newWidth) ) { this.setState({ height: height - paddingTop - paddingBottom, width: width - paddingLeft - paddingRight, }); ...

to simple assignment:

/// svelte-virtualized-auto-sizer/index.js ... if ( (!disableHeight && height !== newHeight) || (!disableWidth && width !== newWidth) ) { height = height_ - paddingTop - paddingBottom; width = width_ - paddingLeft - paddingRight; ...

render

So far, there have been no significant changes. The syntax is a little bit different, but not much. For the render function we got a little more to do. Let's break this down into four steps.

Step 1 - Styling: The React code defines a outerStyle object

/// svelte-virtualized-auto-sizer/index.js ... render() { ... const outerStyle: Object = {overflow: 'visible'}; ...

and in the code, the height and the width of this style object can be set to 0. In Svelte, inline styling is not supplied as an object, but as a string. An equivalent implementation is straightforward, but I take a different route here since Svelte makes it easier to avoid inline styling. I create two boolean variables let outerstylewidth = false, outerstyleheight = false, which are set to true when the width or height style would have been set to 0 by the React component. Then I add the following style element to the .svelte file and make the two classes conditional with the class: directive:

... <style> div { overflow: visible; } .outerstylewidth { width:0; } .outerstyleheight{ height:0; } </style> <div class={className} class:outerstylewidth class:outerstyleheight bind:this={_autoSizer}> ...

Step 2 - ref -> bind: I use the The bind:this directive as an exchange for the ref-logic. Note that this makes the _setRef member function unnecessary.

Step 3 - make render code reactive: The logic part of the render function is marked as reactive, since it is called each time something changes:

/// svelte-virtualized-auto-sizer/index.js const childParams: Object = {}; // Avoid rendering children before the initial measurements have been collected. // At best this would just be wasting cycles. let bailoutOnChildren = false; if (!disableHeight) { if (height === 0) { bailoutOnChildren = true; } outerStyle.height = 0; childParams.height = height; } if (!disableWidth) { if (width === 0) { bailoutOnChildren = true; } outerStyle.width = 0; childParams.width = width; } ...

To:

/// svelte-virtualized-auto-sizer/index.js ... const childParams = {}; // Avoid rendering children before the initial measurements have been collected. // At best this would just be wasting cycles. let bailoutOnChildren = false, outerstylewidth = false, outerstyleheight = false; $: { bailoutOnChildren = false; if (!disableHeight) { if (height === 0) { bailoutOnChildren = true; } outerstyleheight = true; childParams.height = height; } if (!disableWidth) { if (width === 0) { bailoutOnChildren = true; } outerstylewidth = true; childParams.width = width; } } </script> ...

Step 4 return -> html: Finally, the return statement

/// svelte-virtualized-auto-sizer/index.js ... return ( <div className={className} ref={this._setRef} style={{ ...outerStyle, ...style, }}> {!bailoutOnChildren && children(childParams)} </div> );

is straightforward translated to this code. The {#if} statement replaces the {...&&...} part and children translate to slot:

/// svelte-virtualized-auto-sizer/index.js ... ... <div class={className} class:outerstylewidth class:outerstyleheight bind:this={_autoSizer}> {#if !bailoutOnChildren} <slot width={childParams.width} height={childParams.height} /> {/if} </div>

Types

Typescript declarations make the life of the consumer of the library easier. The types of the React library are stored within the definitelyTyped project @types/react-virtualized-auto-sizer.

The React component is changed into a Svelte component:

/// @types/react-virtualized-auto-sizer/index.d.ts import * as React from "react"; ... export default class extends React.Component<AutoSizerProps> {}

To:

/// svelte-virtualized-auto-sizer/index.d.ts import {SvelteComponentTyped} from 'svelte'; ... export default class extends SvelteComponentTyped<AutoSizerProps,{},{default:{width?:number,height?:number}}> {}

How to use it

That's it, the library is ported. You can load the library with

npm install --save-dev svelte-virtualized-auto-sizer

This is how you would use the original in React:

<AutoSizer> {(height,width)=>( <div style={{!width!}}> Test </div> )} </AutoSizer>

and here is the equivalent in Svelte, using the let: directive:

<AutoSizer let:width={childWidth} let:height={childHeight}> <div style={"width:"+childWidth+"px;height:"+childHeight+"px;"}> Test </div> </AutoSizer>

Poll: Which React library would you love to see in Svelte?

You can select more than one, or add a new entry

Conclusion

I picked a small library to see, how easy it would be to translate it to Svelte. To me, porting react-virtualized-auto-sizer library to Svelte felt like it was straightforward. This is my first post about porting of React libraries, so I was showing all details here. Further posts are planned, I am currently half-way through porting react-window, and I promise to focus on the interesting changes only.

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.