1. Home
  2. /
  3. Blog
  4. /
  5. Svelte for Reactaholics : A guide for React developers

Svelte for Reactaholics : A guide for React developers

July 28, 2021

Svelte for Reactaholics : A guide for React developers

Svelte recently gained tremendous popularity and love. As per the State of JS Survey 2020, Svelte had an 89% satisfaction score and a 66% interest score, which is remarkable!

The numbers can be attributed to the fact that Svelte is simple yet powerful.

Svelte is simple because itโ€™s based on HTML rather than JS. It is powerful because it offers tons of functionality, such as built-in animations and simple global stores. However, even after loving Svelte, many React devs prefer React over Svelte. Thatโ€™s because Svelte doesn't have Hooks or a vast community like React yet. It's partially true too. Hooks are so powerful that not having them in Svelte can feel like a turn-off.

But what if I told you that Svelte's got some tricks up its sleeve that can make the lack-of-hooks-pain go away completely? Letโ€™s take a look.

A Note

This article isn't going to be a definitive guide for moving from React to Svelte. It assumes you've gone through the Svelte tutorial and can write a basic app in it. It won't cover how props are different, how the state works (though there's a section where I talk about it a bit ๐Ÿ˜), etc.

If you want resources for syntax-to-syntax mapping, here are some great articles:๐Ÿ‘‡

Svelte for React Developers @ Soshace

Svelte for the Experienced React Dev @ CSS-tricks


This article will translate the React patterns in your mind to the Svelte equivalent - the kind of patterns that are not available in official docs.

For Class-Based React-ers

If you're still writing class-based React components rather than new Hooks-based syntax, I got a piece of bad news for you.

Wrapping your head around Svelte will be much harder, as class-based components are very different from Svelte components.

Unfortunately, I won't cover class-based components mapping with Svelte in this article, but only Hooks-based.

If you're comfortable with Hooks, no problem, read on. If not, you may want to learn React Hooks or even consider forgetting all the patterns you have in mind. I would recommend learning Svelte-specific practices from scratch rather than relating the two.


For Hooks-Based React-ers

Good news! This article is especially for you. Letโ€™s warm up with some basic stuff.


Local State

In React, the useState hook is used as follows:

const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  return <main>Modal is {isOpen ? 'open' : 'closed'}</main>;
};

In Svelte, we don't have a concept of state variables and regular, non-reactive variables. Anything can be reactive or non-reactive, based on usage. Let me explain.

The above code written in Svelte is as follows:

<script>
  let isOpen = false;
</script>

<main>Modal is {isOpen ? 'open' : 'closed'}</main>


Right now, this variable isn't reactive. It's a value we're storing somewhere. We are not changing it but simply accessing it.

However, say we put a toggle button to change the isOpen value.

<script>
  let isOpen = false;
</script>

<main>
  <button on:click="{() => isOpen = !isOpen}">Toggle</button> <br />
  Modal is {isOpen ? 'open' : 'closed'}
</main>


When you click on the button, the text will change from โ€˜Modal is openโ€™ to โ€˜Modal is closed.โ€™ How did this happen? How did the non-reactive variable suddenly become reactive?

That's Svelte's magic. It analyzes your code and changes the DOM when it truly needs to be changed. Then, react re-runs the entire code again, and when anything in the ancestor changes, Svelte changes only those elements that need to be changed.

But that's not the critical point here.

Take Away: Every variable in Svelte is a reactive state by default (no special useState, State, or anything.) You may declare a let variable, modify it directly, and you're done. And the ones you don't modify are simply values stored somewhere else, totally un-reactive, and do not affect perf.


Scratching Your Itch: useState in Svelte

If you find the "just-define-variable-and-it-becomes-state" concept strange, here's something that could make the transition easy: useState in Svelte

Here's the code:

export function useState(initialState) {
  const state = writable(initialState);

  const update = (val) =>
    state.update((currentState) => (typeof val === 'function' ? val(currentState) : val));

  const readableState = derived(state, ($state) => $state);

  return [readableState, update];
}

And you can use this the same way you use useState in React.

Oh, and if you're a TypeScripter, you may want to copy the following function instead.

export function useState<TState>(initialState: TState) {
  const state = writable(initialState);

  const update = (val: (e: TState) => void | TState) =>
    state.update((currentState) => (typeof val === 'function' ? val(currentState) : val));

  const readableState = derived(state, ($state) => $state);

  return [readableState, update] as const;
}

This should work well and give you the correct IntelliSense.


Effects

First off, the header says Effects, not useEffect because React contains both useEffect and useLayoutEffect, and they both work differently.

If you only care about side effects when some state changes, and you donโ€™t really care about DOM changes (as in, you don't care when they change), you can treat the $: syntax as your useEffect as just side-effects. Then,

useEffect(() => {
  console.log(name);
}, [name]);

Becomes ๐Ÿ‘‡

$: console.log(name);

You do not need to provide any dependency array. Svelte will automatically sort out your dependencies and trigger this effect whenever any dependency changes.

But sometimes, you may use several variables in your reactive statements. In that case, you may not want the statements to trigger on all of them; you can use a very similar method to provide your own dependency array.

function doSomethingBigWithLotsOfDeps(state1, state2, state3, state4, state5) {
  /* Do epic shit! */
}

$: doSomethingBigWithLotsOfDeps(state1, state2, state3, state4, state5);

Here, state1, state2, and others are reactive variables passed to the function. Inside the function, you can use as many other reactive variables. The reactive variables changing will trigger the reactive statement only when any of these 5 reactive variables are changed. If you don't want to see state4 and state5, do not accept the following in the function: ๐Ÿ‘‡

function doSomethingBigWithLotsOfDeps(state1, state2, state3) {
  /* Do epic shit! */
}

$: doSomethingBigWithLotsOfDeps(state1, state2, state3);

Emulating useEffect and useLayoutEffect

useEffect vs. useLayoutEffect:

useEffects in a component are gathered, put in a queue, and executed after the component has rendered its HTML. On the other hand, useLayoutEffects are collected and run before the DOM rendering process begins.

Now, if you want to know how your side-effects are timed with respect to your DOM changes, I got some bad news for you: $: statements in Svelte are equivalent to useLayoutEffect, not useEffect.

The useLayoutEffect runs when state changes but before React goes to change the DOM according to the new state (the order React runs these: useLayoutEffect -> DOM ย rerendering -> useEffect). The same is the case with $: reactive statements. If you want useEffect, you need to use afterUpdate that runs after the DOM is updated, but no dependencies there. It runs every single time any DOM update happens, plus the pattern doesn't look as good.

You'd end up doing your own diffing. Hence, use it sparingly.

Cleanup Function?

One prominent feature of useEffect and useLayoutEffect is the ability to return a cleanup function. Svelte's reactive statements, unfortunately, have no built-in method to do so, but we can apply the following hack to achieve it:

let cleanup;
$: {
  cleanup?.();

  doStuff();

  cleanup = () => someFunctionThatDoesCleanup();
}

And no, this won't trigger an infinite loop. So any reassignment inside a reactive statement won't trigger that reactive statement again.

Alternatively, sometimes you'd need to do a cleanup when the component is destroyed. Hence, you'd need to run the following cleanup function inside the `onDestroy` lifecycle method:

onDestroy(() => {
  cleanup?.();
});


Scratching Your Itch: useEffect in Svelte

If you find this pattern a little complex to wrap your head around, there's another way in which you can actually use useEffect in Svelte.

const useEffect = (subscribe) => ({ subscribe });

export let track;

let effect;
$: effect = useEffect(() => {
  doStuff();

  return () => {
    doCleanupStuff();
  };
});
$: $effect;

This may feel weird, but it shows how you can have useEffect in Svelte, albeit in a slightly different manner. For an excellent explanation of this technique, read this blog post: A Svelte Version of useEffect


Styling

Styling is something we devs don't think much about, but in React, styling is pure pain! There's no recommended way of styling, so you have to read about a dozen libraries for days and then choose one in frustration. Having choices is excellent, but itโ€™s frustrating if there's no base recommended way to fall back to.

Now, this is the most kickass feature of Svelte for me: Styling in place.

Svelte is like a plain HTML file: You can put your styles right in your component file. Unlike HTML, however, the styles are scoped to that component.

<section class="container">
  <img class="avatar" src="..." />
  <div class="userInfo">
    <div class="userName">...</div>
    <div class="userStatus">...</div>
  </div>
</section>

<style>
  .container {
  }

  .avatar {
  }

  .userInfo {
  }

  .userName {
  }

  .userStatus {
  }
</style>

Itโ€™s so tidy, clean, and straightforward :)

And these styles are scoped by default. You don't have to do anything else to achieve the scoping, unlike React, where first you have to learn ten different ways of styling, spend days researching them, and then just pick one in frustration.

Unlike CSS Modules, you don't have to litter your HTML with css.container or css.avatar Instead, you can stick solely to class. You can style IDs and tag names directly. You can do whatever you want without letting the framework come in your way. This is the beauty of styling in Svelte.
Unlike Styled components, you need not declare style in a string template and install VSCode extension for syntax highlighting and autocomplete, which itself is pretty buggy a lot of the time.

But you probably know this already. So I won't beat around the bush.

Let's talk about something that is not discussed at all. This is something that we React devs find very difficult in Svelte: Styling Components from Outside.


Styling Components From Outside โ€“ In React

Let's assume you have an Avatar component for displaying a user's profile picture in the card component.

For brevity, let's assume the code above to be in React that somehow has scoping.

Say you want to modify the style of the Avatar component from outside, that is, from the card component only. How do you do this?

Well, if you are using CSS Modules/Emotion/Any other library where you define your styles and have a className to work with, like with CSS modules, then: ๐Ÿ‘‡

/* CSS MODULES */
import css from './Card.module.css';

export const Card = () => {
  return (
    <section className={css.container}>
      <img className={css.avatar} src="..." />
      <div className={css.userInfo}>
        <div className={css.userName}>...</div>
        <div className={css.userStatus}>...</div>
      </div>
    </section>
  );
};

Or with JSS

import { createUseStyles } from 'react-jss';

const useStyles = createUseStyles({
  container: {
    /* Container styles here */
  },
  avatar: {
    /* Avatar styles here */
  },
  userInfo: {
    /* userInfo styles here */
  },
  userName: {
    /* userName styles here */
  },
  userStatus: {
    /* userStatus styles here */
  },
});

export const Card = () => {
  const css = useStyles();

  return (
    <section className={css.container}>
      <img className={css.avatar} src="..." />
      <div className={css.userInfo}>
        <div className={css.userName}>...</div>
        <div className={css.userStatus}>...</div>
      </div>
    </section>
  );
};

So when we swap it with the Avatar component and style it ourselves (Let's say, make it square rather than round), the markup is as simple as this:

<section class={css.container}>
  <Avatar class={css.squaredAvatar} />
  <div class={css.userInfo}>
    <div class={css.userName}>...</div>
    <div class={css.userStatus}>...</div>
  </div>
</section>

As you can see, we simply pass the new style along to the Avatar component as css.squaredAvatar class, and our Avatar component is set up to take all the props passed to it. The props are then passed to the underlying <img> component. This just works!

But if you try the same thing in Svelte (assuming you have set up Avatar to accept all the props and apply them to the underlying <img>), then:

<script>
  import Avatar from './Avatar.svelte';
</script>

<section class="container">
  <Avatar class="squaredAvatar" />
  <div class="userInfo">
    <div class="userName">...</div>
    <div class="userStatus">...</div>
  </div>
</section>

<style>
  .container {
  }

  .squaredAvatar {
  }

  .userInfo {
  }

  .userName {
  }

  .userStatus {
  }
</style>

...this won't work.

Svelte will pass along the class you gave it to the underlying <img> element, but it won't apply the styles.

Svelte's limitation is that the class must be on a DOM element, like div , section or <img>, and only then can Svelte recognize it. But if you declare styling for the squaredAvatar class and apply it to a component, Svelte will mark your style as unused and remove it in production.

The fix for this is using :global().

Find a parent of the component. In this case, it's the section.container element.

Then define your style like this:

.container :global(.squaredAvatar) {
  /* Your styles go here */
}

Breakdown: We use a class of a component that Svelte can recognize, then we write a:global(.squaredAvatar) after it. Using :global in Svelte is like telling Svelte that you know what you're doing, you're right, and the compiler is wrong. And so, Svelte Compiler will let the squaredAvatar class be preserved here as it is.

Why not directly use :global(.squaredAvatar) {?

Because if you use this directly, Svelte will output the style globally .squaredAvatar { /* Styles here */ }, which could mess up with any other .squaredAvatar class defined anywhere else in your app. Even if it is scoped, it will be affected.

That's why we scope this class to our component by writing it as a child of an element that Svelte can scope.


Composing Classes and Conditional Classes

If you're using CSS like JS in React and want to compose multiple classes, you have to use the following syntax: ๐Ÿ‘‡

<div className={`${css.class1} ${css.class2} ${css.class3}`} />

This isn't as bad. But look - when you have to apply classes based on condition, things get messy.

<div className={`${css.class1} ${condition ? css.class2 : ''} ${css.class3}`} />

As you can see, now it has become pretty complex. Make class3 conditional, and the whole thing is going to look unreadable and cluttered.

Hopefully, you might have fixed this problem by using libraries like classnames and clsx.

<div className={clsx(css.class1, condition && css.class2, css.class3)} />

Itโ€™s much cleaner and intentional, but itโ€™s kind of a bummer that we have to pull in a whole library only to do conditional classes cleanly, especially when these use cases are present in almost every app.

In Svelte, you need nothing else. First, let's see how you do the non-conditional example above. ๐Ÿ‘‡

<div class="class1 class2 class3" />

And that's it! Ultimate cleanliness!

And the conditional class is even easier. ๐Ÿ‘‡

<div class="class1 class3" class:class3="{condition}" />

This is why I love Svelte so much. The developer experience it provides is the best in class. ๐Ÿ˜


Props

Props in React are a little different in terms of authoring but behave more or less the same, ultimately.

export let prop;

Prop with a default value? ๐Ÿ‘‡

export let prop = 'Hello';

Props with TypeScript? ๐Ÿ‘‡

export let prop: string = 'Hello';

Works flawlessly!


Rest props ({...props})

When authoring general-purpose reusable components, such as Button or Image, where you want the component to simply accept all the props it is given and pass them as it is to its child component, the actual <button> or <img> component, you use is:

const Button = ({ children, ...props }) => {
  return <button {...props}>{children}</button>;
};

And every prop that you pass to Button will be given to <button>. This pattern is instrumental.

Svelte has $$props and $$restProps variables available globally in every component. To achieve the behavior of passing all props to an element from the component ย in Svelte, you use these variables.

$$props: All the props passed to the component, including the ones you declare (export let propName)

$$restProps: All the unknown props, i.e., the ones not declared in the component

So the component above will simply become this: ๐Ÿ‘‡

<!-- Button.svelte -->

<button {...$$restProps}>
  <slot />
</button>


TypeScript

In React, if your general-purpose component is a simple wrapper over Button or any other component, you can type your props to accept all the props that <button> or <img> would take.

export const ButtonBase = ({ children, ...props }: JSX.IntrinsicElements['button']) => {
  return <button {...rest}>{children}</button>;
};

When you add props to <ButtonBase>, you'll get IntelliSense of every property passed to a regular <button>. Also, you won't be able to enter any property like src or href, which simply does not exist on a <button>. So you get some fantastic Type Safety too.

Unfortunately, in Svelte, there's no way to give types to $$props or $$restProps. This is one place where Svelte loses some significant points, for me, as I'm a die-hard TypeScript Dev ๐Ÿ™ƒ.

Global State

In React, the most popular way to have a global state is using the context API. And even more popular is using useReducer with it. Well, some news for you (good or bad, depends on you), Svelte has neither of those (it has context, though it's a little different from the React context). And reducers are discouraged, but you can create your own methods of using reducers (more on that later).

Svelte context

Svelte has context API that is similar to React but with a few differences. But if you're reading this article and have some experience with Svelte, I recommend using Svelte Stores. They're one of the best APIs in Svelte and make global state management amazingly easy.

Svelte Stores ๐Ÿ’ช

Svelte Stores are one of the best parts of Svelte - super easy to use, simple to understand, and overall one of the best API Designs I have ever seen.

So, a Store API is actually closer to useState in React, but it can be declared inside the component as well as outside, in a separate file. So you can use Stores as local state for your components too. ๐Ÿ˜€

But all that aside, here's the syntax: ๐Ÿ‘‡

import { writable } from 'svelte/stores';

export const count = writable(0);

Here we have created a store named count and initialized it with a value of 0.

Now you can create function that update the value of count, and use them anywhere,

function incrementCount() {
  count.update((n) => n++);
}


You can even subscribe to these stores and watch them.

count.subscribe((c) => console.log(c));

And you can have immutable stores that simply can't be changed in any way. You can use these to create values that should come from somewhere else, like a timer store.

export const time = readable(new Date(), function start(set) {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);

  return function stop() {
    clearInterval(interval);
  };
});

Now time will update on its own. You won't be able to modify it. Think of it as a reactive Date. Date as state; updating your other state is just so cool. You can even use readable stores to create reactive localStorage wrappers, triggering updates if localStorage was changed.

export const theme = readable(null, function start(set) {
  const interval = setInterval(() => {
    set(localstorage.getItem('theme'));
  }, 500);

  return function stop() {
    clearInterval(interval);
  };
});

As you can see, we made a theme variable that comes exclusively from localstorage. To change this state, you'd have to change the localstorage value directly.

Note: This isn't entirely reactive, as we put a timer of 500ms. So changes within the 500ms window won't be caught, and only the last one will be caught. But still, this pattern is considerable.

And you can compose multiple stores to form 1 big store that changes when any of its constituents change. ๐Ÿ‘‡

import { derived } from 'svelte/stores';
export const elapsed = derived(time, ($time) => Math.round(($time - start) / 1000));

This is another reactive store telling us the time has elapsed since a given time.

Or with many more stores ๐Ÿ‘‡

export const derivedVal = derived(
  [name, baseStore],
  ([$name, $baseStore]) => {
    return $baseStore.counter * 2;
  },
  0
);

1st argument takes an array of all the stores you want to compose. In the 2nd argument, the function, you can destructure the parameter as an array and you'll get your values.

Using in components

Using Svelte stores in components is an amazing experience. By using Svelte's magic, you can get stores to work just like local state. ๐Ÿ‘‡

<script>
  import { time } from './stores';
</script>

<div>The time currently: {$time}</div>

I simply imported the time store and used it directly as a value by putting a $ before the name. Where did this variable come from? Svelte made it available! Now your template will update automatically as $time changes!!.

Scratching Your Itch: useReducer in Svelte

Now, as I promised, I will show you how you can implement the useReducer pattern in Svelte, exactly like React.

function reducer(count, action) {
  switch (action.type) {
    case 'increment':
      return count + 1;
    case 'decrement':
      return count - 1;
    default:
      throw new Error();
  }
}
const [count, dispatch] = useReducer(0, reducer);

And this is how you would define useReducer:

function useReducer(state, reducer) {
  const { update, subscribe } = writable(state);

  function dispatch(action) {
    update((state) => reducer(state, action));
  }

  return [{ subscribe }, dispatch];
}

Here it is. We're using a store to implement the state here. But again, I implore you, try to use this as minimally as possible. Svelte is simple, so the overall code should be simple too. ๐Ÿ™‚

Final Note

I hope this article helped you understand some React-related patterns in Svelte. Now go out there and make your own kickass Svelte web apP - tiny, fast, and marvelous!


Like what youโ€™re reading?

Get Audio/video engineering tips straight into your inbox