<History {pomodoros}/>
This is something I've longed for with React!
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 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.
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.
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.
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>
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.