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
s. 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.
useEffect
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
becomes useShuffle
since Svelte component names are capitalized.
Use Shuffle.svelte
<!-- "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
into two parts. The outer part would call Component
in the script section, the custom hook in the html section, and the inner component as a child with someCode
, props
and x
as props. The inner component would call y
and moreCode
.
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
inside the html section. If moreCode
is only one variable that you only use in one place, just go for it. But what, if you need z
in several locations?
z
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
directive. This works well if bind
can is reactive on the return value of the hook and can run well with initial values.
moreCode
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
and let
. Would hooks be necessary, if React had this kind of syntax?
bind
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:
- React Hooks in Svelte
- tree-shake-css - project page
- Sveltes tick is my new best friend - porting react-textfit to Svelte
- How to merge cells with svelte-window
- Porting React to Svelte - From react-window 1:1 to svelte-window
- Poll: Which React library would you love to see in Svelte?
- Porting React components to Svelte - It might be easier than you think
- Svelte and Typescript
- How this blog is made - Basic Structure, Elder and Markdown
- How this blog is made part 2 - Make your website sketchy with Tailwind and Roughjs
- Showcase: The Descent Ripple Effect or The React Descent Ripple Effect
- Why are component libraries so complicated with React compared to Svelte?