Svelte > React

Apr 7, 2024

I’m so sick and tired of waiting for React to get better. As much as people clown on Angular, I’ve always really liked it, but React was the hot new thing, and so I switched over like everyone else. “SPAs are the future!” I was told repeatedly. Vue comes along, bringing with it some of the best parts of Angular, but it still wasn’t so much better than React that I felt it worth switching. “UseEffects aren’t that bad,” I would tell myself in the mirror, “This functional components thing is gonna get better.” I’d just learned NextJS too, and who can forget that the React ecosystem was unbeatable.

One day, I stumbled upon Fireship’s video, SvelteKit is my mistress. Holy shit, what a breath of fresh air. The simplicity, intuitiveness, the brashness of its creator Rich Harris. Going back to React after toying around with Svelte just broke me. There was no longer joy. I’d daydream about the clean developer experience Svelte had to offer. I watched as Vercel attempted to make React usable with NextJS (which I’m still a fan of), but you can only polish a turd for so long. “The next version of React will borrow a lot from Svelte!” my Facebook software engineering friends would say. “Just hang in there!”

No. I will not wait. I can’t take it anymore. I feel like I’ve been stranded in a desert, and I’m expected to refuse water cause Gatorade may or may not be on the way. I canned all of Yuzu’s React code and spun up a fresh SvelteKit project a couple of months ago and never looked back since.

Svelte/Sveltekit has a lot of valid issues, but the only real criticism of Svelte I personally agree with is the lack of ecosystem. My frustration, however, is that it’s not going to develop one if people don’t jump in. React provides nothing that I like better than the Svelte equivalent, and it’s about time I reward Svelte for that. I’m taking the leap, and I hope you do too.

1. No more stupid hooks like useEffect , useState , etc.

// REACT
import React from 'react'

export default function Counter() {
  const [counter, setCounter] = React.useState(0);
	const doubled = React.useMemo(() => (counter * 2), [counter]);
	React.useEffect(() => console.log(counter), [counter]);

  return (
    <button
      onClick={() => {
        setCounter(counter + 1);
      }}
    >
      counter: {counter}
			doubled: {doubled}
    </button>
  );
}

-------------------------------------------------------

// SVELTE
<script>
	let counter = 0;
	$: doubled = counter * 2;
	$: console.log(counter);
</script>

<button on:click={() => (counter = counter + 1)}>
	counter: {counter}
	doubled: {doubled}
<

2. Component events instead of passing callbacks

// REACT
// Parent.jsx
import React from 'react'
import Counter from './Counter'

export default function Parent() {
	const [counter, setCounter] = React.useState(0);
	const increment = React.useCallback(
		(e) => {
			setCounter(counter + 1);
		},
		[counter]
	);

	return (
		<Counter increment={increment} />
	)
}

// Counter.jsx
export default function Counter({ increment }) {
  return (
    <button onClick={increment}>
      increment
    </button>
  );
}

-------------------------------------------------------

// SVELTE
// Parent.svelte
<script>
	import Counter from './Counter.svelte';
	let counter = 0;
</script>

<Counter on:increment={() => (counter = counter + 1)} />

// Counter.svelte
<script>
	import { createEventDispatcher } from 'svelte';
	const dispatch = createEventDispatcher();
</script>

<button on:click={() => dispatch('increment')}>
	increment
</button>

3. Conditional Classes

// REACT
export default function Label({ size, label }) {
	<div
		className={`
			p-1
			${size === 'big'
					? 'text-lg'
					: size === 'small'
					? 'text-sm'
					: ''
				}
		`}
	>
		{label}
	</div>
}

-------------------------------------------------------

// SVELTE
<script>
	export let size = '';
	export let label = '';
</script>

<div
	class="p-1"
	class:text-lg={size === 'big'}
	class:text-sm={size === 'small'}
>
	{label}
</div>

4. Inline logic

// REACT
export default function Sign({ status }) {
	return (
		<div>
			{status === 'stop' ? (
				<div className="text-red">STOP!</div>
			) : status === 'go' ? (
				<div className="text-green">GO!</div>
			) : (
				<div>Chilling</div>
			)}
		</div>
	)
}

-------------------------------------------------------

// SVELTE
<script>
	export let status;
</script>

<div>
	{#if status === 'stop'}
		<div class="text-red">STOP!</div>
	{:else if status === 'go'}
		<div class="text-green">GO!</div>
	{:else}
		<div>Chilling</div>
	{/if}
</div>

5. Built in css-in-js

// REACT
/** @jsx jsx */
import { jsx, css } from '@emotion/react'
// Take your pick of the dozens of options

export default function Block() {
	return(
	  <div
	    className={css`
	      padding: 10px;

	      &:hover {
	        color: white;
	      }
	    `}
	  >
	    Hover to change color.
	  </div>
	)
}

-------------------------------------------------------

// SVELTE
<div class="styling">
	Hover to change color.
</div>

<style>
	.styling {
		padding: 10px;

    &:hover {
      color: white;
    }
	}
<

6. Bundle Sizes

gzipped
React + React-Dom is 42.2KB
Svelte is 1.6KB

Svelte is over 96% smaller than React

7. Input Binds

// REACT
import React from "react";
export default function App() {
  const [name, setName] = React.useState("world");

  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <h1>Hello {name}!</h1>
    </>
  )
}

-------------------------------------------------------

// SVELTE
<script>
	let name = "world";
</script>

<input bind:value={name}>

<h1>Hello {name}!</h1>

8. No. More. Redux.

// REACT
// Jesus I can't believe I went back and wrote Redux code for this stupid post
// Like what the hell is this???

// counter.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = { value: 0 };

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
	    state.value += 1;
    },
  },
});

export const { increment } = counterSlice.actions;
export const selectCount = (state) => state.counter.value;
export default counterSlice.reducer;

// Counter.jsx
import { useDispatch } from 'react-redux';
import { increment } from './counter.js';
export function Counter() {
  const dispatch = useDispatch();

	<button onClick={() => dispatch(increment())}>
		Increment
	</button>
}

// Parent.jsx
import { useSelector } from 'react-redux';
import { selectCount } from './counter.js';
import Counter from './Counter';

export function Counter() {
  const count = useSelector(selectCount);

	return (
		<>
			<h1>Count: {$count}</h1>
			<Counter />
		</>
	)
}

-------------------------------------------------------

// SVELTE
// I feel safe again

// counter.js
import { writable } from 'svelte/store';
export const count = writable(0); 

// Counter.svelte
<script>
	import { count } from './counter.js';
</script>

<button on:click={() => count.update(n => n + 1)}>
	Increment
</button>

// Parent.svelte
<script>
	import { count } from './counter.js';
	import Counter from './Counter.svelte';
</script>

<h1>Count: {$count}</h1>
<Counter />

9. Slots vs Children

// REACT
// Parent.tsx
import Child from './Child'
export default function Parent() {
	return (
		<Child header={<h1>Header</h1>}>
			<p>Contents</p>
		</Child>
	)
}

// Child.jsx
export default function Child({ children, header }) {
	return (
		<div>
			{header || <h2>Fallback Header</h2>}

			<br />

			{children || <span>Fallback Children</span>}
		</div>
	)
}

-------------------------------------------------------

// SVELTE
// Parent.svelte
<script>
	import Child from './Child.svelte';
</script>

<Child>
	<h1 name="header">Header</h1>
	<p>Contents</p>
</Child>

// Child.svelte
<div>
	<slot name="header">
		<h2>Fallback Header</h2>
	</slot>

	<br />

	<slot>
		<span>Fallback Children</span>
	</slot>
</div>

menu