Svelte Thoughts: Svelte Components
I wanted to try doing more documentation/publishing of things I’m studying/reading. The main goal is to use note-taking as a means of making my reading process more active/engaged. The publishing is so that I move more in the direction of doing shareable work.
I have used Svelte for a couple small projects and wanted to really go through the documentation to solidify my understanding. This document will contain my notes as I read through the Svelte docs.
Format
I will quote from the docs using >
syntax, and then enter my thoughts underneath the quote. Quotes/thoughts are separated with a horizontal line.
Example:
Here’s something from the document I’m reading
And here’s my thoughts
Notes
A
<script>
block contains JavaScript that runs when a component instance is created.
Coming from Android, I’m quite sensitive to lifecycle events
as a frontend concept. This stands out as a thing that you’d absolutely have to keep in mind when designing/implementing components.
A thing I like here is that it comports with my (vague) understanding of how websites work from back in my middle school days. <script>
tags are not just “loaded” to be run at some later time when a page loads. No, they are executed eagerly when encountered. Nice that Svelte components follow that standard. Makes it easy to learn if you have only a slight familiarity/background in web dev.
1. Export Creates a Component Prop
Svelte uses the
export
keyword to mark a variable declaration as a property or prop, which means it becomes accessible to consumers of the component
I like this. I don’t know much JS, but it’s nice to have a keyword that clearly defines what will be shown as the public-facing api of a component. I come from a background (Android) where things have increasingly moved in the direction of consolidating UI state into well-defined state objects. The Svelte approach has structural similarity, or at least supports it. I can imagine defining something like:
<script>
export let uiState: MyUiStateType;
</script>
And then consumers of the component would provide the necessary state to render the view.
The other side of state management in a component, responding to component-related events, is similarly well supported:
<script>
export let eventSink: (e: MyComponentEventType) => void;
</script>
You can specify a default initial value for a prop. It will be used if the component’s consumer doesn’t specify the prop on the component (or if its initial value is
undefined
) when instantiating the component. Note that if the values of props are subsequently updated, then any prop whose value is not specified will be set toundefined
(rather than its initial value).
I don’t think I fully understand the last part of this section. Is it saying that a consumer, which normally passes in all the props, would end up overriding a default value with undefined
if the consumer passes in some, but not all of the props? If so, hmm that seems kinda bad/unexpected/surprising in a negative way. Would seem to imply that you are either all in on managing all the state of a component (never relying on default behavior of the component) or all in on default behavior. Would be nicer to support a mix-and-match approach, if possible.
If you export a
const
,class
orfunction
, it is readonly from outside the component. Functions are valid prop values, however, as shown below.
Seems kind of interesting, but I tend to expect my components to be simple/dumb. The only thing they should expose is 1) a data object that controls what is rendered and 2) a way to receive events from the component. read-only functions from a component seem to be unnecessary, at least in my worldview of clean/safe/efficient ui development. The place for those kinds of things is likely in some other part of the system (in Android, it’d be called a ViewModel/Presenter/Controller etc).
update: I asked ChatGPT what are some common use cases for exporting a function/class from a component. Some of the answers didn’t seem super common/relevant. But I thought this one was interesting:
[[Pasted image 20240406191850.png]]
A higher-order component would indeed be cool/powerful, as a concept. It’s hazy to me exactly how you’d build this or fit everything together. I’ve never seen anything like this conceptually in Android. If you wanted logging or performance analysis stuff, you’d inject it into the ViewModel and configure/call into those things at that layer.
You can use reserved words as prop names.
<script> /** @type {string} */ let className; // creates a `class` property, even // though it is a reserved word export { className as class }; </script>
Interesting feature. Feels like it exists because people really want a very specific conception of a ‘clean’ API. Strikes me as unlikely to be worth the confusion to use this kind of thing.
2. Assignments Are ‘Reactive’
To change component state and trigger a re-render, just assign to a locally declared variable.
<script> let count = 0; function handleClick() { // calling this function will trigger an // update if the markup references `count` count = count + 1; } </script>
This is beautiful. On Android, usually we have to publish something in a Flow
and call the .copy()
method of a data class
to get reactivity. This is obviously much simpler/nicer as an API.
Because Svelte’s reactivity is based on assignments, using array methods like
.push()
and.splice()
won’t automatically trigger updates. A subsequent assignment is required to trigger the update. This and more details can also be found in the tutorial.
<script> let arr = [0, 1]; function handleClick() { // this method call does not trigger an update arr.push(2); // this assignment will trigger an update // if the markup references `arr` arr = arr; } </script>
Very large gotcha here. Easy to forget. Likely hard to identify that this is the error vs alternatives (e.g. “oh I must have forgot to call handleClick()
in the right spot”). Probably leads to significant frustration for newcomers.
3. $: Marks a Statement As ReactivePermalink
Any top-level statement (i.e. not inside a block or a function) can be made reactive by prefixing it with the
$:
JS label syntax. Reactive statements run after other script code and before the component markup is rendered, whenever the values that they depend on have changed.
This is pretty nice. Seems like this is the first “non-standard” syntax added by Svelte. Oh, actually if you follow the link, this syntax is part of a JS standard. Ok, well done. I like it.
Only values which directly appear within the
$:
block will become dependencies of the reactive statement. For example, in the code below total will only update when x changes, but not y.
<script> let x = 0; let y = 0; /** @param {number} value */ function yPlusAValue(value) { return value + y; } $: total = yPlusAValue(x); </script> Total: {total} <button on:click={() => x++}> Increment X </button> <button on:click={() => y++}> Increment Y </button>
Significant gotcha. Another one of those things where forgetting would be a painful debugging experience. I guess the silver lining is that if it hurts a lot, that’s going to make it easier to remember going forward 😂
It is important to note that the reactive blocks are ordered via simple static analysis at compile time, and all the compiler looks at are the variables that are assigned to and used within the block itself, not in any functions called by them. This means that
yDependent
will not be updated whenx
is updated in the following example:
<script> let x = 0; let y = 0; /** @param {number} value */ function setY(value) { y = value; } $: yDependent = y; $: setY(x); </script>
Moving the line
$: yDependent = y
below$: setY(x)
will causeyDependent
to be updated whenx
is updated.
Another gotcha, though probably not super common. I think a small lesson so far is that you don’t want to overly-rely on these nice reactive features in your component. Fortunately, if your ui rendering is a pure function of a state object, and your component exposes only that state object and a way to receive events, I think you can get pretty far without using these $:
type features more than a few times in any given component.
On the other hand, the existence of these features makes me wonder if maybe there’s an expected approach to building components that is different from my expectation. Maybe in JS/web development, components are expected to be more like microservices than pure functions. Maybe components are supposed to be making network requests and updating themselves as mostly independent entities. The main reason that we avoid that in Android seems to be that testing code in/near the UI layer is very hard. If you put logic in your UI layer, it’s much easier to get that code wrong or introduce a regression that isn’t caught in testing vs putting logic in the viewmodel layer. Thus, on Android, views are simple/(hopefully) pure functions of state. And the handling of things like making network requests of DB updates is done in a different layer.
Prefix stores with
$
to access their valuesA store is an object that allows reactive access to a value via a simple store contract. The
svelte/store
module contains minimal store implementations which fulfil this contract.Any time you have a reference to a store, you can access its value inside a component by prefixing it with the
$
character. This causes Svelte to declare the prefixed variable, subscribe to the store at component initialization and unsubscribe when appropriate.Assignments to
$
-prefixed variables require that the variable be a writable store, and will result in a call to the store’s.set
method.Note that the store must be declared at the top level of the component — not inside an
if
block or a function, for example.
Cool. I feel like I never got a great answer for cross-component communication in Android. “Create a parcel and send it to the other fragment” always felt heavy, but requiring a component to know all consumers or having consumers always know exactly which component provided a piece of data also felt heavy. This feels like a really lightweight inter-component communication mechanism. Does seem like it would have some tradeoffs around making debugging harder for “who updated this state” or “who sent me this state update,” but I’m a little hazy there.
Also fair to note that you probably don’t want to have a ton of store usage in your site. As the complexity of interactions would likely grow very quickly. Best to keep things in a more well-defined hierarchy if possible.
You can create your own stores without relying on
svelte/store
, by implementing the store contract:
Maybe good to know, but also seems doubtful I’d use this for most applications. Wonder what kinds of applications need custom store types.
A
<script>
tag with acontext="module"
attribute runs once when the module first evaluates, rather than for each component instance. Values declared in this block are accessible from a regular<script>
(and the component markup) but not vice versa.
This is interesting. I feel like this falls outside my understanding of “things you’d want to do in your frontend.” But it ties in with the idea that “lifecycle events are important,” so worth noting.
Variables defined in
module
scripts are not reactive — reassigning them will not trigger a rerender even though the variable itself will update. For values shared between multiple components, consider using a store.
Ah, this hints at the use of “module-level” scripts: sharing data across components. So like, a shared object that knows how to fetch/update resources from the network. In kotlin/java, this would be how you’d do package-scoped singletons and such.
CSS inside a
<style>
block will be scoped to that component.
Love this! I feel like a huge barrier to entry for me with CSS is the extent to which the “cascading” can occur across your entire frontend. Limiting that to a specific file feels like a great choice for “unit of cascading,” though it might lead to repetition. Maybe there’s a way to pass styles to children to avoid that, though.
This works by adding a class to affected elements, which is based on a hash of the component styles (e.g.
svelte-123xyz
).
Good implementation detail to keep in mind.
To apply styles to a selector globally, use the
:global(...)
modifier.
Hmmm seems important, seems a little dangerous.
p:global(.red) {
/* this will apply to all <p> elements belonging to this
component with a class of red
}
They give an example of how to use global
scoped css for descendants of a component, which seems like the way you’d mostly want to use global
if you are gonna rely on it.
There should only be 1 top-level
<style/>
tag per component.
Important.
However, it is possible to have
<style>
tag nested inside other elements or logic blocks.
In that case, the<style>
tag will be inserted as-is into the DOM, no scoping or processing will be done on the<style>
tag.
Good to know, hopefully don’t rely on this too much.