Porting React to Svelte

From react-window 1:1 to svelte-window

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

Large lists can drain the performance of your website. Virtualization to the rescue! react-window is a great solution for your React app, and now, a port of it svelte-window is available on npm for your Svelte apps. Here I show, how it was ported, and how you can use it.

an open window

Introduction

react-window is a smaller rewrite of react-virtualized with 9.8k stars at GitHub. Both libraries increase the performance of your website with virtualization, which means that they only render those parts of a large table or list, which are currently visible.

I will focus on the trickier changes, which were needed to port the library, since in my last post, I covered in detail the basic steps of porting a library from React to Svelte. The ported library in that post is react-virtualized-auto-sizer. The library has only one component with a neat little helper to automatically scale up an html element to fill the space on a page. Compared to that library, react-window is more complex.

I think it is a great exercise to see, where React and Svelte are similar and also where they are different. I show here what you can do when:

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

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

Anatomy of react-window

react-window is written with static type checking via flow. Static type checking is great for debugging, but I don't believe that it is supported by Svelte (didn't check, actually), and I am more into typescript anyway, so I just remove all the type annotations. I pull in the types from definitelyTyped and adjust them. This way users with the typescript vscode (or neoVim etc.) plugin get all the benefits like IntelliSense, while the Svelte preprocessor is not mandatory.

There are four different exported components in react-window, a grid and a list, both as variable and fixed size variants. Internally, only one grid and one list class component is defined. There are no big differences in the implementation of the grid and the list, so I will only refer to the grid. The class definition is in src/createGridComponent.js. The trick to differentiate between the fixed and variable-sized variant is that the class definition is wrapped into a function so that the inputs of the functions can be treated as external constants inside the class. It basically looks like this:

/// pseudo react-window/src/createGridComponent.js ... export default function createGridComponent({ // fixed-/variable-size specific functions and props ... }{ return class Grid<T> extends PureComponent<Props<T>, State> { // React component class definition, // which treats the fixed-/variable-size specific functions and props as constants ... } } ...

This pattern can't be immediately transferred to Svelte, since the component definition has to be put into a separate .svelte file. To achieve a similar behavior I add an extra prop named specificFunctionProps to the inner component, which is defined in src/GridComponent.svelte. All functions and props are passed into this object. The export from the .svelte file is a class, which I can extend within the function and overload the constructor to inject the outer prop:

/// svelte-window/src/createGridComponent.js import GridComponent from './GridComponent.svelte'; export default function createGridComponent( specificFunctionProps ) { return class Grid extends GridComponent { constructor(options){ options.props.specificFunctionProps = specificFunctionProps; super(options); } } }

In the script part of the GridComponent.svelte file, the functions are unloaded as const, since they won't change:

/// svelte-window/src/GridComponent.svelte ... export let specificFunctionProps; const { // fixed-/variable-size specific functions and props ... } = specificFunctionProps; ...

Props, state, and render

In React, every change in props or state will cause a rerender, unless it is stopped by shouldComponentUpdate member function. I Svelte, it is handled a bit differently. Take a look at the following component:

<script> let n=1, a, b; const getN = () => n; $: a = getN(); $: b = getN(n+1); </script> <h1>n: {n}, a: {a}, b: {b}</h1> <button on:click={()=>n++}>click</button>

You can execute the code in REPL. When you click the button, n increases. The function getN depends on n. Yet, if n changes, the reactive assignment of a is not executed, since the dependency on n is not explicitly shown in the statement. The assignment of b is executed, even though the function getN does not take any inputs. Other programming languages would throw an error if you don't match the expected number of inputs for a function, but javascript generously ignores the supplied n+1.

In the render function of the Grid and List components, there are function calls that mask the updated variables similar to the example that I just showed. To also rerun respective reactive statement when a prop or state changes, I load the props and state into an object like this to easier track change:

let props, state; $: props = { ... } $: state = { ... }

For example, In the render function, react-window calls a function this._getHorizontalRangeToRender(). It does not seem to take inputs, but in fact, inside it unloads most of the state variables and passes all props to the external callback getColumnStartIndexForOffset. Thus, I wrap the whole render code into a reactive bracket $: {...}.

The render function creates an array items and in React the children elements are directly created. This is not possible directly in Svelte. Instead, the items array becomes a state and is filled with the data, which is necessary to render the children. The array is passed down as a slot prop. I'll show down below how this can be used.

In react-window you can select the tags via outerElementType and innerElementType, so you can have a <span> inside a <main> or similar. For Svelte, there is an ongoing PR, which would easily allow this as well. As long as that is not merged, you will get a <div> in a <div> with svelte-window. I think this is fine for 99% of use-cases, so I will not spend time implementing a workaround.

Exported member functions

The member functions of a React component can be called from outside the component, eg. in a parent component. In react-window you can use scrollTo and scrollToItem. This is the example from the react-window site:

import { FixedSizeList as List } from 'react-window'; const listRef = React.createRef(); // You can programmatically scroll to an item within a List. // First, attach a ref to the List: <List ref={listRef} {...props} /> // Then call the scrollToItem() API method with an item index: listRef.current.scrollToItem(200);

It doesn't seem to be covered by the Svelte tutorial(yet?), but there is a simple syntax for it. You can use export const scrollTo = (...)=>{...} and use it similarly:

import { FixedSizeList as List } from 'svelte-window'; let listRef; <List bind:this={listRef} {...props} /> listRef.scrollToItem(200);

Additional callback on setState

On a React component, you update the state with setState and in Svelte this corresponds to a simple assignment. The setState function also takes a callback, which is called after the state has been updated and react-window uses this. Eg. in the scrollTo member function there is the following code:

this.setState(prevState=>..., this._resetIsScrollingDebounced);

In the ported code, I add a state variable request_resetIsScrollingDebounced, which is set to false. When the code is reached, that corresponds to the setState statement, I set it to true. Then I use Sveltes afterUpdate lifecycle function:

import {afterUpdate} from 'svelte'; let request_resetOsScrollingDebounded = false; export const scrollTo = ... afterUpdate(()=>{ if (request_resetIsScrollingDebounced){ _resetIsScrollingDebounced(); } });

React style objects

A good thing about React is that you can create inline styles with nice style objects, while in Svelte you can only pass a string to the style attribute. An important part of react-window is to pass a style object down to the children. I decided to also pass the styles as an object and provide a simple parser as additional export of svelte-window, namely the function styleString. This generates some overhead for the user. The rationale is that you can more easily work with objects, eg. if you want to perform cell-merging, you don't have to parse the string back. You can use this small function, which only handles styles that are injected by svelte-window. You could also import more elaborate functions, eg. react-style-object-to-css.

Additional exported member functions

For the variable-sized list and grid, additional methods are added outside of the class definition in src/createGridComponent.js.In src/VariableSizeGrid.js the callback initInstanceProps the keyword this is passed in as instance so that additional member functions can be added to the class. This makes sure, only the variable-sized varieties get these. The methods are resetAfterIndex/resetAfterIndices, resetAfterColumnIndex and resetAfterRowIndex. According to the documentation, these should be called when the row/column heights/widths change, which is typically not the case.

An identical implementation is not possible, since this is not referring to the class in a .svelte file. It is undefined. The way I implemented it is to add an exported object instance:

export const instance = {_getItemStyleCache:_getItemStyleCache};

This object is passed to initInstanceProps instead of this. As a consequence, the functions are attached to the instance member instead of the class itself. So, eg. a call in React which looks like this:

myGrid.resetAfterColumnIndex(42);

would look like this in Svelte:

myGrid.instance.resetAfterColumnIndex(42);

Hope it is not causing too much inconvenience.

Remove memoize-one

These have been all the important steps that are needed to port react-window. At this point, I was able to test it and it works well. It has only one external dependency left to memoize-one. It is not a big library, so I could leave it in. In general, memoization is a technique to improve the performance of an app. It means that you save the inputs and the outputs of an expensive function, and when you need to re-run the function with the same inputs, you just use the saved output instead of re-running. It is a common case in React, that functions are executed eg. on each render, even though nothing changed. memoize-one is a library that only keeps track of the last run of the expensive function.

When you port from React to Svelte, it makes sense to look at the memoized functions. In general, React defaults to re-execute functions due to the functional paradigm, while Svelte defaults to only execute functions if something changes, called reactivity. In this case, I think the case is different, because, to me, memoization was used in an unorthodox manner. Let's take a look.

Case one and two: onItemsRendered and onScroll

onItemsRendered and onScroll are props. You can supply a callback here, and when items are rendered or scrolling happened, these are fired. They are wrapped via memoize-one in _callOnItemsRendered and _callOnScroll and inside the function _callPropsCallbacks it is checked if the callbacks should be fired. It is not important if the callbacks are expensive and the return value is ignored. Memoization here is just a shorthand to check if the inputs changed. In total there are 7 variables for the list and 13 for the grid which is checked, so I add cache objects for both cases and check for differences manually. It is just a little more to write, but we can get rid of an external dependency quite easily.

Case three: _getItemStyleCache

Remember that memoizations typical use is to wrap expensive functions? In this case, a dummy function is wrapped:

const _getItemStyleCache = memoizeOne((_, __, ___) => ({}));

When you call this function with new inputs, it returns an empty object. This object is used to cache styles and when you call _getItemStyleCache with the same inputs, it will return the object with the styles, not an empty one. That is another funky use-case of memoize-one. I exchange that with this:

let _styleCache = {},_styleCacheCheck={}; const _getItemStyleCache = (a,b,c)=>{ if ( a === _styleCacheCheck.a && b === _styleCacheCheck.b && c === _styleCacheCheck.c ){ return _styleCache; } _styleCacheCheck = {a,b,c} _styleCache = {}; return _styleCache; }

Instead of two lines, there are 14 lines now, but it means that we can completely get rid of an external dependency.

How to use svelte-window

I noted before, that the children of the list cannot be rendered directly as in react-window. Instead, the item information is passed down as a slot prop, and the children have to be rendered explicitly. So, if we take the first example from the react-window site:

import { FixedSizeList as List } from 'react-window'; const Row = ({ index, style }) => ( <div style={style}>Row {index}</div> ); const Example = () => ( <List height={150} itemCount={1000} itemSize={35} width={300} > {Row} </List> );

it would look like this in Svelte:

<script> import { FixedSizeList as List, styleString as sty } from 'svelte-window'; </script> <List height={150} itemCount={1000} itemSize={35} width={300} let:items> {#each items as it (it.key)} <div style={sty(it.style)}>Row {it.index}</div> {/each} </List>

Here is another example with a fixed size grid with a scroll-to button, scrolling indicators:

<script> import { FixedSizeGrid as Grid , styleString as sty} from 'svelte-window'; let grid; const click = () => { if (grid){ grid.scrollToItem({ align: "start", columnIndex: 150, rowIndex: 300 }); } } </script> <Grid bind:this={grid} columnCount={1000} columnWidth={100} height={150} rowCount={1000} rowHeight={35} width={300} useIsScrolling let:items > {#each items as it (it.key)} <div style={sty(it.style)}> { isScrolling ? 'Scrolling' : `Row ${rowIndex} - Col ${columnIndex}` } <div> {/each} </Grid> <button on:click={click}> To row 300, column 150 </button>

With these two examples, it should be easy enough to replicate the examples from https://react-window.now.sh/.

Summary

With react-window I ported a flexible, high-quality React library to Svelte. There were few things, which are implemented differently, but after all the process is more or less the same. It will be interesting to see, how this library performs in real-world situations. Github currently lists 19k projects which depend on this library, so I think it might be a good addition to the Svelte eco-system.

The syntax is slightly different, the user needs to do a little more than with the React counterpart. I think that there is an upside to it since you have a little more control over the process. Especially it should be possible to merge cells and rows more easily. I'll go more into detail in an upcoming post.

I hope you enjoy this library. After all, porting was not a lot of work. Many thanks to the authors of this library, especially Brian Vaughn!

Share on Twitter - Discuss on Reddit - 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.