Home
/ Blog /
Ultimate Svelte Guide for React DevelopersOctober 21, 202223 min read
Share
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.
We recently build Clubhouse Clone using Svelte and 100ms.
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.
Try your hands on 100ms React SDK with building Slack Huddle clone.
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.
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.
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.
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.
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);
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 the 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 which 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.
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?.();
});
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 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
orcss.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.
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.
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 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!
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>;
};
```svelte
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: 👇
```svelte
<!-- Button.svelte -->
<button {...$$restProps}>
<slot />
</button>
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 🙃.
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 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 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 the 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 functions that updates 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 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!!.
Now, as 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, to try to use this as minimally as possible. Svelte is simple, so the overall code should be simple too. 🙂
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!
General
Share