A Guide to the Svelte Framework: Enhancing Speed and Efficiency

The world of web applications is constantly expanding, favored for their user-friendliness, rapid performance, and accessibility across various platforms. Single Page Applications (SPAs) play a significant role in this trend, with frameworks like Angular, Vue.js, and React empowering developers to create exceptional user experiences efficiently, while maintaining code that is easy to support and expand upon. These established tools have long dominated the field, offering numerous advantages over newer alternatives, almost forming an oligopoly in the SPA development landscape. However, a group of innovative developers is challenging this dominance with a powerful contender—Svelte.

Svelte presents a novel method for crafting user interfaces. Let’s delve into its unique approach by building a standard login form.

Architecture

Svelte’s design prioritizes speed, surpassing other libraries by eliminating the need to load a framework for virtual DOM manipulation. Instead of relying on a runtime tool, Svelte compiles code into vanilla JavaScript during the build process. This results in applications that are free from dependencies and start up rapidly.

SvelteOther SPA libraries (React, Vue.js, Angular, etc.)

1. Open a website
2. Render the page using pure JS

1. Open a website
2. Wait until the code for building a virtual DOM is loaded
3. Render the page using the library

The table above illustrates Svelte’s clear advantage in startup performance, achieved not through specific optimizations but by leveraging the browser’s native JavaScript compiler instead of an external one.

Installation

One of Svelte’s strengths is its straightforward installation process, which contributes to a smooth development experience. Begin by obtaining the project template:

1
npx degit sveltejs/template svelte-login-form

Once this command is executed, you’ll have a basic Svelte project template. However, it’s currently empty, lacking the necessary NPM packages. Let’s address that:

1
2
cd svelte-login-form
npm install

The application is now ready for launch. Use the following command:

1
npm run dev

Structure

Every Svelte component potentially consists of three sections:

  • Script
  • Style
  • Template

Let’s examine an example within the src/App.svelte file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script>
  export let name;
</script>

<style>	
  h1 {
    color: purple;
}
</style>

<h1>{name}</h1>

This code snippet clearly demonstrates the three sections:

  1. The script tag, an optional block for JavaScript code. Here, you define variables and functions used within the component.

  2. The style tag, another optional block. While similar to a standard HTML style tag, it differs significantly in scope. Styles defined within this block are confined to the current component, preventing unintended style conflicts. This eliminates the need for complex class naming conventions and ensures styles are applied precisely where intended.

  3. Finally, the indispensable template block, represented here as an h1 tag. This section determines the presentation or view of your component, working in tandem with the script and style blocks to dictate its appearance and behavior.

Svelte champions modularity in front-end development, not only by separating components but also by isolating logic, view, and template.

Returning to our login form, let’s create a new file, LoginForm.svelte, within the src folder and populate it with the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<style>	
  form {
    background: #fff;
    padding: 50px;
    width: 250px;
    height: 400px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    box-shadow: 0px 20px 14px 8px rgba(0, 0, 0, 0.58);
  }
  label {
    margin: 10px 0;
    align-self: flex-start;	
    font-weight: 500;
  }
  input {
    border: none;
    border-bottom: 1px solid #ccc;
    margin-bottom: 20px;
    transition: all 300ms ease-in-out;
    width: 100%;
  }
  input:focus {
    outline: 0;
    border-bottom: 1px solid #666;
  }
  button {
    margin-top: 20px;
    background: black;
    color: white;
    padding: 10px 0;
    width: 200px;
    border-radius: 25px;
    text-transform: uppercase;
    font-weight: bold;
    cursor: pointer;
    transition: all 300ms ease-in-out;
  }
  button:hover {
    transform: translateY(-2.5px);
    box-shadow: 0px 1px 10px 0px rgba(0, 0, 0, 0.58);
  }
  h1 {
    margin: 10px 20px 30px 20px;
    font-size: 40px;
  }
</style>


<form>	
  <h1>👤</h1>

  <label>Email</label>
  <input name="email" placeholder="name@example.com" />

  <label>Password</label>
  <input name="password" type="password" placeholder="password" />

  <button type="submit">Log in 🔒</button>
</form>

For now, it’s a simple, styled component. We’ll enhance its functionality later. To display it on our site, we need to render it within the root component, App. Modify src/App.svelte as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<script>
  import LoginForm from "./LoginForm.svelte";
</script>

<style>	
  section {
    height: 100vh;
    width: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    background: linear-gradient(to right, #cd76e2, #e358ab);	
  }
</style>

<section>
  <LoginForm />
</section>

If everything is set up correctly and the application is running, you should see the form at localhost:5000. Let’s take our Svelte skills further and make the form interactive.

Going Stateful

In Svelte, each component can maintain its own state. The state is represented by a special variable or a group of variables used within the template. These variables are considered special because any change to them automatically triggers an update to the template, ensuring that the displayed content reflects the latest state. This mechanism facilitates rapid responses to user interactions.

We’ll define email and password as state variables to store the corresponding form field values. This ensures that these variables are always synchronized with the form, allowing us to submit the values at any time without discrepancies between the submitted data and the actual form content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script>
  let email = "";
  let password = "";
  let isLoading = false;
  const handleSubmit = () => {
      isLoading = true;
      // Simulate network request
      setTimeout(() => {
        isLoading = false;
        // Authorize the user
      }, 1000);
  };
</script>

<style>	
/* Style is unchanged */
</style>


<form on:submit|preventDefault={handleSubmit}>
  <h1>👤</h1>

  <label>Email</label>
  <input name="email" placeholder="name@example.com" bind:value={email} />

  <label>Password</label>
  <input name="password" type="password" bind:value={password} />

    {#if isLoading}Logging in...{:else}Log in 🔒{/if}
</form>

At first glance, state variables resemble regular JavaScript variables. However, to establish synchronization with form values (binding them to the form fields), we use the bind:value directive. Additionally, we introduce a couple of new concepts:

  • on:submit|preventDefault offers a concise way to prevent default event behavior, providing a cleaner alternative to repeatedly writing e.preventDefault().

  • {#if isLoading}Logging in...{:else}Log in 🔒{/if} demonstrates Svelte’s template syntax. Since JavaScript code is not directly executed within the template block, Svelte provides special syntax for conditional statements, loops, and similar constructs.

Now, let’s utilize the state to enhance our form with validation. We’ll achieve this by introducing another state variable, errors, which will store error messages when the form is submitted with invalid data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<script>
  let email = "";
  let password = "";
  let isLoading = false;
  let errors = {};
  const handleSubmit = () => {
    errors = {};
    if (email.length === 0) {
      errors.email = "Field should not be empty";
    }
    if (password.length === 0) {
      errors.password = "Field should not be empty";
    }
    if (Object.keys(errors).length === 0) {
      isLoading = true;
      // Simulate network request
      setTimeout(() => {
        isLoading = false;
        // Authorize the user
      }, 1000);
    }
  };
</script>

<style>	
  // Previous styles unchanged
  .errors {
    list-style-type: none;
    padding: 10px;
    margin: 0;
    border: 2px solid #be6283;
    color: #be6283;
    background: rgba(190, 98, 131, 0.3);
  }
</style>

<form on:submit|preventDefault={handleSubmit}>
  <h1>👤</h1>
  
  <label>Email</label>
  <input name="email" placeholder="name@example.com" bind:value={email} />
  
  <label>Password</label>
  <input name="password" type="password" bind:value={password} />
  
  <button type="submit">
    {#if isLoading}Logging in...{:else}Log in 🔒{/if}
  </button>
  
  {#if Object.keys(errors).length > 0}
    <ul class="errors">
      {#each Object.keys(errors) as field}
        <li>{field}: {errors[field]}</li>
      {/each}
    </ul>
  {/if}
</form>
Login form error

Our form is nearly complete. All that remains is displaying a success message upon successful authentication.

We’ll introduce a state variable named isSuccess, initially set to false, to track successful submissions. After a successful form submission, this variable’s value should change to true.

1
let isSuccess = false;

We also need to adjust the function responsible for handling form submissions to reflect the logic of toggling isSuccess after a successful operation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const handleSubmit = () => {
  errors = {};
  if (email.length === 0) {
    errors.email = "Field should not be empty";
  }
  if (password.length === 0) {
    errors.password = "Field should not be empty";
  }
  if (Object.keys(errors).length === 0) {
    isLoading = true;
    // Simulate network request
    setTimeout(() => {
      isLoading = false;
      isSuccess = true;
      // Authorize the user
    }, 1000);
  }
};

With this modification, the form will transition to a success state immediately after submission.

However, if you observe the development server, you won’t notice any changes in the form’s behavior. While we’ve modified the code, we haven’t updated the template. We need to instruct the template to display a success message when a user successfully logs in. Svelte’s template syntax simplifies this task:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<form on:submit|preventDefault={handleSubmit}>
  {#if isSuccess}
    <div class="success">
      🔓
      <br />
      You've been successfully logged in.
    </div>
  {:else}
    <h1>👤</h1>

    <label>Email</label>
    <input name="email" placeholder="name@example.com" bind:value={email} />

    <label>Password</label>
    <input name="password" type="password" bind:value={password} />

    <button type="submit">
      {#if isLoading}Logging in...{:else}Log in 🔒{/if}
    </button>

    {#if Object.keys(errors).length > 0}
      <ul class="errors">
        {#each Object.keys(errors) as field}
          <li>{field}: {errors[field]}</li>
        {/each}
      </ul>
    {/if}
  {/if}
</form>

Abstract with Properties

With the internal state of our component defined, let’s shift our focus to external dependencies known as properties, or “props”. Props act as inputs or arguments passed into a component, influencing its appearance or behavior.

Declaring a prop is similar to declaring a state variable, except for the use of the export keyword.

1
2
3
4
5
<script>
	export let answer;
</script>

<p>The answer is {answer}</p>
1
2
3
4
5
<script>
	import Nested from './Nested.svelte';
</script>

<Nested answer={42}/>

Working with props is straightforward: declare and pass.

But how do these properties relate to our login form component? Props enable us to create a more generic login form by extracting the submission logic into a property. This allows us to reuse the component with different submission actions, such as sending requests to a test server or a live server. We’ll name this prop submit. It will be a function that returns a resolved promise upon successful submission and a rejected promise if an error occurs. Let’s declare the prop using the example provided earlier:

1
export let submit;

Next, we need to modify the submission handler within the login form to utilize the newly introduced submit property.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const handleSubmit = () => {
  errors = {};
  if (email.length === 0) {
    errors.email = "Field should not be empty";
  }
  if (password.length === 0) {
    errors.password = "Field should not be empty";
  }
  if (Object.keys(errors).length === 0) {
    isLoading = true;
    submit({ email, password })
      .then(() => {
        isSuccess = true;
        isLoading = false;
      })
      .catch(err => {
        errors.server = err;
        isLoading = false;
      });
  }
};

Our component seems ready. However, if you test the form, you’ll find that the button remains in a loading state and an error appears in the console: Uncaught TypeError: submit is not a function. This occurs because while we declared the prop, we haven’t passed it to the component. Let’s define a function within the app component and pass it to the login form.

1
2
const submit = ({ email, password }) =>
  new Promise((resolve, reject) => setTimeout(resolve, 1000));
1
2
3
<section>
  <LoginForm submit={submit} />
</section>

With this adjustment, the form now functions as intended, displaying errors when necessary and informing the user upon successful login.

Login form success

Context Sharing

At this point, we’ve covered the essential elements for building an application. With properties and internal state management, we can create complex SPAs. However, efficiently sharing data among numerous components can still pose challenges.

Consider a scenario where we have a globally accessible user variable. Numerous components might need to adjust their behavior based on the user’s role, age, status, etc. Relying solely on props to pass the user object to each component would result in repetitive and less maintainable code.

To address this, Svelte provides the context API.

This API offers a way for components to communicate without constantly passing data and functions through props or triggering a multitude of events. While considered an advanced feature, it proves incredibly valuable.

Let’s integrate the user context into our login form. Create a file named userContext.js within the src folder and add the following code:

1
2
export const key = "userContext";
export const initialValue = null;

The key acts as a unique identifier for the context, as an application can have multiple contexts that need to be accessed independently. initialValue sets the default value for the context before it’s explicitly set.

Next, we’ll introduce the context to our application. Open the App.svelte file and add two import statements:

1
2
3
4
5
import { onMount, setContext } from "svelte";
import {
  key as userContextKey,
  initialValue as userContextInitialValue
} from "./userContext";

You might be curious about what we’re importing from the svelte package. The onMount function is a helper that accepts a callback function as an argument. This callback will be executed when the current component is mounted, which is at the very beginning of the component loading process. The setContext function, on the other hand, is used to set the value of a context. It requires the context’s key and the new value as arguments.

Let’s utilize the onMount function to establish a default value for our context:

1
2
3
onMount(() => {
  setContext(userContextKey, userContextInitialValue);
});

Now, modify the submit function to update the user context:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const submit = ({ email, password }) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      setContext(userContextKey, {
        name: "Foo",
        lastName: "Bar",
        email: "foo@bar.com"
      });
      resolve();
    }, 1000);
  });

That’s all it takes. A successful submission will update the user context with a placeholder user object. We can access this object using the getContext function:

1
2
3
4
5
<script>
import { getContext } from 'svelte';
import { key as userContextKey } from "./userContext";
const user = getContext(key);	
</script>

Summary

Svelte is a powerful and versatile tool for building high-performance web applications. In addition to the core concepts covered in this article, Svelte offers a range of features out of the box, including:

  • Reactive declarations and statements
  • Await blocks within templates
  • Dimension binding
  • A global store similar to Redux
  • Helpers for animations and transitions
  • Debugging tools

In conclusion, Svelte is a robust library that fulfills the requirements for building sophisticated SPAs and beyond. It has the potential to rival and even surpass the leading frameworks in the industry. What Svelte currently needs most is wider adoption and support within the front-end development community.

Note: All code examples used in this article are available in the teimurjan/svelte-login-form GitHub repository. You can find a live demo of the login form here.

Licensed under CC BY-NC-SA 4.0