This content originally appeared on Jack Franklin and was authored by Jack Franklin
Last year I created Pomodone, a small time tracking application based on the Pomodoro technique of working in 25 minute intervals. It's a pretty basic app; it has a 25 minute timer (that runs in a Web Worker) and saves a history of your "poms" to a small Firebase database. I initially built it using React (well, Preact actually) but I then started to play around with Svelte, and decided rebuilding the app in Svelte might be a nice way to blog about the similarities and differences between the libraries.
This is not a post declaring Svelte to be better than React, or vice-versa. This is a post where I'll tell you about my preferences, and what I find easier or harder with either framework. I'm not here to pick a fight! Plus, Pomodone is hardily a vastly complex application that could be used to fully put React or Svelte through its paces. Think of this post as a commentary based on my experience throwing a side project together, focusing on the developer experience putting these components together.
Authentication
The app uses Firebase Authentication to log a user in via either their GitHub or Google account. I love Firebase Authentication, it's such an easy way to add auth to side projects.
React's hooks are a great way to package this up; I create a useCurrentUser
hook which listens out to authentication changes and sets some state
accordingly. I can then trust React to re-render as required when an
authentication change is noted.
export const useCurrentUser = () => {
const [currentUser, setCurrentUser] = useState(undefined)
useEffect(() => {
return firebase.auth().onAuthStateChanged((details) => {
setCurrentUser(
details
? {
displayName: details.displayName,
provider: {
'google.com': 'Google',
'github.com': 'GitHub',
}[details.providerData[0].providerId],
uid: details.uid,
}
: null
)
})
}, [])
return [currentUser]
}
Within any component, I can write:
const [currentUser] = useCurrentUser()
This is nice; it's low effort and lets any component quickly access the current
user. The only downside of this is that you potentially have many
onAuthStateChanged
listeners; I could mitigate this by only binding one
listener, or by putting the current user in a
context instead.
Talking of context, that's much closer to the approach I take with Svelte and use a writable store.
export const currentUser = writable()
export const listenForAuthChanges = () => {
return firebase.auth().onAuthStateChanged((details) => {
if (details) {
currentUser.set({
displayName: details.displayName,
provider: {
'google.com': 'Google',
'github.com': 'GitHub',
}[details.providerData[0].providerId],
uid: details.uid,
})
} else {
currentUser.set(null)
}
})
}
Within the top level Svelte component, I can call this within onMount
, which
will run once when the component is mounted (the function is return
ed so we
unsubscribe when the component is removed, much like how useEffect
lets you
return a function).
onMount(() => {
return listenForAuthChanges()
})
Now anywhere in my Svelte codebase, a component can import the currentUser
writable store, and act accordingly. What I like is that currentUser
isn't a
value, it's a store, and therefore you have full control over how you deal with
it. You can either subscribe to it and manually control with state changes:
currentUser.subscribe(newValue => {
...
})
Or, if you want to just read the latest value, you can prefix it with a $
:
console.log($currentUser)
This is where some of Svelte's syntax trickery begins to shine; this dollar
prefix trick automatically subscribes you to the store's latest value. I both
like and dislike this; it's a nice syntax once you know it, but it's a bit odd
as a beginner to get used to. However I like that Svelte doesn't make me use the
subscribe
API every time I need to read the value.
As far as basic authentication goes, both libraries seem to take similar approaches here. Whilst the terminology and exact syntax differs slightly, both allow you to subscribe to a Firebase listener and get updated when the authentication state changes. React's contexts and Svelte's stores play almost identical roles for their library.
Using a worker
Pomodone has to keep a 25 minute timer going and try to be as accurate as
possible. If a browser tab is backgrounded (e.g., not the focused tab), most
browsers will lower the priority of its setTimeout
calls and not run them
strictly to time. Most of the time on the web this isn't a massive deal, but
when a user is tracking 25 minutes of work via your app, it is! Plus, over the
course of 25 minutes, any slight time drift will cause the final time to be
quite far off. However, if these timeouts are moved into a web worker, they
should run to time and not get de-prioritised by the browser.
Therefore, in my Tracker
component, I need to instantiate a web worker, send
it messages and receive data (such as time remaining) back. This is one area
where I found React more "admin heavy" than Svelte; because React components are
re-executed every time the component re-renders, you can easily end up with
thousands of workers being created! It's essential to use
useRef to avoid this
problem by maintaining a reference to the worker that you've created.
Firstly I set up the initial state I need for the component:
const [currentPom, setCurrentPom] = useState(null)
const [currentUser] = useCurrentUser()
const worker = useRef(null)
And then create a useEffect
hook that will instantiate the worker, if
required, and bind an event listener to listen for messages:
useEffect(() => {
if (!worker.current) {
worker.current = new Worker(workerURL)
window.worker = worker.current
}
const onMessage = (event) => {
if (event.data.name === 'tick') {
setCurrentPom((currentPom) => ({
...currentPom,
secondsRemaining: event.data.counter,
}))
} else if (event.data.name === 'start') {
// More branches removed here to save space...
}
}
worker.current.addEventListener('message', onMessage)
return () => {
worker.current.removeEventListener('message', onMessage)
}
}, [currentUser])
And then, when the user hits the "Start" button, I have to send the worker a message:
const onStartPom = () => {
if (!worker.current) return
worker.current.postMessage('startTimer')
}
Svelte looks pretty similar, but has two small changes that personally make the Svelte code easier to read, in my opinion:
- We don't have to keep the worker in
useRef
, and can just assign it to a variable. - We can pull the event listener code out into a function more easily, as that
function won't then become a dependency to a
useEffect
- at which point we will have to wrap it inuseCallback
.
Instantiating the worker is now:
let worker
onMount(() => {
worker = new Worker(workerURL)
worker.addEventListener('message', onWorkerMessage)
return () => {
worker.removeEventListener('message', onWorkerMessage)
}
})
We also don't have to set state by using React's setX(oldX => newX)
convention, and can just mutate the local variable:
function onWorkerMessage(event) {
if (event.data.name === 'tick') {
currentPom = {
...currentPom,
secondsRemaining: event.data.counter,
}
} else if (event.data.name === 'start') {
// More branches here removed to save space...
}
}
Here's where I start to have a preference for Svelte; the two are very similar
but once I got used to Svelte I found that React felt like jumping through
hoops. You can't create a worker instance, it has to go in a useRef
, and then
you can't easily pull code out into a function without then requiring
useCallback
so it can be a safe dependency on useEffect
. With Svelte I write
code that's closer to "plain" JavaScript, whereas in React more of my code is
wrapped in a React primitive.
Conditional rendering
One part of React that I've always championed is how it's just JavaScript. I like that in React you don't use a distinct template syntax and instead embed JavaScript, compared to Svelte's templating language:
<ul>
{pomsForCurrentDay.map(entryData, index) => {
const finishedAt = format(new Date(entryData.timeFinished), 'H:mm:ss')
return <li title={`Finished at ${finishedAt}`}>{index + 1}</li>
})}
</ul>
<ul class="poms-list">
{#each currentDayPoms as value, index}
<li
title={`Finished at ${format(
new Date(value.timeFinished),
'H:mm:ss'
)}`}
>
{index + 1}
</li>
{/each}
</ul>
I was pleasantly surprised by Svelte's templating; in the past I've found templating languages overwhelming and inflexible, but Svelte offers just the right amount of templating whilst enabling you to use JavaScript too. That said, I will always find React's approach easier - at least in my head - and I think more friendly to people familiar with JavaScript who are learning a library.
However, Svelte does have some nice touches to its syntax when it comes to rendering components (which feels very JSX-like). My favourite is the ability to collapse props:
<History pomodoros={pomodoros} />
<History {pomodoros}/>
This is something I've longed for with React!
Reactivity in Svelte with $
React requires us to use useEffect
and other hooks because it fully controls
how all your code is run and re-runs your code whenever a component is
re-rendered. Svelte is different in that by default most of your code is only
going to run once; a console.log('foo')
line in a component will only run when
that component is first rendered. In React, it will run many times.
React's re-rendering approach has its upsides: let's say you are taking in a big list of data and running some function to convert it into data that you can render. In React, within your component, you can write:
const input = props.inputData
const transformed = input.map((item) => transformItem(item))
return <div>{JSON.stringify(transformed, null, 2)}</div>
And this will always be up to date - should the user provide new
props.inputData
, the component will re-render and the output will be updated.
The same is not true in Svelte:
<script>
export let input;
const transformed = input.map((item) => transformItem(item))
</script>
<div>{JSON.stringify(transformed, null, 2)}</div>
Here the output will be rendered the first time the component is rendered, but
then not updated at all. We can solve this in two ways, either by using the $:
label syntax, which marks the code as reactive, or by moving our transform logic
into the template:
<script>
export let input;
$: transformed = input.map((item) => transformItem(item))
</script>
<div>{JSON.stringify(transformed, null, 2)}</div>
<script>
export let input;
</script>
<div>{JSON.stringify(input.map((item => transformItem(item))), null, 2)}</div>
This is another example of Svelte taking JavaScript syntax and using it for a slightly different meaning; it tells Svelte that the statement is reactive and should be recalculate should any imports change. You might also call it a "computed property". The second solution simply moves the logic into the template, thus ensuring that when the component re-renders the logic is executed again. In my time with Svelte this is the approach I've gone with most of the time, often pulling out the logic into a function:
<div>{calculateOutputForItems(input)}</div>
Coming from React to Svelte this did catch me out numerous times but for me I
now prefer Svelte's approach, particularly because it removes some of the
boilerplate around useEffect
.
Component composition
Component composition is a huge part of what makes working with a component
based framework enjoyable or not and it's something that both React and Svelte
solve well. React's children
prop makes it very easy to render any provided
content:
function Box(props) {
return <div>{props.children}</div>
}
function App() {
return (
<Box>
<p>hello world!</p>
</Box>
)
}
(If you've not read it, the React guide on Composition is well worth a read).
Svelte does similar, using slots:
<!-- Box component -->
<div class="box">
<slot></slot>
</div>
<!-- App component -->
<Box>
<p>hello world!</p>
</Box>
They take different approaches when it comes to multiple children, and this is where I find myself preferring Svelte's approach more. React suggest passing through multiple props:
function Box(props) {
return (
<div>
<div class="left">{props.left}</div>
<div class="right">{props.right}</div>
</div>
)
}
function App() {
return <Box left={<p>hello</p>} right={<p>world!</P>} />
}
One gripe I've had with this approach is that you lose the visual cues that you're passing children into the Box component; they now aren't nested within the Box when you render them like we're used to in HTML; it's now up to you to read the props and spot which ones are being used to provide children. It's easily done on this dummy example, but harder in "real world" applications - or at least, I find it harder!
Svelte's approach is to define multiple slots with explicit names to let the user provide the elements that should fill those slots:
<!-- Box component -->
<div class="box">
<slot name="left"></slot>
<slot name="right"></slot>
</div>
<!-- App component -->
<Box>
<p slot="left">hello</p>
<p slot="right">world!</p>
</Box>
I like this approach more because I can scan the code that renders the Box
component and easily spot that it takes two children. If the Box
took any
props, they'd be within the opening <Box>
tag, and they would be distinct from
any children props.
My preference here is biased by the fact that I spend everyday at work building web components, so Svelte's approach feels very familiar to slots in web components.
Styling
I enjoy that Svelte has an opinion about styling; especially in the context of small side projects like Pomodone, it's great to have that decision made for me. The fact that Svelte can also detect unused CSS is great, and this is one of the reasons why I suspect I'll reach more for Svelte in future projects.
This isn't really a downside to React; one of React's strengths is that it lets you control so much and slot React into your environment, but I like that Svelte comes with a good CSS story out the box.
Conditional classes
One small feature I love about Svelte is how I can apply classes conditionally to an element:
<div class:is-active={isActive}>
This will apply the class is-active
to the element, but only if the value
isActive
is truthy. This reads well, is clear and is great that it comes out
of the box.
I have used classnames to achieve similar functionality in React, and it's a good solution, but I enjoy that Svelte provides this out the box.
Binding event listeners
Similarly to conditional classes, Svelte packs in some extra utilities for
binding event listeners in the form of
modifiers. These let you modify
event listeners to ask Svelte to include common functionality, such as calling
event.preventDefault()
, for you.
<script>
function click(event) {
event.preventDefault()
// logic here
}
</script>
<button on:click={click}>
Click me!
</button>
<script>
function click() {
// No need to preventDefault ourselves
// logic here
}
</script>
<button on:click|preventDefault={click}>
Click me!
</button>
Conclusion
I like both React and Svelte. Put me in a codebase with either of them and I'll enjoy it, be productive and happy putting new features together or fixing bugs. I have side projects in React, and others in Svelte, and I'm in no rush to convert any from one to the other. React and Svelte are very similar in many ways, but what I've found is that in all the little ways that they are different, I prefer Svelte. The codebase for Pomodone makes more sense to me in Svelte, not React. I find it easier to navigate and work with.
If I were to sum up why in one sentence, it's because I don't miss
useEffect
. I understand why it exists, I understand the approach React
takes, and there are benefits of its approach. But writing complex React
components feels more like admin; a constant worry that I'll miss a dependency
in my useEffect
call and end up crashing my browser session. With Svelte I
don't have that lingering feeling, and that's what I've come to enjoy. Svelte is
there when I need it with useful APIs, but fades into the background as I put my
app together.
This content originally appeared on Jack Franklin and was authored by Jack Franklin

Jack Franklin | Sciencx (2021-03-09T00:00:00+00:00) Comparing Svelte and React. Retrieved from https://www.scien.cx/2021/03/09/comparing-svelte-and-react/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.