Porting React to Svelte

How to Implement Custom React Hooks in Svelte

By Michael Lucht
Keywords: Svelte, React, Hooks, Custom Hooks
April 6, 2021

You want to port something from React which you miss in Svelte? Or you just want to understand the differences between Svelte and React? You're at the right place. I'll show two strategies, hours you can translate custom hooks to Svelte. The first approach is very direct and takes a good look at how hooks work in general. The second one is the one I would recommend. So, if you just want to see the quickest approach, you can skip to the second part.

Introduction

When React introduced functional components it felt like you could achieve more with less code. The old class components have a lot of boilerplate. Being able to write simple components with an easy function call was a revelation. It still is great, if you can keep it simple. Somehow I felt like I was failing in this. I (over-?) optimized my code, which meant that I used a lot of hooks, mostly useEffects. It leads to hard-to-read code with, again, a lot of boilerplate. My solution ultimately was to switch to Svelte, but other developers with more patience improve the readability by embracing a new concept: the custom hook. Here is a good article about it.

Custom hooks are an extension of the basic hook concept. They are very flexible since they are 'just' functions, whose name has to start with 'use'. Their special feature is that you can call other hooks from within them. There is no direct equivalent feature in Svelte. So without further ado, let's see how you can translate the concept.

The problem

Let's pick up an example from the previous post about basic hooks:

const Component = ()=>{ const [state,setState] = React.useState(1); React.useEffect(()=>{ if (state > .5) setState(Math.random()); },[state]) return <div>{state<.5 ? state : "shuffle"}</div> }

The component uses state and one effect. It draws a random number, renders the result, and runs the effect. If the number is below .5 it stops. If it is larger, it draws a new random number, and the cycle restarts.

If we want to achieve the same behavior in Svelte, we can use tick:

import {tick} from 'svelte'; let state=1; $: tick().then(()=>{ if (state > .5) state = Math.random(); })

So far it is pretty easy. The real trouble starts with custom hooks. Let's just wrap the hook code into a function:

const Component = ()=>{ const state = useShuffle(); return <div>{state}</div> } const useShuffle = ()=>{ const [state,setState] = React.useState(1); React.useEffect(()=>{ if (state > .5) setState(Math.random()); },[state]) return state < .5 ? state : "shuffle"; }

From a React perspective, there is no big difference. The shuffle functionality is moved out of the main component so that we can use it in other components. How would you port that?

The problem is that we cannot apply most of the tactics from my previous post within a normal javascript function, since they use reactive statements, lifecycle functions, and even simple state variable initializations.

First Idea

A 1:1 solution is to implement an own hook framework. Here is a great article, where the process is described, I borrow some code from there and adjust it to work within Svelte. You store the information for the hooks in a single array at the top of your component. All hooks follow in a single reactive statement since they need to be called in exactly the same order:

import {tick} from 'svelte'; import {initHooks} from 'myHooks.js'; let hooks:[]; let updateDummy = 0; const requestUpdate = ()=>{updateDummy++} $: { // initialize the Hooks initHooks(hook,tick,requestUpdate,updateDummy); // all hooks here useShuffle(); }

Then you need to implent the hooks and the initialisation:

/// myHooks.js let _hooks = [], _tick=new Promise(r=>r()), _currentHook=0,_requestUpdate=()=>{}; export const initHooks = (hooks,tick,requestUpdate) => { _hooks = hooks; _tick = tick; _currentHook = 0; _requestUpdate = requestUpdate; } export function useState(initialValue) { _hooks[_currentHook] = _hooks[_currentHook] || initialValue // type: any const setStateHookIndex = _currentHook // for setState's closure! const setState = newState => {(_hooks[setStateHookIndex] = newState);requestUpdate()} return [_hooks[_currentHook++], setState] } export function useEffect(callback, depArray) { const hasNoDeps = !depArray const deps = _hooks[_currentHook] // type: array | undefined const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true if (hasNoDeps || hasChangedDeps) { _tick().then(callback) _hooks[_currentHook] = depArray } _currentHook++ // done with this hook }

The hook initialization works in a singleton-ish style so that the hooks themselves don't need a reference to our hooks array. This can make you nervous, can't a different process change it in the middle of the execution? It depends. In React, there are extra hook rules, which make sure, that all hooks are called in the same order. If you stick to these rules, the code should work. Unlike in React, the IDE would not warn you, if you violate the rules.

The advantage of this approach is that we only need to implement all hooks once, add just a little code to the top of each component, which uses hooks. Most of the React code can be directly re-used.

The disadvantage is that we ignore all the nice tools Svelte offers. Instead of using reactivity, we need to build our own checks if inputs changed. Also, we have to add a boilerplate to each component, which uses custom hooks.

My preferred solution

As you may recall from the previous post, implementing the basic hooks is relatively straightforward in a Svelte component. Can we use that?

A React component is a function, which can call hooks and returns a jsx object. A custom hook is a function that can call hooks but returns an arbitrary javascript object.

Just like we translate React components, we can put our hook code into a svelte component. But how do we return a value? Slot props!

Let's look at the example. The custom hook useShuffle becomes Use Shuffle.svelte since Svelte component names are capitalized.

<!-- "UseShuffle.svelte" --> <script> import { tick } from 'svelte'; let state = 1; tick().then(()=>{ if (state > .5) state = Math.random(); }) </script> <slot state={state < .5 ? state : "shuffle"} />

Calling it from the baseline component works like this:

<!-- "Component.svelte" --> <script> import UseShuffle from './UseShuffle.svelte'; </script> <UseShuffle let:state> <div>{state}</div> </UseShuffle>

Boom, done. I don't know if this solution was obvious to you, but when I realized, it is so easy it felt great. What if your custom hook needs an input? You simply add a prop to your Svelte hook.

Order of execution

In React, hooks should usually be executed somewhere at the beginning of the component code. The return values can be used in the code that follows. When the hook is ported into a component, it is called after the code in the script tag. You might need to refactor the code.

My example is tiny, no change is needed. To my experience, in many cases, the return value of the hook will be passed down to the child components add it is. Or, there are only minor adjustments, which you can easily inline into the html section of your Svelte component.

But let's say, you have a more complicated component, which fits into this pseudo-React-code format:

const Component = (props) => { const x = someCode(props) const y = useSomeCustomHook(x, props); const z = moreCode(x, y, props); return render(x, y, z, props);

You have at least two options. The first is to split Component into two parts. The outer part would call someCode in the script section, the custom hook in the html section, and the inner component as a child with props, x and y as props. The inner component would call moreCode and render.

There is not a lot that can go wrong with this first approach, but it may not be very elegant. The code is stretched out into at least three files. What if you have even more custom hooks, each using the return values from the previous hook?

The second option is to call moreCode inside the html section. If z is only one variable that you only use in one place, just go for it. But what, if you need z in several locations?

In that case you can try the following. In your script section:

let z; const wrapMoreCode = (x, y, props) => { z = moreCode(x, y, props); return ""; }

This function can be called inside a mustache {} bracket right after the hook:

<UseSomeCustomHook let:y {x} {props}> {wrapMoreCode (x, y, props)} render(x, y, z, props) </UseSomeCustomHook>

Since the function returns an empty string nothing visible is added to the DOM. Make sure to add all dependencies as input to the wrap function, so that it is reactive.

I like using slot props since it resembles the functional nature of the hook, but alternatively, you could also use the bind directive. This works well if moreCode can is reactive on the return value of the hook and can run well with initial values.

Conclusion

I showed two strategies you can apply to port a react custom hook to Svelte. The first approach directly replicates the hook framework on a component level. It gives some good insights, how hooks work in React.

The second approach is more a 'Svelte' way. Currently, I am finishing up a port of framer-motion, a React animation library. It uses a ton of custom hooks and I was able to apply this approach in almost all cases. Within Svelte components, you can apply the strategies for basic hooks, which I discussed in the previous blog post. Sometimes the code needs to be refactored.

React wants you to follow a functional programming paradigm. But it also lets you jailbreak from it via hooks. As such, I consider it a pattern you should try to avoid. Libraries like Framer-Motion do the opposite: they almost raise this pattern to a new form of art.

A custom hook is a generalization of a React component. The only difference is, that the hook can return anything, not only a jsx object. A Svelte component is powered by syntax additions let and bind. Would hooks be necessary, if React had this kind of syntax?

The hook pattern extracts functionality. The implementation I show here in Svelte shares some common features with actions. Unlike actions, you have access to context and all lifecycle functions. Also, actions can only be added to DOM elements. But the biggest advantage is that you can use reactive statements, while you have to write the update function yourself in actions. So, this pattern might be interesting, when when you are not porting code from React.

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.