Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Thu, 15 Aug 2024 01:15:38 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 225069128 Fine-Grained Reactivity in Svelte 5 https://frontendmasters.com/blog/fine-grained-reactivity-in-svelte-5/ https://frontendmasters.com/blog/fine-grained-reactivity-in-svelte-5/#respond Wed, 14 Aug 2024 15:05:23 +0000 https://frontendmasters.com/blog/?p=3438 We’ve been looking at the up and coming Svelte 5. We looked at basic features like state, props, and side effects. Then we looked at Snippets, which is a lightweight feature Svelte added for re-using bits of HTML within (for now) a single component.

Article Series

In this post, we’ll take a close look at Svelte’s new fine-grained reactivity.

What is fine-grained reactivity?

The best way to describe fine-grained reactivity is to show what it isn’t, and the best example of non-fine grained reactivity is React. In React, in any component, setting a single piece of state will cause the entire component, and all of the descendent components to re-render (unless they’re created with React.memo). Even if the state you’re setting is rendered in a single, simple <span> tag in the component, and not used anywhere else at all, the entire world from that component on down will be re-rendered.

This may seem absurdly wasteful, but in reality this is a consequence of the design features that made React popular when it was new: the data, values, callbacks, etc., that we pass through our component trees are all plain JavaScript. We pass plain, vanilla JavaScript objects, arrays and functions around our components and everything just works. At the time, this made an incredibly compelling case for React compared to alternatives like Angular 1 and Knockout. But since then, alternatives like Svelte have closed the gap. My first post on Svelte 5 showed just how simple, flexible, and most importantly reliable Svlete’s new state management primitives are. This post will show you the performance wins these primitives buy us.

Premature optimization is still bad

This post will walk through some Svelte templates using trickery to snoop on just how much of a component is being re-rendered when we change state. This is not something you will usually do or care about. As always, write clear, understandable code, then optimize when needed (not before). Svelte 4 is considerably less efficient than Svelte 5, but still much more performant than what React does out of the box. And React is more than fast enough for the overwhelming majority of use cases — so it’s all relative.

Being fast enough doesn’t mean we can’t still look at how much of a better performance baseline Svelte now starts you off at. With a fast-growing ecosystem, and now an incredibly compelling performance story, hopefully this post will encourage you to at least look at Svelte for your next project.

If you’d like to try out the code we’ll be looking at in this post, it’s all in this repo.

Getting started

The code we’ll be looking at is from a SvelteKit scaffolded project. If you’ve never used SvelteKit before that’s totally fine. We’re not really using any SvelteKit features until the very end of this post, and even then it’s just re-hashing what we’ll have already covered.

Throughout this post, we’re going to be inspecting if and when individual bindings in a component are re-evaluated when we change state. There’s various ways to do this, but the simplest, and frankly dumbest, is to force some global, non-reactive, always-changing state into these bindings. What do I mean by that? In the root page that hosts our site, I’m adding this:

<script>
  var __i = 0;
  var getCounter = () => __i++;
</script>

This adds a global getCounter function, as well as the __i variable. getCounter will always return the next value, and if we stick a call to it in our bindings, we’ll be able to snoop on when those bindings are being re-executed by Svelte. If you’re using TypeScript, you can avoid errors when calling this like so:

declare global {
  interface Window {
    getCounter(): number;
  }
}

export {};

This post will look at different pages binding to the same data, declared mostly like this (we’ll note differences as we go).

let tasks = [
  { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
  { id: 2, title: "Task B", assigned: "Adam", importance: "Medium" },
  { id: 3, title: "Task C", assigned: "Adam", importance: "High" },
  { id: 4, title: "Task D", assigned: "Mike", importance: "Medium" },
  { id: 5, title: "Task E", assigned: "Adam", importance: "High" },
  { id: 6, title: "Task F", assigned: "Adam", importance: "High" },
  { id: 7, title: "Task G", assigned: "Steve", importance: "Low" },
  { id: 8, title: "Task H", assigned: "Adam", importance: "High" },
  { id: 9, title: "Task I", assigned: "Adam", importance: "Low" },
  { id: 10, title: "Task J", assigned: "Mark", importance: "High" },
  { id: 11, title: "Task K", assigned: "Adam", importance: "Medium" },
  { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
];

And we’ll render these tasks with this markup:

<div>
  {#each tasks as t}
    <div>
      <div>
        <span>{t.id + getCounter()}</span>
        <button onclick={() => (t.id += 10)} class="border p-2">Update id</button>
     </div>
     <div>
       <span>{t.title + getCounter()}</span>
       <button onclick={() => (t.title += 'X')} class="border p-2">Update title</button>
     </div>
     <div>
        <span>{t.assigned + getCounter()}</span>
        <button onclick={() => (t.assigned += 'X')} class="border p-2">Update assigned</button>
      </div>
      <div>
        <span>{t.importance + getCounter()}</span>
        <button onclick={() => (t.importance += 'X')} class="border p-2">Update importance</button>
      </div>
    </div>
  {/each}
</div>

The Svelte 4 code we’ll start with uses the on:click syntax for events, but everything else will be the same.

The calls to getCounter inside the bindings will let us see when those bindings are re-executed, since the call to getCounter() will always return a new value.

Let’s get started!

Svelte 4

We’ll render the content we saw above, using Svelte 4.

Plain and simple. But now let’s click any of those buttons, to modify one property, of one of those tasks—it doesn’t matter which.

Notice that the entire component (every binding in the component) re-rendered. As inefficient as this seems, it’s still much better than what React does. It’s not remotely uncommon for a single state update to trigger multiple re-renders of many components.

Let’s see how Svelte 5 improves things.

Svelte 5

For Svelte 5, the code is pretty much the same, except we declare our state like this:

let tasks = $state([
  { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
  <em>// and so on ...</em>
  { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
]);

We render the page, and see the same as before. If you’re following along in the repo, be sure to refresh the page after navigating, so the page will start over with the global counter.

Now let’s change one piece of state, as before. We’ll update the title for Task C, the third one.

Just like that, only the single piece of state we modified has re-rendered. Svelte was smart enough to leave everything else alone. 99% of the time this won’t make any difference, but if you’re rendering a lot of data on a page, this can be a substantial performance win.

Why did this happen?

This is the default behavior when we pass arrays and objects (and arrays of objects) into the $state rune, like we did with:

let tasks = $state([

Svelte will read everything you pass, set up Proxy objects to track what changes, and update the absolute minimum amount of DOM nodes necessary.

False Coda

We could end the post here. Use the $state primitive to track your reactive data. Svelte will make it deeply reactive, and update whatever it needs to update when you change anything. This will be just fine the vast majority of the time.

But what if you’re writing a web application that has to manage a ton of data? Making everything deeply reactive is not without cost.

Let’s see how we can tell Svelte that only some of our data is reactive. I’ll stress again, laboring over this will almost never be needed. But it’s good to know how it works if it ever comes up.

Rediscovering a long-lost JavaScript feature

Classes in JavaScript have gotten an unfortunately bad reputation. Classes are an outstanding way to declare the structure of a set of objects, which also happen to come with a built-in factory function for creating those objects. Not only that, but TypeScript is deeply integrated with them.

You can declare:

class Person {
  firstName: string;
  lastName: string;

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

Not only will this provide you a factory function for creating instances of a Person, via new Person('Adam', 'Rackis'), but Person can also be used as a type within TypeScript. You can create variables or function parameters of type Person. It’s one of the few things that exist as a runtime construct and also a TypeScript type.

That said, if you find yourself reaching for extends in order to create deep inheritance hierarchies with classes, please please re-think your decisions.

Anyway, why am I bringing up classes in this post?

Fine-grained reactivity in Svelte 5

If you have a performance-sensitive section of code where you need to mark some properties as non-reactive, you can do this by creating class instances rather than vanilla JavaScript objects. Let’s define a Task class for our tasks. For the properties we want to be reactive, we’ll set default values with the $state() rune. For properties we don’t want to be reactive, we won’t.

class Task {
  id: number = 0;
  title = $state("");
  assigned = $state("");
  importance = $state("");

  constructor(data: Task) {
    Object.assign(this, data);
  }
}

And then we just use that class

let tasks = $state([
  new Task({ id: 1, title: "Task A", assigned: "Adam", importance: "Low" }),
  <em>// and so on</em>
  new Task({ id: 12, title: "Task L", assigned: "Adam", importance: "High" }),
]);

I simplified the class a bit by taking a raw object with all the properties of the class, and assigning those properties with Object.assign. The object literal is typed in the constructor as Task, the same as the class, but that’s fine because of TypeScript’s structural typing.

When we run that, we’ll see the same exact thing as before, except clicking the button to change the id will not re-render anything at all in our Svelte component. To be clear, the id is still changing, but Svelte is not re-rendering. This demonstrates Svelte intelligently not wiring any kind of observability into that particular property.

Side note: if you wanted to encapsulate / protect the id, you could declare id as #id to make it a private property and then expose the value with a getter function.

Going deeper

What if you don’t want these tasks to be reactive at the individual property at all? What if we have a lot of these tasks coming down, and you’re not going to be editing them? So rather than have Svelte set up reactivity for each of the tasks’ properties, you just want the array itself to be reactive.

You basically want to be able to add or remove entries in your array, and have Svelte update the tasks that are rendered. But you don’t want Svelte setting up any kind of reactivity for each property on each task.

This is a common enough use case that other state management systems support this directly, for example, MobX’s observable.shallow. Unfortunately Svelte does not have any such helper, as of yet. That said, it is currently being debated, so keep your eyes open for a $state.shallow() that would do what we’re about to show. But even if it does get added, implementing it ourselves will be a great way to kick the tires of Svelte’s new reactivity system. Let’s see how.

Implementing our own $state.shallow() equivalent

We already saw how passing class instances to an array shut off fine-grained reactivity by default, leaving you to opt-in, as desired, by setting class fields to $state(). But our data are likely coming from a database, as plain (hopefully typed) JavaScript objects, unrelated to any class; more importantly we likely have zero desire to cobble together a class just for this.

So let’s simulate it. Let’s say that a database is providing our Task objects as JS objects. We (of course) have a type for this:

type Task = {
  id: number;
  title: string;
  assigned: string;
  importance: string;
};

We want to put those instances into an array that itself is reactive, but not the individual properties on the tasks. With a tiny bit of cleverness we can make it mostly painless.

class NonReactiveObjectGenerator {
  constructor(data: unknown) {
    Object.assign(this, data);
  }
}

function shallowObservable<T>(data: T[]): T[] {
  let result = $state(data.map(t => new NonReactiveObjectGenerator(t) as T));
  return result;
}

Our NonReactiveObjectGenerator class takes in any object, and then smears all that object’s properties onto itself. And our shallowObservable takes an array of whatever, and maps it onto instances of our NonReactiveObjectGenerator class. This will force each instance to be a class instance, with nothing reactive. The as T is us forcing TypeScript to treat these new instances as whatever type was passed in. This is accurate, but something TypeScript needs help understanding, since it’s not (as of now) able to read and understand our call to Object.assign in the class constructor.

If you closely read my first post on Svelte 5, you might recall that you can’t directly return reactive state from a function, since the state will be read and unwrapped right at the call-site, and won’t be reactive any longer. Normally you’d have to do this:

return {
  get value() {
    return result;
  },
  set value(newData: T[]) {
    result = newData;
  },
};

Why wasn’t that needed here? It’s true, the $state() value will be read at the function’s call site. So with…

let tasks = shallowObservable(getTasks());

…the tasks variable will not be reactive. But the array itself will still be fully reactive. We can still call push, pop, splice and so on. If you can live without needing to re-assign to the variable, this is much simpler. But even if you do need to set the tasks variable to a fresh array of values, you still don’t even need to use variable assignment. Stay tuned.

I changed the initial tasks array to help out in a minute, but the rest is what you’d expect.

const getTasks = () => [
  { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
  <em>// ...</em>
  { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
];

let tasks = shallowObservable(getTasks());

And with that, rendering should now work, and none of our properties are reactive. Clicking the edit buttons do nothing.

But we can now add a button to push a new task onto our array.

<button
  onclick={() =>
    tasks.value.push(
      new NonReactiveObjectGenerator({
        id: nextId++,
        title: 'New task',
        assigned: 'Adam',
        importance: 'Low'
      }) as Task
    )}
>
  Add new task
</button>

We can even add a delete button to each row.

<button onclick={() => tasks.value.splice(idx, 1)}>
  Delete
</button>

Yes, Svelte’s reactive array is smart enough to understand push and splice.

Editing tasks this way

You might be wondering if we can still actually edit the individual tasks. We assumed the tasks would be read-only, but what if that changes? We’ve been modifying the array and watching Svelte re-render correctly. Can’t we edit an individual task by just cloning the task, updating it, and then re-assigning to that index? The answer is yes, with a tiny caveat.

Overriding an array index (with a new object instance) does work, and makes Svelte update. But we can’t just do this:

tasks[idx] = { ...t, importance: "X" + t };

Since that would make the new object, which is an object literal, deeply reactive. We have to keep using our class. This time, to keep the typings simple, and to keep the code smell that is the NonReactiveObjectGenerator class hidden as much as possible, I wrote up a helper function.

function cloneNonReactive<T>(data: T): T {
  return new NonReactiveObjectGenerator(data) as T;
}

As before, the type assertion is unfortunately needed. This same function could also be used for the add function we saw above, if you prefer.

To prove editing works, we’ll leave the entire template alone, except for the importance field, which we’ll modify like so

  <div>
    <span>{t.importance + getCounter()}</span>
    <button
      onclick={() => {
        const taskClone = cloneNonReactive(t);
        taskClone.importance += 'X';
        tasks[idx] = cloneNonReactive(taskClone);
      }}
    >
      Update importance
    </button>
</div>

Now running shows everything as it’s always been.

If we click the button to change the id, title or assigned value, nothing changes, because we’re still mutating those properties directly (since I didn’t change anything) in order to demonstrate that they’re not reactive. But clicking the button to update the importance field runs the code above, and updates the entire row, showing any other changes we’ve made.

Here I clicked the button to update the title, twice, and then clicked the button to update the importance. The former did nothing, but the latter updated the component to show all changes.

Re-assigning to the tasks array

We saved a bit of convenience by returning our state value directly from our shallowObservable helper, but at the expense of not being able to assign directly to our array. Or did we?

If you know a bit of JavaScript, you might know…

tasks.length = 0;

…is the old school way to clear an array. That works with Svelte; the Proxy object Svelte sets up to make our array observable works with that. Similarly, we can set the array to a fully new array of values (after clearing it like we just saw) like this:

tasks.push(...newArray);

It’s up to you which approach you take, but hopefully Svelte ships a $state.shallow to provide the best of both worlds: the array would be reactive, and so would the binding, since we don’t have to pass it across a function boundary; it would be built directly into $state.

SvelteKit

Let’s wrap up by briefly talking about how data from SvelteKit loaders is treated in terms of reactivity. In short, it’s exactly how you’d expect. First and foremost, if you return a raw array of objects from your loader like this:

export const load = () => {
  return {
    tasks: [
      { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
      <em>// ...</em>
      { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
    ],
  };
};

Then none of that data will be reactive in your component. This is to be expected. To make data reactive, you need to wrap it in $state(). As of now, you can’t call $state in a loader, only in a universal Svelte file (something that ends in .svelte.ts). Hopefully in the future Svelte will allow us to have loaders named +page.svelte.ts but for now we can throw something like this in a reactive-utils.svelte.ts file.

export const makeReactive = <T>(arg: T[]): T[] => {
  let result = $state(arg);
  return result;
};

Then import it and use it in our loader.

import { makeReactive } from "./reactive-utils.svelte";

export const load = () => {
  return {
    tasks: makeReactive([
      { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
      <em>// ...</em>
      { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
    ]),
  };
};

Now those objects will support the same fine-grained reactivity we saw before. To customize which properties are reactive, you’d swap in class instances, instead of vanilla object literals, again just like we saw. All the same rules apply.

If you’re wondering why we did this…

export const makeReactive = <T>(arg: T[]): T[] => {
  let result = $state(arg);
  return result;
};

…rather than this…

export const makeReactive = <T>(arg: T[]): T[] => {
  return $state(arg);
};

… the answer is that the latter is simply disallowed. Svelte forces you to only put $state() calls into assignments. It cannot appear as a return value like this. The reason is that while returning $state variables directly across a function boundary works fine for objects and arrays, doing this for primitive values (strings or numbers) would produce a senseless result. The variable could not be re-assigned (same as we saw with the array), but as a primitive, there’d be no other way to edit it. It would just be a non-reactive constant.

Svelte forcing you to take that extra step, and assign $state to a variable before returning, is intended to help prevent you from making that mistake.

Wrapping up

One of the most exciting features of Svelte 5 is the fine-grained reactivity it adds. Svelte was already lightweight, and faster than most, if not all of the alternatives. These additions in version 5 only improve on that. When added to the state management improvements we’ve already covered in prior posts, Svelte 5 really becomes a serious framework option.

Consider it for your next project.

Article Series

]]>
https://frontendmasters.com/blog/fine-grained-reactivity-in-svelte-5/feed/ 0 3438
Snippets in Svelte 5 https://frontendmasters.com/blog/snippets-in-svelte-5/ https://frontendmasters.com/blog/snippets-in-svelte-5/#respond Wed, 07 Aug 2024 13:03:50 +0000 https://frontendmasters.com/blog/?p=3341 This post is the second in a 3-part series on Svelte 5. Part one was a basic introduction, covering nuts and bolts features like state, props, and effects. This post is all about snippets, an exciting new feature that allows for content reuse, and more importantly, injecting content into components you render.

Article Series

If you’d like to see and experiment with the code in this post, see the GitHub repo.

What are snippets?

Snippets are a new feature in Svelte 5. They allow you to define, well… snippets of content. They’re almost lightweight components that you can find inside of a component file. Before you get too excited: they do not, as of now, allow you to define multiple components in one file. Snippets cannot be exported from anywhere, and even if they could, they do not allow you to define state. They are limited to props.

They seem initially similar to React’s minimally useful Stateless Functional Components from back before hooks were a thing. But snippets also have a second use: they allow you to inject content into other components, and in so doing replace one of Svelte’s most awkward features: slots.

Let’s see how.

Defining snippets

We define snippets with the #snippet directive. The simplest snippet imaginable looks like this:

{#snippet helloWorld()}
  <span>Hello World</span>
{/snippet}

That defines the snippet. To render the snippet, we use the @render directive, like this:

{@render helloWorld()}

As you might have guessed, snippets can also receive props, or really, parameters, since snippets are more of a function, than a component. Parameters are listed in the parens, with types if you’re using TypeScript.

{#snippet productDisplay(p: Product)}
<div>
  <img src="{p.url}" alt="product url" />
  <div>
    <h2>{p.name}</h2>
    <span>${p.price.toFixed(2)}</span>
  </div>
</div>
{/snippet}

Snippets can render other snippets

For example, this simple snippet…

{#snippet productReview(review: Review)}
<div>
  <span>{review.date}</span>
  <span>{review.content}</span>
</div>
{/snippet}

… can be used in this bigger snippet:

{#snippet productDisplay(p: Product)}
<div>
  <div>
    <img src="{p.url}" alt="product url">
    <div>
      <h2>{p.name}</h2>
      <span>${p.price.toFixed(2)}</span>
    </div>
  </div>
  <h3>Reviews:</h3>
  <div>
    {#each p.reviews ?? [] as review}
      {@render productReview(review)}
    {/each}
  </div>
</div>
{/snippet}

Then you can reuse that productDisplay snippet with different products in your component. Let’s see a minimal, full example:

<script lang="ts">
  type Review = {
    date: string;
    content: string;
  };
  type Product = {
    name: string;
    url: string;
    price: number;
    reviews?: Review[];
  };

  let searchedBook = $state<Product>({
    name: "Effective TypeScript: 83 Specific Ways to Improve Your TypeScript, 2nd Edition",
    url: "https://m.media-amazon.com/images/I/71eWL4AqPqL._SL1500_.jpg",
    price: 44.99,
    reviews: [
      { date: "2/14/2024", content: "Absolutely loved this book" },
      { date: "6/2/2024", content: "Even better than the first edition" },
    ],
  });
  let relatedProduct = $state<Product>({
    name: "Modern C++ Design: Generic Programming and Design Patterns Applied",
    url: "https://m.media-amazon.com/images/I/914ncVx1hxL._SL1413_.jpg",
    price: 55.49,
  });
</script>

{#snippet productReview(review: Review)}
<div>
  <span>{review.date}</span>
  <span>{review.content}</span>
</div>
{/snippet}

{#snippet productDisplay(p: Product)}
<div>
  <div>
    <img src="{p.url}" alt="product url" />
    <div>
      <h2>{p.name}</h2>
      <span>${p.price.toFixed(2)}</span>
    </div>
  </div>
  <h3>Reviews:</h3>
  <div>{#each p.reviews ?? [] as review} {@render productReview(review)} {/each}</div>
</div>
{/snippet}

<section>
  <h1>Product Display Page</h1>

  {@render productDisplay(searchedBook)}

  <aside>You might also be interested in:</aside>

  {@render productDisplay(relatedProduct)}
</section>

If that was the extent of Snippets they’d be a marginally useful convenience for re-using small bits of markup within a single component.

But the main benefit of snippets is for injecting content into components. Previously, if you wanted to pass content into a component you’d use slots. Slots were always an awkward feature of Svelte, but they’re now deprecated in Svelte 5. We won’t cover them here, so check out the docs if you’re curious.

Passing snippets to components

Snippets shine brightest when we pass them into other components. Let’s imagine a (grossly simplified) DisplayProduct page. It takes in a product, an optional related product, and a snippet to display a single product. This component will also render content in the header, which we’ll also pass in as a snippet.

<script lang="ts">
  import type { Snippet } from "svelte";
  import type { Product } from "./types";

  type Props = {
    product: Product;
    relatedProduct?: Product;
    productDisplay: Snippet<[Product]>;
    children: Snippet;
  };

  let { product, relatedProduct, productDisplay, children }: Props = $props();
</script>

<section>
  {@render children()}
  {@render productDisplay(product)}
  
  {#if relatedProduct}
    <aside>You might also be interested in:</aside>
    {@render productDisplay(relatedProduct)}
  {/if}
</section>

There’s a Snippet type that Svelte exports for us, so we can type the snippets we’re receiving. Specifying the parameters that a snippet receives is a little weird, because of how TypeScript is: we list the argumentes as a Tuple. So our productDisplay snippet will take a single argument that’s a Product.

The snippet for showing the header I decided to name “children” which has some significance as we’ll see in a moment.

Let’s put this component to use:

{#snippet productDisplay(p: Product)}
<div>
  <img src="{p.url}" alt="Image of product">
  <div>
    <h2>{p.name}</h2>
    <span>${p.price.toFixed(2)}</span>
  </div>
</div>
{/snippet}

<DisplayProduct product="{searchedBook}" relatedProduct="{recommendedBook}" {productDisplay}>
  <h1>Product Display Page</h1>
</DisplayProduct>

We’re passing the productDisplay snippet in for the productDisplay prop. Little note: Svelte allows you to write {a} instead of a={a} as a convenient shortcut.

But notice the content we put directly inside of the DisplayProduct tags. If the component has a prop called children that’s a snippet, this content will be passed as that snippet. This is a special case just for props called children (similar to the children prop in React). You don’t have to do this; you’re free to manually pass a children prop, just like we did for productDisplay if you really want to.

Let’s take a look at one more authoring convenience Svelte 5 gives us. If we’re just defining a snippet to be passed one time, to one component, Svelte lets us clean the syntax up a bit, like so:

<DisplayProduct product="{searchedBook}" relatedProduct="{recommendedBook}">
  <h1>Product Display Page</h1>
  {#snippet productDisplay(p: Product)}
  <div>
    <img src="{p.url}" alt="product url" />
    <div>
      <h2>{p.name}</h2>
      <span>${p.price.toFixed(2)}</span>
    </div>
  </div>
  {/snippet}
</DisplayProduct>

As before, we have our <h1> content directly inside of the tags, as children. But we’ve also defined a snippet inside of those tags. This is a nice shorthand for passing a snippet as a prop (with the same name) to our component. Don’t worry, if the name you give this inline snippet doesn’t match a prop, TypeScript will tell you.

Default Content with Snippets

One nice feature with slots is that you could define default content pretty easily.

<slot name="header-content">
  <span>Default content</span>
</slot>

Snippets don’t quite have anything like this built in, but they’re a flexible enough primitive that you really don’t need it.

Let’s see how we can provide our own default content for when a Snippet is not passed in. As before let’s say we have our DisplayProduct component, except now our productDisplay and children snippets are optional

type Props = {
  product: Product;
  relatedProduct?: Product;
  productDisplay?: Snippet<[Product]>;
  children?: Snippet;
};

let { product, relatedProduct, productDisplay, children }: Props = $props();

We have a few straightforward options for falling back to our own default content. We can simply test if we have a value for the snippet right in our template, and render the fallback if not.

{#if children}
  {@render children()} 
{:else}
  <h1>Fallback content</h1>
{/if}

Or, we can set up our fallback right in our script:

let productDisplaySnippetToUse: Snippet<[Product]> = productDisplay ?? productDisplayFallback;
{#snippet productDisplayFallback(p: Product)}
<div>
  <img src="{p.url}" alt="product url" />
  <div>
    <h2>{p.name}</h2>
  </div>
</div>
{/snippet}

Then we render that:

{@render productDisplaySnippetToUse(product)}

Parting thoughts

Svelte 5 is an exciting release. This post turned to one of the more interesting new features: snippets, useful for injecting content into components, and for re-using small bits of content within a single component.

Out with slots, in with snippets.

Article Series

]]>
https://frontendmasters.com/blog/snippets-in-svelte-5/feed/ 0 3341
Introducing Svelte 5 https://frontendmasters.com/blog/introducing-svelte-5/ https://frontendmasters.com/blog/introducing-svelte-5/#comments Fri, 19 Jul 2024 18:40:23 +0000 https://frontendmasters.com/blog/?p=3067 Svelte has always been a delightful, simple, and fun framework to use. It’s a framework that’s always prioritized developer experience (DX), while producing a light and fast result with minimal JavaScript. It achieves this nice DX by giving users dirt simple idioms and a required compiler that makes everything work. Unfortunately, it used to be fairly easy to break Svelte’s reactivity. It doesn’t matter how fast a website is if it’s broken.

These reliability problems with reactivity are gone in Svelte 5.

In this post, we’ll get into the exciting Svelte 5 release (in Beta at the time of this writing). Svelte is the latest framework to add signals to power their reactivity. Svelte is now every bit as capable of handling robust web applications, with complex state, as alternatives like React and Solid. Best of all, it achieved this with only minimal hits to DX. It’s every bit as fun and easy to use as it was, but it’s now truly reliable, while still producing faster and lighter sites.

Article Series

Let’s jump in!

The Plan

Let’s go through various pieces of Svelte, look at the “old” way, and then see how Svelte 5 changes things for the better. We’ll cover:

  1. State
  2. Props
  3. Effects

If you find this helpful, let me know, as I’d love to cover snippets and Svelte’s exciting new fine-grained reactivity.

As of this writing, Svelte 5 is late in the Beta phase. The API should be stable, although it’s certainly possible some new things might get added.

The docs are also still in beta, so here’s a preview URL for them. Svelte 5 might be released when you read this, at which point these docs will be on the main Svelte page. If you’d like to see the code samples below in action, you can find them in this repo.

State

Effectively managing state is probably the most crucial task for any web framework, so let’s start there.

State used to be declared with regular, plain old variable declarations, using let.

let value = 0;

Derived state was declared with a quirky, but technically valid JavaScript syntax of $:. For example:

let value = 0;
$: doubleValue = value * 2;

Svelte’s compiler would (in theory) track changes to value, and update doubleValue accordingly. I say in theory since, depending on how creatively you used value, some of the re-assignments might not make it to all of the derived state that used it.

You could also put entire code blocks after $: and run arbitrary code. Svelte would look at what you were referencing inside the code block, and re-run it when those things changed.

$: {
  console.log("Value is ", value);
}

Stores

Those variable declarations, and the special $: syntax was limited to Svelte components. If you wanted to build some portable state you could define anywhere, and pass around, you’d use a store.

We won’t go through the whole API, but here’s a minimal example of a store in action. We’ll define a piece of state that holds a number, and, based on what that number is at anytime, spit out a label indicating whether the number is even or odd. It’s silly, but it should show us how stores work.

import { derived, writable } from "svelte/store";

export function createNumberInfo(initialValue: number = 0) {
  const value = writable(initialValue);

  const derivedInfo = derived(value, value => {
    return {
      value,
      label: value % 2 ? "Odd number" : "Even number",
    };
  });

  return {
    update(newValue: number) {
      value.set(newValue);
    },
    numberInfo: derivedInfo,
  };
}

Writable stores exist to write values to. Derived stores take one or more other stores, read their current values, and project a new payload. If you want to provide a mechanism to set a new value, close over what you need to. To consume a store’s value, prefix it with a $ in a Svelte component. It’s not shown here, but there’s also a subscribe method on stores, and a get import. If the store returns an object with properties, you can either “dot through” to them, or you can use a reactive assignment ($:) to get those nested values. The example below shows both, and this distinction will come up later when we talk about interoperability between Svelte 4 and 5.

<script lang="ts">
  import { createNumberInfo } from './numberInfoStore';

  let store = createNumberInfo(0);

  $: ({ numberInfo, update } = store);
  $: ({ label, value } = $numberInfo);
</script>

<div class="flex flex-col gap-2 p-5">
  <span>{$numberInfo.value}</span>
  <span>{$numberInfo.label}</span>
  <hr />
  <span>{value}</span>
  <span>{label}</span>

  <button onclick={() => update($numberInfo.value + 1)}>
    Increment count
  </button>
</div>

This was the old Svelte.

This is a post on the new Svelte, so let’s turn our attention there.

State in Svelte 5

Things are substantially simpler in Svelte 5. Pretty much everything is managed by something new called “runes.” Let’s see what that means.

Runes

Svelte 5 joins the increasing number of JavaScript frameworks that use the concept of signals. There is a new feature called runes and under the covers they use signals. These accomplish a wide range of features from state to props and even side effects. Here’s a good introduction to runes.

To create a piece of state, we use the $state rune. You don’t import it, you just use it — it’s part of the Svelte language.

let count = $state(0);

For values with non-inferable types, you can provide a generic

let currentUser = $state<User | null>(null);

What if you want to create some derived state? Before we did:

$: countTimes2 = count * 2;

In Svelte 5 we use the $derived rune.

let countTimes2 = $derived(count * 2);

Note that we pass in a raw expression. Svelte will run it, see what it depends on, and re-run it as needed. There’s also a $derived.by rune if you want to pass an actual function.

If you want to use these state values in a Svelte template, you just use them. No need for special $ syntax to prefix the runes like we did with stores. You reference the values in your templates, and they update as needed.

If you want to update a state value, you assign to it:

count = count + 1;
// or count++;

What about stores?

We saw before that defining portable state outside of components was accomplished via stores. Stores are not deprecated in Svelte 5, but there’s a good chance they’re on their way out of the framework. You no longer need them, and they’re replaced with what we’ve already seen. That’s right, the $state and $derived runes we saw before can be defined outside of components in top-level TypeScript (or JavaScript) files. Just be sure to name your file with a .svelte.ts extension, so the Svelte compiler knows to enable runes in these files. Let’s take a look!

Let’s re-implement our number / label code from before, in Svelte 5. This is what it looked like with stores:

import { derived, writable } from "svelte/store";

export function createNumberInfo(initialValue: number = 0) {
  const value = writable(initialValue);

  const derivedInfo = derived(value, value => {
    return {
      value,
      label: value % 2 ? "Odd number" : "Even number",
    };
  });

  return {
    update(newValue: number) {
      value.set(newValue);
    },
    numberInfo: derivedInfo,
  };
}

Here it is with runes:

export function createNumberInfo(initialValue: number = 0) {
  let value = $state(initialValue);
  let label = $derived(value % 2 ? "Odd number" : "Even number");

  return {
    update(newValue: number) {
      value = newValue;
    },
    get value() {
      return value;
    },
    get label() {
      return label;
    },
  };
}

It’s 3 lines shorter, but more importantly, much simpler. We declared our state. We computed our derived state. And we send them both back, along with a method that updates our state.

You may be wondering why we did this:

  get value() {
    return value;
  },
  get label() {
    return label;
  }

rather than just referencing those properties. The reason is that reading that state, at any given point in time, evaluates the state rune, and, if we’re reading it in a reactive context (like a Svelte component binding, or inside of a $derived expression), then a subscription is set up to update any time that piece of state is updated. If we had done it like this:

// this won't work
return {
  update(newValue: number) {
    value = newValue;
  },
  value,
  label,
};

That wouldn’t have worked because those value and label pieces of state would be read and evaluated right there in the return value, with those raw values getting injected into that object. They would not be reactive, and they would never update.

That’s about it! Svelte 5 ships a few universal state primitives which can be used outside of components and easily constructed into larger reactive structures. What’s especially exciting is that Svelte’s component bindings are also updated, and now support fine-grained reactivity that didn’t used to exist.

Props

Defining state inside of a component isn’t too useful if you can’t pass it on to other components as props. Props are also reworked in Svelte 5 in a way that makes them simpler, and also, as we’ll see, includes a nice trick to make TypeScript integration more powerful.

Svelte 4 props were another example of hijacking existing JavaScript syntax to do something unrelated. To declare a prop on a component, you’d use the export keyword. It was weird, but it worked.

// ChildComponent.svelte
<script lang="ts">
  export let name: string;
  export let age: number;
  export let currentValue: string;
</script>

<div class="flex flex-col gap-2">
  {name} {age}
  <input bind:value={currentValue} />
</div>

This component created three props. It also bound the currentValue prop into the <input>, so it would change as the user typed. Then to render this component, we’d do something like this:

<script lang="ts">
  import ChildComponent from "./ChildComponent.svelte";

  let currentValue = "";
</script>

Current value in parent: {currentValue}
<ChildComponent name="Bob" age={20} bind:currentValue />

This is Svelte 4, so let currentValue = '' is a piece of state that can change. We pass props for name and age, but we also have bind:currentValue which is a shorthand for bind:currentValue={currentValue}. This creates a two-way binding. As the child changes the value of this prop, it propagates the change upward, to the parent. This is a very cool feature of Svelte, but it’s also easy to misuse, so exercise caution.

If we type in the ChildComponent’s <input>, we’ll see currentValue update in the parent component.

Svelte 5 version

Let’s see what these props look like in Svelte 5.

<script lang="ts">
  type Props = {
    name: string;
    age: number;
    currentValue: string;
  };

  let { age, name, currentValue = $bindable() }: Props = $props();
</script>

<div class="flex flex-col gap-2">
  {name} {age}
  <input bind:value={currentValue} />
</div>

The props are defined via the $props rune, from which we destructure the individual values.

let { age, name, currentValue = $bindable() }: Props = $props();

We can apply typings directly to the destructuring expression. In order to indicate that a prop can be (but doesn’t have to be) bound to the parent, like we saw above, we use the $bindable rune, like this

 = $bindable()

If you want to provide a default value, assign it to the destructured value. To assign a default value to a bindable prop, pass that value to the $bindable rune.

let { age = 10, name = "foo", currentValue = $bindable("bar") }: Props = $props();

But wait, there’s more!

One of the most exciting changes to Svelte’s prop handling is the improved TypeScript integration. We saw that you can assign types, above. But what if we want to do something like this (in React)

type Props<T> = {
  items: T[];
  onSelect: (item: T) => void;
};
export const AutoComplete = <T,>(props: Props<T>) => {
  return null;
};

We want a React component that receives an array of items, as well as a callback that takes a single item (of the same type). This works in React. How would we do it in Svelte?

At first, it looks easy.

<script lang="ts">
  type Props<T> = {
    items: T[];
    onSelect: (item: T) => void;
  };

  let { items, onSelect }: Props<T> = $props();
  //         Error here _________^
</script>

The first T is a generic parameter, which is defined as part of the Props type. This is fine. The problem is, we need to instantiate that generic type with an actual value for T when we attempt to use it in the destructuring. The T that I used there is undefined. It doesn’t exist. TypeScript has no idea what that T is because it hasn’t been defined.

What changed?

Why did this work so easily with React? The reason is, React components are functions. You can define a generic function, and when you call it TypeScript will infer (if it can) the values of its generic types. It does this by looking at the arguments you pass to the function. With React, rendering a component is conceptually the same as calling it, so TypeScript is able to look at the various props you pass, and infer the generic types as needed.

Svelte components are not functions. They’re a proprietary bit of code thrown into a .svelte file that the Svelte compiler turns into something useful. We do still render Svelte components, and TypeScript could easily look at the props we pass, and infer back the generic types as needed. The root of the problem, though, is that we haven’t (yet) declared any generic types that are associated with the component itself. With React components, these are the same generic types we declare for any function. What do we do for Svelte?

Fortunately, the Svelte maintainers thought of this. You can declare generic types for the component itself with the generics attribute on the <script> tag at the top of your Svelte component:

<script lang="ts" generics="T">
  type Props<T> = {
    items: T[];
    onSelect: (item: T) => void;
  };

  let { items, onSelect }: Props<T> = $props();
</script>

You can even define constraints on your generic arg:

<script lang="ts" generics="T extends { name: string }">
  type Props<T> = {
    items: T[];
    onSelect: (item: T) => void;
  };

  let { items, onSelect }: Props<T> = $props();
</script>

TypeScript will enforce this. If you violate that constraint like this:

<script lang="ts">
  import AutoComplete from "./AutoComplete.svelte";

  let items = [{ name: "Adam" }, { name: "Rich" }];
  let onSelect = (item: { id: number }) => {
    console.log(item.id);
  };
</script>

<div>
  <AutoComplete {items} {onSelect} />
</div>

TypeScript will let you know:

Type '(item: { id: number; }) => void' is not assignable to type '(item: { name: string; }) => void'. Types of parameters 'item' and 'item' are incompatible.

Property 'id' is missing in type '{ name: string; }' but required in type '{ id: number; }'.

Effects

Let’s wrap up with something comparatively easy: side effects. As we saw before, briefly, in Svelte 4 you could run code for side effects inside of $: reactive blocks

$: {
  console.log(someStateValue1, someStateValue2);
}

That code would re-run when either of those values changed.

Svelte 5 introduces the $effect rune. This will run after state has changed, and been applied to the dom. It is for side effects. Things like resetting the scroll position after state changes. It is not for synchronizing state. If you’re using the $effect rune to synchronize state, you’re probably doing something wrong (the same goes for the useEffect hook in React).

The code is pretty anti-climactic.

$effect(() => {
  console.log("Current count is ", count);
});

When this code first starts, and anytime count changes, you’ll see this log. To make it more interesting, let’s pretend we have a current timestamp value that auto-updates:

let timestamp = $state(+new Date());
setInterval(() => {
  timestamp = +new Date();
}, 1000);

We want to include that value when we log, but we don’t want our effect to run whenever our timestamp changes; we only want it to run when count changes. Svelte provides an untrack utility for that

import { untrack } from "svelte";

$effect(() => {
  let timestampValue = untrack(() => timestamp);
  console.log("Current count is ", count, "at", timestampValue);
});

Interop

Massive upgrades where an entire app is updated to use a new framework version’s APIs are seldom feasible, so it should come as no surprise that Svelte 5 continues to support Svelte 4. You can upgrade your app incrementally. Svelte 5 components can render Svelte 4 components, and Svelte 4 components can render Svelte 5 components. The one thing you can’t do is mix and match within a single component. You cannot use reactive assignments $: in the same component that’s using Runes (the Svelte compiler will remind you if you forget).

Since stores are not yet deprecated, they can continue to be used in Svelte 5 components. Remember the createNumberInfo method from before, which returned an object with a store on it? We can use it in Svelte 5. This component is perfectly valid, and works.

<script lang="ts">
  import { createNumberInfo } from '../svelte4/numberInfoStore';

  const numberPacket = createNumberInfo(0);

  const store = numberPacket.numberInfo;
  let junk = $state('Hello');
</script>

<span>Run value: {junk}</span>
<div>Number value: {$store.value}</div>

<button onclick={() => numberPacket.update($store.value + 1)}>Update</button>

But the rule against reactive assignments still holds; we cannot use one to destructure values off of stores when we’re in Svelte 5 components. We have to “dot through” to nested properties with things like {$store.value} in the binding (which always works) rather than:

$: ({ value } = $store);

… which generates the error of:

$: is not allowed in runes mode, use $derived or $effect instead

The error is even clear enough to give you another alternative to inlining those nested properties, which is to create a $derived state:

let value = $derived($store.value);
// or let { value } = $derived($store);

Personally I’m not a huge fan of mixing the new $derived primitive with the old Svelte 4 syntax of $store, but that’s a matter of taste.

Parting thoughts

Svelte 5 has shipped some incredibly exciting changes. We covered the new, more reliable reactivity primitives, the improved prop management with tighter TypeScript integration, and the new side effect primitive. But we haven’t come closing to covering everything. Not only are there more variations on the $state rune, but Svelte 5 also updated it’s event handling mechanism, and even shipped an exciting new way to re-use “snippets” of HTML.

Svelte 5 is worth a serious look for your next project.

Article Series

]]>
https://frontendmasters.com/blog/introducing-svelte-5/feed/ 3 3067
Using Auth.js with SvelteKit https://frontendmasters.com/blog/using-nextauth-now-auth-js-with-sveltekit/ https://frontendmasters.com/blog/using-nextauth-now-auth-js-with-sveltekit/#respond Mon, 29 Apr 2024 15:22:25 +0000 https://frontendmasters.com/blog/?p=1764 SvelteKit is an exciting framework for shipping performant web applications with Svelte. I’ve previously written an introduction on it, as well as a deeper dive on data handling and caching.

In this post, we’ll see how to integrate Auth.js (Previously next-auth) into a SvelteKit app. It might seem surprising to hear that this works with SvelteKit, but this project has gotten popular enough that much of it has been split into a framework-agnostic package of @auth/core. The Auth.js name is actually a somewhat recent rebranding of NextAuth.

In this post we’ll cover the basic config for @auth/core: we’ll add a Google Provider and configure our sessions to persist in DynamoDB.

The code for everything is here in a GitHub repo, but you won’t be able to run it without setting up your own Google Application credentials, as well as a Dynamo table (which we’ll get into).

The initial setup

We’ll build the absolute minimum skeleton app needed to demonstrate authentication. We’ll have our root layout read whether the user is logged in, and show a link to content that’s limited to logged in users, and a log out button if so; or a log in button if not. We’ll also set up an auth check with redirect in the logged in content, in case the user browses directly to the logged in URL while logged out.

Let’s create a SvelteKit project if we don’t have one already, using the insutructions here. Chose “Skeleton Project” when prompted.

Now let’s install some packages we’ll be using:

npm i @auth/core @auth/sveltekit

Let’s create a top-level layout that will use our auth data. First, our server loader, in a file named +layout.server.ts. This will hold our logged-in state, which for now is always false.

export const load = async ({ locals }) => {
  return {
    loggedIn: false,
  };
};

Now let’s make the actual layout, in +layout.svelte with some basic markup

<script lang="ts">
  import type { PageData } from './$types';
  import { signIn, signOut } from '@auth/sveltekit/client';

  export let data: PageData;
  $: loggedIn = data.loggedIn;
</script>

<main>
  <h1>Hello there! This is the shared layout.</h1>

  {#if loggedIn}
    <div>Welcome!</div>
    <a href="/logged-in">Go to logged in area</a>
    <br />
    <br />
    <button on:click={() => signOut()}>Log Out</button>
  {:else}
    <button on:click={() => signIn('google')}>Log in</button>
  {/if}
  <section>
    <slot />
  </section>
</main>

There should be a root +page.svelte file that was generated when you scaffolded the project, with something like this in there

<h1>This is the home page</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the SvelteKit docs</p>

Feel free to just leave it.

Next, we’ll create a route called logged-in. Create a folder in routes called logged-in and create a +page.server.ts which for now will always just redirect you out.

import { redirect } from "@sveltejs/kit";

export const load = async ({}) => {
  redirect(302, "/");
};

Now let’s create the page itself, in +page.svelte and add some markup

<h3>This is the logged in page</h3>

And that’s about it. Check out the GitHub repo to see everything, including just a handful of additional styles.

Adding Auth

Let’s get started with the actual authentication.

First, create an environment variable in your .env file called AUTH_SECRET and set it to a random string that’s at least 32 characters. If you’re looking to deploy this to a host like Vercel or Netlify, be sure to add your environment variable in your project’s settings according to how that host does things.

Next, create a hooks.server.ts (or .js) file directly under src. The docs for this file are here, but it essentially allows you to add application-wide wide side effects. Authentication falls under this, which is why we configure it here.

Now let’s start integrating auth. We’ll start with a very basic config:

import { SvelteKitAuth } from "@auth/sveltekit";
import { AUTH_SECRET } from "$env/static/private";

const auth = SvelteKitAuth({
  providers: [],
  session: {
    maxAge: 60 * 60 * 24 * 365,
    strategy: "jwt",
  },

  secret: AUTH_SECRET,
});

export const handle = auth.handle;

We tell auth to store our authentication info in a JWT token, and configure a max age for the session as 1 year. We provide our secret, and a (currently empty) array of providers.

Adding our provider

Providers are what perform the actual authentication of a user. There’s a very, very long list of options to choose from, which are listed here. We’ll use Google. First, we’ll need to create application credentials. So head on over to the Google Developers console. Click on credentials, and then “Create Credentials”

Click it, then choose “OAuth Client ID.” Choose web application, and give your app a name.

For now, leave the other options empty, and click Create.

Screenshot

Before closing that modal, grab the client id, and client secret values, and paste them into environment variables for your app

GOOGLE_AUTH_CLIENT_ID=....
GOOGLE_AUTH_SECRET=....

Now let’s go back into our hooks.server.ts file, and import our new environment variables:

import { AUTH_SECRET, GOOGLE_AUTH_CLIENT_ID, GOOGLE_AUTH_SECRET } from "$env/static/private";

and then add our provider

providers: [
  GoogleProvider({
    clientId: GOOGLE_AUTH_CLIENT_ID,
    clientSecret: GOOGLE_AUTH_SECRET
  })
],

and then export our auth handler as our hooks handler.

export const handle = auth.handle;

Note that if you had other handlers you wanted SvelteKit to run, you can use the sequence helper:

import { sequence } from "@sveltejs/kit/hooks";

export const handle = sequence(otherHandleFn, auth.handle);

Unfortunately if we try to login now, we’re greeted by an error:

Clicking error details provides some more info:

We need to tell Google that this redirect URL is in fact valid. Go back to our Google Developer Console, open the credentials we just created, and add this URL in the redirect urls section.

And now, after saving (and possibly waiting a few seconds) we can click login, and see a list of our Google accounts available, and pick the one we want to log in with

Choosing one of the accounts should log you in, and bring you right back to the same page you were just looking at.

So you’ve successfully logged in, now what?

Being logged in is by itself useless without some way to check logged in state, in order to change content and grant access accordingly. Let’s go back to our layout’s server loader

export const load = async ({ locals }) => {
  return {
    loggedIn: false,
  };
};

We previously pulled in that locals property. Auth.js adds a getSession method to this, which allows us to grab the current authentication, if any. We just logged in, so let’s grab the session and see what’s there

export const load = async ({ locals }) => {
  const session = await locals.getSession();
  console.log({ session });

  return {
    loggedIn: false,
  };
};

For me, this logs the following:

All we need right now is a simple boolean indicating whether the user is logged in, so let’s send down a boolean on whether the user object exists:

export const load = async ({ locals }) => {
  const session = await locals.getSession();
  const loggedIn = !!session?.user;

  return {
    loggedIn,
  };
};

and just like that, our page updates:

The link to our logged-in page still doesn’t work, since it’s still always redirecting. We could run the same code we did before, and call locals.getSession to see if the user is logged in. But we already did that, and stored the loggedIn property in our layout’s loader. This makes it available to any routes underneath. So let’s grab it, and conditionally redirect based on its value.

import { redirect } from "@sveltejs/kit";

export const load = async ({ parent }) => {
  const parentData = await parent();

  if (!parentData.loggedIn) {
    redirect(302, "/");
  }
};

And now our logged-in page works:

Persisting our authentication

We could end this post here. Our authentication works, and we integrated it into application state. Sure, there’s a myriad of other auth providers (GitHub, Facebook, etc), but those are just variations on the same theme.

But one topic we haven’t discussed is authentication persistence. Right now our entire session is stored in a JWT, on our user’s machine. This is convenient, but it does offer some downsides, namely that this data could be stolen. An alternative is to persist our users’ sessions in an external database. This post discusses the various tradeoffs, but most of the downsides of stateful (i.e. stored in a database) solutions are complexity and the burden of having to reach out to an external storage to grab session info. Fortunately, Auth.js removes the complexity burden for us. As far as performance concerns, we can choose a storage mechanism that’s known for being fast and effective: in our case we’ll look at DynamoDB.

Adapters

The mechanism by which Auth.js persists our authentication sessions is database adapters. As before, there are many to choose from. We’ll use DynamoDB. Compared to providers, the setup for database adapters is a bit more involved, and a bit more tedious. In order to keep the focus of this post on Auth.js, we won’t walk through setting up each and every key field, TTL setting, and GSI—to say nothing of AWS credentials if you don’t have them already. If you’ve never used Dynamo and are curious, I wrote an introduction here. If you’re not really interested in Dynamo, this section will show you the basics of setting up database adapters, which you can apply to any of the (many) others you might prefer to use.

That said, if you’re interested in implementing this yourself, the adapter docs provide CDK and CloudFormation templates for the Dynamo table you need, or if you want a low-dev-ops solution, it even lists out the keys, TTL and GSI structure here, which is pretty painless to just set up.

We’ll assume you’ve got your DynamoDB instance set up, and look at the code to connect it. First, we’ll install some new libraries

npm i @auth/dynamodb-adapter @aws-sdk/lib-dynamodb @aws-sdk/client-dynamodb

First, make sure your dynamo table name, as well as your AWS credentials are in environment variables

Now we’ll go back to our hooks.server.ts file, and whip up some boilerplate (which, to be honest, is mostly copied right from the docs).

import { GOOGLE_AUTH_CLIENT_ID, GOOGLE_AUTH_SECRET, AMAZON_ACCESS_KEY, AMAZON_SECRET_KEY, DYNAMO_AUTH_TABLE, AUTH_SECRET } from "$env/static/private";

import { DynamoDB, type DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
import { DynamoDBAdapter } from "@next-auth/dynamodb-adapter";
import type { Adapter } from "@auth/core/adapters";

const dynamoConfig: DynamoDBClientConfig = {
  credentials: {
    accessKeyId: AMAZON_ACCESS_KEY,
    secretAccessKey: AMAZON_SECRET_KEY,
  },

  region: "us-east-1",
};

const client = DynamoDBDocument.from(new DynamoDB(dynamoConfig), {
  marshallOptions: {
    convertEmptyValues: true,
    removeUndefinedValues: true,
    convertClassInstanceToMap: true,
  },
});

and now we add our adapter to our auth config:

  adapter: DynamoDBAdapter(client, { tableName: DYNAMO_AUTH_TABLE }),

and now, after logging out, and logging back in, we should see some entries in our DynamoDB instance

Authentication hooks

The auth-core package provides a number of callbacks you can hook into, if you need to do some custom processing.

The signIn callback is invoked, predictably, after a successful login. It’s passed an account object from whatever provider was used, Google in our case. One use case with this callback could be to optionally look up, and sync legacy user metadata you might have stored for your users before switching over to OUath authentication with established providers.

async signIn({ account }) {
  const userSync = await getLegacyUserInfo(account.providerAccountId);
  if (userSync) {
    account.syncdId = userSync.sk;
  }

  return true;
},

The jwt callback gives you the ability to store additional info in the authentication token (you can use this regardless of whether you’re using a database adapter). It’s passed the (possibly mutated) account object from the signIn callback.

async jwt({ token, account }) {
  token.userId ??= account?.syncdId || account?.providerAccountId;
  if (account?.syncdId) {
    token.legacySync = true;
  }
  return token;
}

We’re setting a single userId onto our token that’s either the syndId we just looked up, or the providerAccountId already attached to the provider account. If you’re curious about the ??= operator, that’s the nullish coalescing assignment operator.

Lastly, the session callback gives you an opportunity to shape the session object that’s returned when your application code calls locals.getSession()

async session({ session, user, token }: any) {
  session.userId = token.userId;
  if (token.legacySync) {
    session.legacySync = true;
  }
  return session;
}

now our code could look for the legacySync property, to discern that a given login has already sync’d with a legacy account, and therefore know not to ever prompt the user about this.

Extending the types

Let’s say we do extand the default session type, like we did above. Let’s see how we can tell TypeScript about the things we’re adding. Basically, we need to use a TypeScript feature called interface merging. We essentially re-declare an interface that already exists, add stuff, and then TypeScript does the grunt work of merging (hence the name) the original type along with the changes we’ve made.

Let’s see it in action. Go to the app.d.ts file SvelteKit adds to the root src folder, and add this

declare module "@auth/core/types" {
  interface Session {
    userId: string;
    provider: string;
    legacySync: boolean;
  }
}

export {};

We have to put the interface in the right module, and then we add what we need to add.

Note the odd export {}; at the end. There has to be at least one ESM import or export, so TypeScript treats the file correctly. SvelteKit by default adds this, but make sure it’s present in your final product.

Wrapping up

We’ve covered a broad range of topics in this post. We’ve seen how to set up Auth.js in a SvelteKit project using the @auth/core library. We saw how to set up providers, adapters, and then took a look at various callbacks that allow us to customize our authentication flows.

Best of all, the tools we saw will work with SvelteKit or Next, so if you’re already an experienced Next user, a lot of this was probably familiar. If not, much of what you saw will be portable to Next if you ever find yourself using that.

]]>
https://frontendmasters.com/blog/using-nextauth-now-auth-js-with-sveltekit/feed/ 0 1764