Best Practices

Best practices for working with Svelte and SvelteKit applications.

Svelte & SvelteKit

https://svelte.dev/

Svelte is our standard JavaScript framework for developing web-based applications.

What

Svelte was selected as our web framework of choice. SvelteKit is the meta-framework that helps build full stack web applications, using Svelte as its component framework.

How

Svelte and SvelteKit come with many features that serve a wide variety of web applications.

Svelte best practices

Some general rules to keep in mind:

  • Components SHOULD NOT contain page-level logic such as handling forms or manage global state. Try to keep component logic to a minimum and handle page state on the server.

  • A foundational or primitive component MAY be a global CSS class, or a component, but MUST NOT be both. Be sure to abstract highly reusable and mostly-atomic components.

  • Follow the project-configured formatter for code style. We RECOMMEND configuring your editor to autoformat files upon save.

    The current tool for formatting .svelte files is Prettier and it comes with opinions such as formatting props with same-named variables to shorthand. For example:

    -<Button onclick={onclick}>Let's go</Button>
    +<Button {onclick}>Let's go</Button>

Prop names

  • Use variant to represent visual variants.

  • Prefer single-word prop names, but use camelCase if necessary.

  • Skeleton loaders SHOULD be separate components, and not a prop toggle.

  • Flags: props that toggle a feature via true or false values are represented as plain adjectives without an is prefix. Examples: expandable, selectable, selected. Prefer to have them default to false for easy truthy flagging. i.e. absence of selectable implies selectable == false, but the presence of selected toggles to true.

  • HTML attributes are provided via Type declarations and should not be included as explicit props unless it has special treatment as part of a component.

    • ✅ Appropriate prop inclusion: disabled for <Button>.
    • ❌ Inappropriate prop inclusion: aria-label for <TextInput>.
  • If a component contains an underlying interactive element, then a ref prop with 2-way binding should be provided to allow focusing via JS. If a component contains multiple focusable targets, those props MAY use a prefix such as closeButtonRef.

  • Props with multiple options such as size should have all possible values explicitly typed, including defaults.

    • ✅ Good example

      interface Props {
      	size: 'sm' | 'default' | 'lg';
      }
      type $$Props = Props;
      export const size: $$Props['size'] = 'default';
    • ❌ Bad example

      interface Props {
      	size: 'sm' | 'lg';
      }
      type $$Props = Props;
      export const size: $$Props['size'] = undefined;
  • Props that accept text SHOULD include a Text suffix. Example: helperText. Such props MAY have a corresponding slot by the same name.

References:

Slots

  • All props that accept copy SHOULD have named slots with the same name as their respective prop provided, to allow formatted text or icons to be added.

    • If a prop and named slot is provided, the slot has priority and should render instead

    • Example:

      <script>
      	export let helperText;
      </script>
      
      <div>
      	A gift for you.
      	<slot name="helperText">
      		{#if helperText}
      		<span>{helperText}</span>
      		{/if}
      	</slot>
      </div>

Events

  • Forward useful native events such as on:click.

  • Prefer pointer events over mouse events to be touch-friendly.

  • Avoid createEventDispatcher since it will have obsolete utility in Svelte 5 thanks to event handlers, and providing callback functions is more ergonomic.

    Button.svelte

    <script>
    	export let ondoubleclick = () => {};
    
    	let lastClick = Date.now();
    
    	function checkDoubleClick() {
    		if (Date.now() - lastClick < 500) {
    			ondoubleclick();
    		} else {
    			lastClick = Date.now();
    		}
    	}
    </script>
    
    <button on:click on:click="{checkDoubleClick}">hi</button>

    +page.svelte

    <script>
    	import Button from './Button.svelte';
    
    	function handleDoubleClick() {
    		console.log('we did it');
    	}
    </script>
    
    <button ondoubleclick="{handleDoubleClick}">That's neat</button>

Respect a11y warnings

  • a11y warnings SHOULD NOT be ignored as they often hint at a real issue.
  • Among rare exceptions, these warnings break down when you’ve provided correct attributes to an element that wraps a <slot />, whose contents fulfill the accessible experience. If this case applies, then those warnings MAY be ignored.

Handle user events, rather than depend on reactivity

Features such as reactive statements ($:) are helpful to derive data to be reused elsewhere. If that data comes from a user’s input, prefer using input events instead as they are much easier to debug and maintain.

<script>
  let value
</script>

-<input bind:value />
+<input on:change={e => value = e.target.value} />

If the derived or bound value relates to a form submission, use form actions instead. In React, this is known as “uncontrolled components” and it’s more suitable for progressive enhancement.

Prepare for Svelte 5

We write our applications in Svelte 4. When Svelte 5 launches, it will feature a refactoring tool, but it will not capture all cases. To prepare for its arrival, here are some recommendations:

  • Do not use CSS Nesting in .svelte files; Svelte 5 CSS will have new scoping behaviours for nested CSS.

  • Avoid createEventDispatcher, use callback props instead such as export let onchange that accepts a function.

    <!-- Component.svelte -->
    <script>
    	export let onchange;
    </script>
    
    <input type="checkbox" on:change="{onchange}" />
    
    <!-- +page.svelte -->
    <script>
    	import Component from './Component.svelte';
    
    	function onchange() {}
    </script>
    
    <Component {onchange} />

    Which may be refactored in Svelte 5 as:

    <!-- Component.svelte -->
    <script>
      export let onchange
    </script>
    
    -<input type="checkbox" on:change={onchange} />
    +<input type="checkbox" {onchange} />

    With this in mind, all your component consumers can remain the same, and only the component needs refactoring.

  • Do not bind to instance properties in an {#each} loop. Svelte 5 will treat instance properties as hard copies and Svelte 4’s “call by reference” behaviour will not work.

    <script>
      import SomeComponent from './SomeComponent.svelte'
      let myArr = [1, 2, 3]
    </script>
    
    - {#each myArr as item}
    + {#each myArr as item, i}
    -   <SomeComponent bind:item />
    +   <SomeComponent bind:item={myArr[i]} />
      {/each}

SvelteKit best practices

  • Account for the no-JS user. On slow connections, it takes time for users to see the page and have it be interactive upon hydration. Always provide interactions that can work in plain HTML until the page hydrates, then provide an enhanced experience. This means:
    • Use form actions
    • Consider regular hyperlinks, and GET endpoints for file downloads
    • Use server-side redirects instead of goto()
  • Don’t abstract components unless it’s necessary. It’s okay to have large +page.svelte files, the “Don’t Repeat Yourself” methodology can work against you and lead to complicated views that span multiple files, making it troublesome to maintain.
  • Avoid shared state on the server, This includes imported stores or other globals. Doing this could leak information between users/requests. If you have request-specific data, use event.locals and load functions as documented by sveltekit.

File names

  • .ts and .js files MUST use kebab-case, such as utils.js or field-options.js. This includes test files.
  • .svelte component files MUST use PascalCase, such as DataTable.svelte; exceptions are SvelteKit route files.

Variable names

  • Functional cookies SHOULD be written in snake_case and have consistent categorical prefixes. Examples: auth_redirect_to, auth_session_id, form_data_new_campaign.
  • Function names SHOULD use descriptive verbs and avoid abbreviations and other shorthand:
    • handleClick(), saveProfile()
    • m(), vnForceAccept()

References:

User authentication and authorization

Users are authenticated in hooks.server.js at every page request. From this centralized location, we can redirect invalid sessions to the login page.

In a given route’s +page.server.js, you may authorize the user for the given page’s data if there are applicable permissions to account for not handled by any upstream APIs. If a user isn’t authorized, or an API responds with a 403 response, then the page MAY return a 403 error.

Enhanced form actions

Part of our accessibility pledge is to provide operable and resilient experiences over a spectrum of user device capabilities. SSR and Form actions helps achieve this vision:

  • SSR pages have a fast first contentful paint
  • <form> elements can be submitted without JavaScript

Some key recommendations to keep in mind:

  • Make use of export let form or $page.form to server-render responses from a form submission, and use server-side redirects where applicable.
  • Modify the use:enhance function to display loading state while a form submission processes. This enhanced experience will be visible to users who are on a hydrated page.

Avoid client-side fetch or writing API endpoints

Client-side fetch requires JavaScript to function. In a progressively-enhanced app, it’s important to provide a minimal experience until the page hydrates.

Alternatives to client-side fetch include:

  • Modify a page’s load to accept query parameters to fetch different data sets, which is useful for paginated content.
  • Use <a> links that target a page or GET endpoint.
  • Use form actions.

It is acceptable to perform client-side fetch for situations that may require data polling or listening to server-sent events. Be sure to provide a manual ‘refresh’ button as part of the UI.

Conditional streaming with promises

In general, you may leverage SvelteKit’s streaming with promises feature when a given page’s API data consistently takes longer than 600ms to respond. If data is fast, then prefer server-rendered pages.

In SvelteKit server-side events such as load, a RequestEvent object will provide the isDataRequest property that can be used to identify when enhanced navigation or enhanced form action requests are made by the user’s browser. With this flag, you can determine whether to stream the response or fully server-render the response.

For example:

// +page.server.js

import client from 'api-library';

export async function load({ isDataRequest }) {
	const response = client.profiles.get();

	return {
		profiles: isDataRequest ? response : await response
	};
}

With this feature, you may conditionally display skeleton loaders so that the user has immediate feedback on the state of the page:

<script>
	export let data;
</script>

{#await data.profiles} Loading, showing skeleton components... {:then profiles}
<!-- show profiles -->
{/await}

More reading: https://geoffrich.net/posts/conditionally-stream-data/.

References