Porting React to Svelte

React Hooks in Svelte

By Michael Lucht
Keywords: Svelte, React, Hooks
March 18, 2021

With functional components, React tried to establish a new approach for UI development. It was a risky move, but it was successful: a large group of developers jumped onto the train, who had to learn the new hook logic. I show how these hooks can be translated into Svelte code. This might be helpful, if you try to port some React code to Svelte, or if you want to understand the differences and similarities of both frameworks.

Introduction

In previous blog posts, I described how to port some React libraries, but so far all have been class old-school components only. Some time ago the React team decided, that they want to embrace the functional programming paradigm and today it seems to be en vouge to only use functional components.

Functional programming is an interesting concept with several ingredients, eg. immutability. In a general-purpose language like javascript, it is a lot about self-discipline. One of these rules is that whenever possible, you should structure your code into pure functions. Pure functions only depend on their inputs, are stateless, don't change anything outside of the function and always, for the same inputs, return the same results. If you are not so disciplined, React has so-called hooks for you.

Here is a list of built-in hooks:

I will not cover useReducer (similar to useState), useLayoutEffect (similar to useEffect) and useDebugValue (specific to React DevTools). You also can write custom how, I will not write about them here, they will be the topic of a follow-up post.

useState

With this basic hook, you can add state to your component. An equivalent implementation in Svelte is more a question about the structure you want to in your code. If you have a basic React component

export const MyComponent = (props)=>{ const [thing, setThing] = useState(42); const anotherThing = doSomething(thing); return ( <Child1 a={thing} b={anotherThing}> <Child2 c={setThing}/> </Child> ) }

the simplest Svelte implementation would be

<script> let thing = 42; $: (anotherThing = doSomething(thing)); </script> <Child1 a={thing} b={anotherThing}> <Child2 bind:c={thing}> </Child1>

In this translation, you don't have an explicit setter function. Child2 can change thing, but also sees the value. In React that could cause problems, because with every change in thing Child2 would re-render. In Svelte, code does not re-run by default, you have to mark it as reactive, so this implementation usually doesn't trigger unnecessary re-renders. But of course, if you prefer to have setters you are free to implement it yourself const setThing=(v)=>thing=v;.

Another difference is that the React thing is a const, which means you can't change it directly. This is where the immutability from the functional programming paradigm is applied. If you see a lot of value in it, I would recommend the following implementation using readable stores:

import {readable} from 'svelte'; const useState = (init) => { let set; const state = readable(init,(s) => { set = s; return () => set = undefined; } return [state,set];

This separates the value and the setter and you use it like in React with the one exception that you have to use the $ symbol in front of your state variable (eg. $thing), since it is a store.

useEffect

The name 'effect' comes from the term 'side-effect'. In functional programming, this is considered something you should try to avoid whenever possible. Yet, if you look into a few React libraries, there can be a lot of these. The useEffect hook has quite a few different use-cases. When you transition from React class components, most lifecycle functions end up as useEffects. componentDidMount and componentWillUnmount are implemented as an effect with an explicitly empty dependency array:

useEffect(()=>{ window.addEventListener('keydown',handler); return ()=>window.removeEventListener(handler); },[])

This can be translated to:

import {onMount} from 'svelte'; onMount(()=>{ window.addEventListener('keydown',handler); return ()=>window.removeEventListener(handler); })

Another case is when useEffect is used without a dependency array. It means that it should run after every update, so Sveltes afterUpdate lifecycle comes to mind. This can work well, but it also might be a difference between Svelte and React, since React components can update more often.

The callback which is passed to useEffect can return another callback to clean up. A simple effect without cleanup-callback (the return value) like this:

useEffect(()=>{ doSomething(a,b); },[a,b])

Can often be translated to a simple reactive call:

$: doSomething(a,b);

The reason is that when a or b change, a subsequent state change inside doSomething has to wait for the render process to complete first due to the functional programming paradigm in React. In Svelte you can often trigger the changes right away. One exception is when state changes could cause a loop. In the following example, a random number between 0 and 1 is assigned to the state belowHalf. The reactive statement should be executed until belowHalf gets a value smaller than 0.5. But with this implementation, that would not happen:

let belowHalf = 1; $: belowHalf = belowHalf < .5 ? belowHalf : Math.random();

The statement is only executed once because Svelte batches the reactive statements. Instead, you could implement it, so that it makes use of the component lifecycle, like in the useEffect:

import {tick} from 'svelte'; let belowHalf = 1; $: tick().then( _=> belowHalf = belowHalf < .5 ? belowHalf : Math.random() )

If you need a cleanup callback, you need to implement it yourself:

$: (_cleanup = (()=>{ if (_cleanup){ _cleanup(); } doSomething(a,b); return cleanup; })());

The tick version could look like this:

$:(_cleanup=tick() .then(_cleanup||Promise.resolve) .then(()=>{ doSomething(a,b); return cleanup; }) );

useContext

Both React and Svelte have a context API. For React you need to create a context object with createContext, while in Svelte a string is used as a key. For a 1:1 translation, you could change

const someContext = React.createContext();

into

const someContext = "someCntextUniqueString";

Then you could think that the useContext hook can be easily translated:

const value = useContext(someContext);

to

let value = getContext(someContext);

And that can work occasionally, but in general, there is a caveat: if the value is changed by the context provider in React, it triggers a re-render. As the Svelte tutorial suggests you should use a store as the value. The only small difference then it's that the context consumer in Svelte is now able to change the context. If you don't want that I suggest applying a readable store pattern like in the last example in the useState section.

Another problem is that Reacts create context can return a default value if no context provider is found. This can lead to errors, b if an object with attributes is expected. Here is how I would implement it:

const someContext = React.createContext(defaultObject);
import {writable} from 'svelte/store'; const someContext = () => writable(defaultObject); // In a script part of a Svelte component const value = getContext(someContext)||someContext();

useCallback

The components in React are always re-created on render. Sometimes you don't want to pass around a new reference for a function every time. In that case, you use useCallback. In Svelte you don't have this constant re-creation, so if you define a function in a Svelte file, its reference would not change. Let's look at this example:

const cb = useCallback((a) => foo(a,b),[b])

Usually you can just use let cb = foo, but if you need (the reference to) cb to change, when a value in the dependency array updates, you can implement it like this:

const _cb = (_b) => (a) => foo(a,_b); $: (cb = _cb(b));

useMemo

Ok, it is just that:

React:

const value = useMemo(someFunction, [a,b]);

Svelte':

$: (value = someFunction(a,b));

The only thing you must make sure it's that a and b explicitly appear in what you write for someFunction, and if the React code violates the exhaustive Dependency array rule, that variables you don't want to trigger an update of value are not appearing explicitly.

useRef

In React, useRef is a way to introduce a mutable value. In Svelte, mutable values are common,v just create them with let. The ref in react is an object with the attribute current, you can replicate this or keep in mind to remove the .current. I prefer the latter.

useImperativeHandle

That's a catchy name, right? Let me explain the use of this hook, in case you never heard of it: When there were only React class components, you could add public member functions to it eg. on a FancyList component you could add the function scrollTo(itemID). On the parent component, you would get a ref and call the function directly, so you could do something like this:

const Parent = () =>{ const listRef = useRef(null); return <> <FancyList ref={listRef}/> <button onClick={()=>listRef.current?.scrollTo(500)}> }

To reach compatibility with the class based approach, the React team introduced the useImperativeHandle hook. You can lookup the exact syntax here. Svelte components are a bit closer to class components than functional components, so it is pretty easy to replicate the functionality, by using export const:

/// FancyList.svelte <script> export const scrollTo = (itemID)=>{...} </script> ...

In a parent component you can get the reference to the FancyList with bind:this and call scrollTo:

/// Parent.svelte <script> let fancylist; </script> <FancyList bind:this={fancylist}/> <button on:click={()=>fancylist?.scrollTo(500)}>

Conclusion

Hooks are an important ingredient for React functional components. I show how you can replicate the behavior in Svelte and I give you code examples for the most common use-cases. If you encounter a case, which is not covered here, feel free to send me a note.

Most of the examples only work at the top level of a Svelte component. Custom hooks move the functionality into separate functions, which complicates the replication in Svelte. What to do in that case is the topic of an upcoming post.

Share this article 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.