Porting React to Svelte

Sveltes tick is my new best friend - porting react-textfit to Svelte

By Michael Lucht
Keywords: Svelte, React, Textfit
March 11, 2021

Here is a tiny little helper from the React ecosystem, which is now available in Svelte. It is not a 1:1 port, since I concluded, that the textfit component should better be an action. I will explain the reasons below. In this post, I also cover some parts of the lifecycle of Svelte components and especially some praise for the tick function.

>

Check out the ported library svelte-textfit

Introduction

react-textfit exports a single component, which automatically adjusts the font size of its content to fill the whole element. It currently has 333 stars on GitHub. There are not so many use-cases for such a library, but I use it in one of my projects, which I would like to move over to Svelte.

In my project, I have a schedule filled with events. These can be short or take longer and they can spread over several days. This means that the cell size in the scheduling grid varies a lot. The content is only text, but it also can be only a short notice or contain a lot of details. The grid is quite large, so I use react-virtualized. As a consequence, I cannot easily adjust row and column sizes. react-textfit helps me to make the text as readable as possible while it still respects the cell boundaries.

Porting react-textfit

I have covered the basics on how to port a library from React to Svelte in a previous post, where I ported react-virtualized-auto-sizer.react-textfit is also implemented as a React class component, so I already discussed most of the steps you need to port it (like componentDidMount code goes to onMount lifecycle, render code is marked as reactive, etc.).

What is interesting about the library is how it works with the component lifecycle. When the component mounts or the props change, a function called process is started. The process interpolates the highest fitting font size. It does so by repeatedly adding callbacks to the setState function. It sets the font size via setState, renders the component, and looks if the text fits into the cell. Then it tries a different font size based on the previous results, renders again, and so on until the largest fitting font-size is found.

I already wrote about the additional callback in setState, but here it is entered on several occasions, so I thought about a more generalized way of porting. My first attempt (which I have implemented in svelte-window ... TODO: change it!) was to add a state array afterUpdateCallbacks. A setState call like this

setState({a:1},callback);

would be translated to

a = 1; afterUpdateCallbacks.push(callback);

and then in Sveltes afterUpdate lifecycle, the list would be processed:

afterUpdate(()=>{ afterUpdateCallbacks.forEach(v=>v()); afterUpdateCallbacks.length=0; })

That is ok, but then I read one more page from the Svelte tutorial, which brought me to a much more elegant solution:

>

the trick is the tick

The tick is also listed as a lifecycle function like onMount or afterUpdate but it works a bit differently. It is a function, which returns a promise. Promises are a great concept in javascript. They allow you, to execute code asynchronously. They are used, when some part of the code needs to wait for something to happen before it can continue to execute. For example, if you query a database, some part of the code would wait for a result. While it is waiting for the database to respond, other parts of the code could still be executed. Promises make that possible. In this case, we would want the callback to be called, when the component is rendered, so that we have access to the newly computed css values to check if the text fits.

There are usually two ways, how you can interact with a promise. One is the async/await syntax, which is often cleaner when you deal with a lot of promises. Here I work with the other option: .then. The tick resolves, when the state changes have been applied to the DOM, so, we can ditch the state array afterUpdateCallbacks and the afterUpdate lifecycle and the setState execution shown above translates to just this very convenient solution:

a=1; tick().then(callback)

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

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

Render ... render?

The render function returns an outer div and a nested inner div and inside of the inner div the children are rendered. Unlike react-window it is not possible here to dynamically choose the html element types here. Let's say we would like to adjust the label of a button. The final rendered DOM would look like this:

<button> <div /*textfit-outer-div*/ > <div /*textfit-inner-div*/> button text </div> </div> </button>

That is a lot of stuff to render for a little button. In most cases that is not a performance problem, but when you have a lot of components, it might become one, see excessive DOM size. What is really necessary?

Well, that depends. Css can be a piece of work, and different html elements behave differently. Eg. by default a div element uses width:100%. In the react-textfit setting with two divs the idea is that the outer div sets the boundary with this maximum width, while the inner div is set to display:inline-block so that the width shrinks to the actual occupied width. The library provides one use-case, which usually works. Doesn't it always work? No, you can easily kill it with custom css.

To me, that means that the process involves some trial and error anyway. That's why I decided to put it in the hands of the user. svelte-textfit does not export a component, but an action. To me, fitting text into some element is functionality, so an action also makes more sense semantically.

Action

An action in svelte is something which you attach to an html element with the use: directive. You can only add it to one element, but react-textfit adds two divs. The solution for me is that the action is applied to the inner div (which now can be any html element). If the setting with an outer div is wanted, you can fill the new parent prop like this:

<div bind:this={outer}> <div use:textfit={{parent:outer}}>...some text</div> </div>

The use of the outer parent is to provide width and height boundaries. Alternatively, you can set fixed pixel values with the new width and height prop.

<button use:textfit={{width:300,height:100}}>click me</button>

What needs to change in the code?

The action is not a Svelte component in a .svelte file, but a function in a .js file. It takes two inputs, node and props. node is a reference to the html element, to which we want to apply the action, and props is an object which contains the formerly exported props of the Svelte component. Other than that, there are very few changes to the code. The function is executed once when it is mounted, so the onMount stuff just goes into the body of the function. onDestroy code is put into the destroy attribute in the return object, just like the componentDidUpdate code after the check if the props did change goes into the update attribute. What about the other lifecycle, which we handle with tick? Does that still work? Yes, it actually does! I'm not sure, how I would have implemented it without tick, perhaps hacking with setTimeout. But that this works is another big reason why tick is great.

Additional props

I already wrote about parent, width and height but I decided to open up a few other things as well.

First, I added a prop update. The value of this is not used, but when it is reassigned, a re-run is triggered. Simple example, you have an input field and the text is shown in a p, you would want the p element to re-render on text changes:

<p use:textfit={{width:400,height:200,update:text}}>{text}</p> <input type="text" bind:value={text}/>

Expample:

The next change is the style prop. Here you can pass a function, how the style should change based on a value. The default is to just set the font-size of the node, but maybe you want to do something different, eg. also adjust the line-height style or increase the border-width. The library is applying an interpolation cycle: choose next candidate value -> apply style(node,value) -> check if element fits -> set next candidate value ...etc. You could do some crazy things here, use your imagination.

The last change is regarding the definition of 'fitting'. react-textfit only looks at the scrollHeight and scrollWidth are lower than the boundaries. With a simple style change to display:inline the scrollWidth is not applicable anymore. For such a case I added a fallback to use the elements offsetWidth instead. But maybe you would like to look at something completely different? In that case you can pass functions into the elementFitsWidth &#x26; elementFitsHeight props. These functions take the node and the width as input and should return a boolean, whether the component fits or not.

Typescript

The original library didn't have typescript definitions. I created a d.ts file for this library and also one for react-textfit. PR is ongoing.

Conclusion

If you need to dynamically fit your text into something, you now got a little helper in Svelte. Hope you like it.

What I definitely like is Sveltes tick function. It makes lifecycle manipulations super easy and it even works within actions. Kudos.

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.