fubits.dev fubits.dev

Svelte 5 Patterns (Part 2): $props, $effect, JSDoc types, Set, Map, and more

More ongoing learnings and new patterns from working with Svelte 5.

💡 This post will serve as a collection of ongoing notes.

💡 Read part 1 of this series on simpler shared runed state, derived state and more.

General Learnings

From studying the migration guide and current GitHub issues for Svelte / Svelte 5 we learn that:

  • actions still exist (e.g. for event modifiers)
  • tick still exists
  • event handling is now more „native“
    • write handler functions that accept an event object, and handle as intended in the Event API

    • this is even more important for event modifiers that relied on syntactic sugar like event|preventDefault

      <script>
      	const handleEvent(e) => e.preventDefault()
      </script>
      
      <button onclick={handleEvent} />
      

Notes on JSDoc Types & IntelliSense

  • Reminder: there is a component annotation / documentation pattern with @component

    Screenshot of a basic component docs example. @slot probably should be replaced with @image (snippet).
    Screenshot of a basic component docs example. @slot probably should be replaced with @image (snippet).
    • unlike JSDoc typing, it must be within the markup / HTML section
    • but it seems you can just put it at the top of the file before the very first <script> tag:
    <!--
    @component
    @slot default - The map container with image, vector, and text annotations
    -->
    <script>
    	import { getContext } from 'svelte'
    	import { fade } from 'svelte/transition'
    	// ...
    </script>
    
    <div>your markup</div>
    
    • TBC: if types can be defined / imported / exported here instead of within
  • JSDoc types can be recycled - exported / imported - when defined in <script module /> (Maintainer comment)

    • define in one component
    // $lib/ComponentA.svelte
    <script module>
    /** @typedef {"myType"} MyType */
    </script>
    
    <script>
    	// your logic
    </script>
    
    • import and recycle in another component
    // $lib/ComponentB.svelte
    <script>
      /** @import { MyType } from "$lib/ComponentA.svelte" */
    
      /**
       * @param {MyType} x
       */
      function stuff(x) {
      
      }
    </script>
    
    • you can even import in utility libraries / .js files (that probably includes .svelte.js files)
    // $utils/+utils.js
    /** @import { MyType } from "$lib/ComponentA.svelte" */
    
    /**
     * @param {MyType} x
     */
    function stuff(x) {
      
    }
    

Notes on $effect

  • $effect and onMount are not the same

    • onMount() is only run when the component is mounted.
    • $effect is run when
      • the component is mounted
      • when any of the tracked dependencies are changed
    • $effect.pre runs before changes are applied to the DOM
  • observation: it’s unclear if onMount is supposed to be deprecated or not (probably a Svelte 6 thing)

  • $effect is not encouraged (Docs, GitHub Issue by Core Maintainer, Tweet by Core Maintainer)

  • $effect doesn’t run on the server

    • $derived does
  • $effect.once() doesn’t exist yet (svelte@5.1.2), but can be simulated with

    import { untrack } from 'svelte';
    
    $effect(() => {
    	untrack(async () => {
    		await fetch("/");
    	})
    });
    
  • async within $effect is discouraged (Maintainer)

  • async within onMount is discouraged (example)

Notes on $props()

  • to only extract specific properties and collect the rest

    let { foo, bar, ...rest } = $props();
    
  • to collect / preserve all properties: don’t destructure

    let props = $props();
    
    • pass to child
    <Button {...rest} />
    <Button {...props} />
    
    // Button.svelte
    <script>
    	let props = $props();
    </script>
    
    <button {...props}>
    	click me
    </button>
    
  • $bindable can have a default

    let { item = $bindable("default") } = $props()
    

Reactive Built-Ins: Set, Map, Date, URL, URLSearchParams with Svelte 5

  • these so called “built-ins” aren’t reactive off the shelf
  • instead, there’s a collection of reactive “built-ins” in svelte/reactivity (Docs)

💡 as of svelte@5.1.3:

  • SvelteDate
  • SvelteMap
  • SvelteSet
  • SvelteURL
  • SvelteURLSearchParams

TODO / WIP: Standalone modules / UMD / ES6 / IIFE with Vite & Svelte 5

💡 if you don’t know what this means, what this Svelte Summit Talk by Jesse Skinner: https://www.youtube.com/watch?v=uWxkaDdqfpI

TL;DR: Svelte is a compiler; you can compile Svelte components to regular standalone JavaScript modules and mount them wherever you can run JavaScript. You can even pass functions as props and expose component methods to the mounting scope.

  • in Svelte 5 components are functions - before, they were Classes (e.g. new SvelteComponent())
  • mount with either mount or hydrate
    • hydrate picks up server-rendered HTML
    • you can preserve the CSS with a compiler option: css: 'injected'
    • options now include an events: {eventType: callback()} property
    • tbc: $state might be used in the instantiating scope
      • only in .svelte / .svelte.js
    • there’s unmount for destroying
    • see working example for a server-side-rendered Svelte component using Bun and the Svelte compiler: https://github.com/fubits1/bun-svelte-ssr

What I don’t understand yet

  • $state.snapshot

    💡 why and how and what for?

    $effect( () => {
    		// Get a snapshot :
    		const data = $state.snapshot(config);
    
    		// Save data
    		// ...
    		console.log(data);
    	});
    
  • level or granularity of shared / bindable reactivity (example GitHub issue)

  • migration guide: „accessors option is ignored“

    “Setting the accessors option to true makes properties of a component directly accessible on the component instance. In runes mode, properties are never accessible on the component instance. You can use component exports instead if you need to expose them.” (Svelte 5 Migration Guide)


💡 to be continued