Forms and Client-Server Interaction

State and Client-Side APIs


Learning Objectives

  • You know how to initialize the state from the API when components are mounted.
  • You understand the importance of planning how and when to read the state from the API to avoid unnecessary or duplicate API calls.
  • You know how to use the $derived rune to create reactive variables based on other reactive variables.

At the end of the chapter Persisting State with LocalStorage in the part Client-Side Pages, Components, and Interactivity, we finished functionality for persisting the shared state in localStorage.

The file bookState.svelte.js that stored the books to localStorage looked similar to the following.

import { browser } from "$app/environment";
const BOOKS_KEY = "books";
let initialBooks = [];
if (browser && localStorage.getItem(BOOKS_KEY) != null) {
initialBooks = JSON.parse(localStorage.getItem(BOOKS_KEY));
}
let bookState = $state(initialBooks);
const saveBooks = () => {
localStorage.setItem(BOOKS_KEY, JSON.stringify(bookState));
};
const useBookState = () => {
return {
get books() {
return bookState;
},
getOne: (id) => {
return bookState.find((b) => b.id === id);
},
addBook: (book) => {
bookState.push(book);
saveBooks();
},
removeBook: (book) => {
bookState = bookState.filter((b) => b.id !== book.id);
saveBooks();
},
updateBook: (book) => {
const index = bookState.findIndex((b) => b.id === book.id);
if (index !== -1) {
bookState[index] = book;
saveBooks();
}
},
};
};
export { useBookState };

The key functionality of the above code is that it (1) uses Svelte’s $state to create a reactive variable, (2) initializes the state from localStorage, (3) saves the state to localStorage whenever the state changes, and (4) exposes a module-level singleton with functions that can be used to access and modify the state.

Function useBookState

0 / 5 points

What is the main purpose of the useBookState function in bookState.svelte.js?

Now that we have a client-side API module, we can use it to replace the localStorage in our Svelte components.

Initializing the State with the API

To use the server-side API instead of localStorage, we need to replace the interactions with localStorage with interactions with the API. We further need to decide how and when to read the state from the API — should we read all the books at start, or should we read them only when they are needed?

As we know about how components are mounted, we can read the books that are needed at the time of mounting a component. To avoid unnecessary or duplicate API calls, this needs to be planned — that is, we need to decide which components will read the books, and when.

Reading books to the state

There are two primary ways how books are used. The page at /books lists all the books in the system, and the page at /books/[id] shows a single book. To support these two use cases, we need to be able to read all the books, and to read a single book by its identifier.

To get started, let’s modify the bookState.svelte.js file to import the booksApi module, and then to provide two additional functions: a function to read all the books, and a function to read a single book by its identifier. Both functions will use the booksApi module we created in the previous chapter.

import { browser } from "$app/environment";
import * as booksApi from "$lib/apis/booksApi.js";
let bookState = $state([]);
const initBooks = async () => {
if (browser) {
bookState = await booksApi.readBooks();
}
};
const initBook = async (id) => {
if (browser) {
const book = await booksApi.readBook(id);
if (book && !bookState.find((b) => b.id === book.id)) {
bookState.push(book);
}
}
};
// ...
export {
initBooks,
initBook,
useBookState
};

Now, we have two functions: initBooks and initBook. The initBooks function reads all the books from the API and sets the state to the returned books. The initBook function reads a single book by its identifier, and if the book is not already in the state, it adds it to the state.

Initialization scope

0 / 5 points

Why does initBooks check for if (browser) before fetching from the API?

Reading all books at /books

As all books are listed when the user navigates to the /books page, it makes sense to read all the books when the /books page is loaded.

Modify the +page.svelte file in the src/routes/books folder so that it imports the book state and calls the initBooks function using $effect rune:

<script>
import { initBooks } from "$lib/states/bookState.svelte.js";
import BookForm from "$lib/components/BookForm.svelte";
import BookList from "$lib/components/BookList.svelte";
$effect(() => {
initBooks();
});
</script>
<h1>Books</h1>
<BookList />
<h2>Add a book</h2>
<BookForm />

Now, when the user navigates to the books page, the initBooks function is called, and all the books are read from the API and set to the state.

Reading a single book

Similarly, the initBook function can be called when the user navigates to the /books/[bookId] page to read a single book by its identifier.


Missing books

0 / 5 points

A developer switches from localstorage to API-backed state. On the /books page, no books appear, even though adding books through a form adds them to the database. Which step was most likely forgotten?

Using the API to Modify the State

Now we have the state initialized from the API, but we still need to modify the state when books are added, removed, or updated. To do this, we need to replace the interactions with localStorage with interactions with the API.

Retrieving all books

Retrieving all books does not change, as the useBookState directly provides a getter to the bookState object. Svelte augments the getter function so that changes to the bookState are reflected in all components that use the getter function.

import { browser } from "$app/environment";
import * as booksApi from "$lib/apis/booksApi.js";
let bookState = $state([]);
const initBooks = async () => {
if (browser) {
bookState = await booksApi.readBooks();
}
};
const initBook = async (id) => {
if (browser) {
const book = await booksApi.readBook(id);
if (book && !bookState.find((b) => b.id === book.id)) {
bookState.push(book);
}
}
};
const useBookState = () => {
return {
get books() {
return bookState;
},
// ...

Retrieving a single book

Our prior implementation for retrieving a single book used a function getOne, which searched for a book from the existing list of books and returned it. The object returned by the getOne function is not reactive, however. The reason our prior implementation has worked is that the book state has been initiated from the localStorage synchronously in the bookState.svelte.js; this has resulted in the getOne function finding the book from the state.

However, when working with APIs or with any asynchronous functionality, we cannot expect that the asynchronous calls would have been finished when we retrieve a single book. Due to this, we drop the getOne function — if we need an individual book, we’ll look for it from the books list returned by the books getter function.

Dropping getOne

0 / 5 points

The material removes getOne(id) from useBookState. Why is the getOne(id) helper problematic after moving to an API-backed, async-initialized state?

Adding a book

To add a book, we need to use the createBook function of the booksApi. The function makes a POST request to the API to create a new book. We will also need to update the state of the bookState variable once the book has been added.

Similar to how we retrieved all the books, we will use the then function associated with the promise returned by the asynchronous createBook function to update the state of the bookState variable once the book has been added.

import { browser } from "$app/environment";
import * as booksApi from "$lib/apis/booksApi.js";
let bookState = $state([]);
const initBooks = async () => {
if (browser) {
bookState = await booksApi.readBooks();
}
};
const initBook = async (id) => {
if (browser) {
const book = await booksApi.readBook(id);
if (book && !bookState.find((b) => b.id === book.id)) {
bookState.push(book);
}
}
};
const useBookState = () => {
return {
get books() {
return bookState;
},
addBook: (book) => {
booksApi.createBook(book).then((newBook) => {
bookState.push(newBook);
});
},
// ...

We could alternatively change the function to be asynchronous, and use await to wait for the book to be added before updating the state.

Adding books

0 / 5 points

Here’s an almost ready implementation for the asynchronous add method for bookState.svelte.js:

add: async (book) => {
const newBook = await booksApi.createBook(book);
// What goes here?
}

Which of the following options correctly completes the implementation?

Removing a book

To remove a book, we use the deleteBook function of the booksApi. The deleteBook function returns a promise that resolves to the deleted book. We will also need to update the state of the bookState variable once the book has been removed.

import { browser } from "$app/environment";
import * as booksApi from "$lib/apis/booksApi.js";
let bookState = $state([]);
const initBooks = async () => {
if (browser) {
bookState = await booksApi.readBooks();
}
};
const initBook = async (id) => {
if (browser) {
const book = await booksApi.readBook(id);
if (book && !bookState.find((b) => b.id === book.id)) {
bookState.push(book);
}
}
};
const useBookState = () => {
return {
get books() {
return bookState;
},
addBook: (book) => {
booksApi.createBook(book).then((newBook) => {
bookState.push(newBook);
});
},
removeBook: (book) => {
booksApi.deleteBook(book.id).then((removed) => {
bookState = bookState.filter((b) => b.id !== removed.id);
});
},
// ...

Like earlier, we could also change the function to be asynchronous, and use await to wait for the book to be removed before updating the state. This would look as follows:

removeBook: async (book) => {
const removed = await booksApi.deleteBook(book.id);
bookState = bookState.filter((b) => b.id !== removed.id);
},

Updating a book

Finally, to update a book, we use the updateBook function of the booksApi. The updateBook function returns a promise that resolves to the updated book. We will also need to update the state of the bookState variable once the book has been updated.

import { browser } from "$app/environment";
import * as booksApi from "$lib/apis/booksApi.js";
let bookState = $state([]);
const initBooks = async () => {
if (browser) {
bookState = await booksApi.readBooks();
}
};
const initBook = async (id) => {
if (browser) {
const book = await booksApi.readBook(id);
if (book && !bookState.find((b) => b.id === book.id)) {
bookState.push(book);
}
}
};
const useBookState = () => {
return {
get books() {
return bookState;
},
addBook: (book) => {
booksApi.createBook(book).then((newBook) => {
bookState.push(newBook);
});
},
removeBook: (book) => {
booksApi.deleteBook(book.id).then(() => {
bookState = bookState.filter((b) => b.id !== book.id);
});
},
updateBook: (book) => {
booksApi.updateBook(book.id, book).then((updatedBook) => {
const index = bookState.findIndex((b) => b.id === updatedBook.id);
if (index !== -1) {
bookState[index] = updatedBook;
}
});
},
};
};
export { useBookState };

Similar to the earlier examples, we could also use async/await to make the function asynchronous and use await to wait for the book to be updated before updating the state. This would look as follows:

updateBook: async (book) => {
const updatedBook = await booksApi.updateBook(book.id, book);
const index = bookState.findIndex((b) => b.id === updatedBook.id);
if (index !== -1) {
bookState[index] = updatedBook;
}
},

Now, we’ve just plugged in the client-side API to the shared state, and we can use the same functionality as before. Instead of the books being stored in the localStorage, they are now stored in the server-side API; anyone accessing the application would see the same books.

The reason why this works is that the form fields that we use for entering data correspond to the book data that the server-side API expects, and the API returns the data in the same format as the localStorage did.

Coffee beans

0 / 25 points

You have a small Coffee Beans app with a shared state used by a form (to add beans) and a list to view and delete beans. Currently, the shared state persists data to localStorage. Your task is to replace the persistence so the shared state talks to a server API instead. The server API is as follows:

  • GET /api/beans —> returns a list of beans
  • POST /api/beans —> returns a created bean
  • PUT /api/beans/{id} —> returns an updated bean
  • DELETE /api/beans/{id} —> returns a deleted bean

The beans have the following structure:

{
"id": 1,
"name": "Ethiopian Yirgacheffe",
"roast": "light"
}

To do this, first, implement the necessary functions to the API module $lib/apis/beansApi.js. Then, modify the shared state module $lib/states/beanState.svelte.js to use the API module instead of localStorage. Finally, initiate the state from the server when the app loads in +page.svelte.

Showing a single book

Earlier, we dropped the getOne function from useBookState. The reason for this was that the object that getOne returns is not reactive, and that we cannot rely on the state being initiated when getOne is called.

An asynchronous function used to initialize the state can take plenty of time to finish.

Let’s look into how book information for a single book would be shown.

Book.svelte and page for an individual book

In Forms and Form Events, we drafted a Book.svelte component that looked as follows:

<script>
let { bookId } = $props();
import ChapterList from "$lib/components/books/ChapterList.svelte";
import { useBookState } from "$lib/states/bookState.svelte.js";
let bookState = useBookState();
let book = bookState.getOne(bookId);
</script>
{#if book}
<h1>{book.title}</h1>
<p><strong>Description:</strong> {book.description}</p>
<p><strong>Published at:</strong> {book.published_at}</p>
<p><strong>Page count:</strong> {book.page_count}</p>
{:else}
<h1>Loading...</h1>
{/if}
<ChapterList {bookId} />

As the getOne function is no longer available, we need to modify the component to show the book information differently. Let’s start by simplifying the component to just show the book identifier. Modify Book.svelte to show the following:

<script>
let { bookId } = $props();
</script>
<h1>Book id {bookId}</h1>

Then, modify src/routes/books/[bookId]/+page.svelte file as follows to import and show the individual book, also adding a call to initBook to load book details. We drop the chapter-specific functionality for now, but will add it back in the next chapter.

<script>
import { page } from "$app/state";
import Book from "$lib/components/Book.svelte";
import { initBook } from "$lib/states/bookState.svelte.js";
let bookId = $derived(parseInt(page.params.bookId));
$effect(() => {
initBook(bookId);
});
</script>
<Book bookId={bookId} />

Showing book information

Then, let’s modify the Book.svelte component in the src/lib/components folder to display the book information. Let’s first just show the title of a single book.

<script>
import { useBookState } from "$lib/states/bookState.svelte.js";
let { bookId } = $props();
let bookState = useBookState();
</script>
<h1>
{bookState?.books?.find((book) => book.id === bookId)?.title ?? "Loading..."}
</h1>

Above, as getOne is no longer available, we search for the specific book from the books in bookState. If a book is found, we show its title. Otherwise, we show the text “Loading…”.

Why not in script?

0 / 5 points

Consider this code in Book.svelte:

<script>
import { useBookState } from "$lib/states/bookState.svelte.js";
let { bookId } = $props();
let bookState = useBookState();
// Proposed approach
let book = bookState.books.find((b) => b.id === bookId);
</script>

Why is this not the correct approach?

If we would wish to show the rest of the details as well, including description, publication date, and page count, the above approach would become rather cumbersome.

Rune $derived

Svelte has a rune $derived that’s a perfect fit for our scenario. The $derived rune is used for creating a reactive variable based on other reactive variables. As an example, the following code creates a derived variable previous that is always one smaller than the reactive variable count.

<script>
let count = $state(0);
let previous = $derived(count - 1);
</script>
<button onclick={() => count++}>Increment</button>
<p>Previous: {previous}, Count: {count}</p>

Now, whenever the user presses the button, the count variable is incremented, and the previous variable is automatically updated to be one smaller than the count variable.

Rune $derived

0 / 5 points

In Svelte, what does the $derived rune do?

The $derived rune takes an expression that evaluates to a value, and it automatically updates the derived variable whenever any of the reactive variables used in the expression change. We can use this to create a derived variable book that is always the book with the identifier bookId from the bookState.books array.

<script>
import { useBookState } from "$lib/states/bookState.svelte.js";
let { bookId } = $props();
let bookState = useBookState();
let book = $derived(bookState.books.find((book) => book.id === bookId));
</script>
<h1>{book ? book.title : "Loading..."}</h1>

Now, whenever the bookState.books array changes, the book variable is automatically updated to the book with the identifier bookId. If no such book exists, the book variable is set to undefined.

Replacing getOne with a reactive pattern

0 / 5 points

You removed getOne(id) from the state module. Which component-level pattern best replaces it to keep the UI in sync with async-loaded data?

With the derived book variable, we can easily show the rest of the book details as well.

<script>
import { useBookState } from "$lib/states/bookState.svelte.js";
let { bookId } = $props();
let bookState = useBookState();
let book = $derived(bookState.books.find((book) => book.id === bookId));
</script>
<h1>{book ? book.title : "Loading..."}</h1>
<p><strong>Description:</strong> {book.description}</p>
<p><strong>Published at:</strong> {book.published_at}</p>
<p><strong>Page count:</strong> {book.page_count}</p>

Likes with shared state

0 / 10 points

The exercise comes with an application that allows navigating to various resources using dynamic routing in Svelte. Each resource is identified by a unique resourceId. Each resource has a like count that can be increased — the likes are managed through a shared state, which is backed by an API.

At the moment, the likes are not properly loaded when the user navigates to a specific resource. Modify the application so that the likes are properly derived when navigating to a resource.

Summary

In summary:

  • When replacing a localStorage-based state with a server-side API-based state, we need to initialize the state from the API when the components are mounted, and we need to replace the interactions with localStorage with interactions with the API. Any functionality that explicitly relied on synchronous functionality (e.g. retrieving a single book) needs to be changed.
  • The way how the state is initialized depends on the use case, as we do not wish to make unnecessary API calls. In our example, we read all the books when the /books page is loaded, and we read a single book when the /books/[id] page is loaded.
  • If a state is page-specific, it can be initialized in the +page.svelte file of the page using the $effect rune.
  • The $derived rune can be used to create reactive variables based on other reactive variables. This is especially useful when we need to derive a value from a state that may change over time (e.g., when an asynchronous function is used to initialize the state).

AI Study Aid

Create a chapter diagram

We're looking into to what extent AI-generated diagrams could help with studying.

Use this study aid to generate an AI-generated visual summary of the material you just studied. Each diagram style emphasizes the content in a different way, so you can choose the focus that feels most useful.

Using the diagram generator is voluntary and does not affect your course progress. We encourage you to try it out and see if it helps, but it's totally up to you! Your answers help us understand how to make better study aids in the future.

Diagrams tried: 0

Todos and tasks, todos with an API

0 / 35 points

Continue working from your solution to the earlier “Todos and tasks with forms” exercise (from the end of the chapter “Forms and Form Events”). At the end of the exercise, it was possible to add todos and tasks with forms, todos and tasks were stored in a shared state, and the shared state was backed up by localstorage.

In this exercise, your task is to modify the application so that the todos are stored in a database and accessed through an API.

The tasks should still be stored in the shared state and localstorage. You’ll modify the functionality for tasks in the next chapter.

Api module

First, implement an API module for todos. For this, create a new file called todosApi.js in the src/lib/apis folder. The module should export the functions needed to interact with the todos API you implemented in the “Todo Controller and Repository” exercise. The API endpoints are as follows:

  • GET /api/todos -> returns all todos from the database
  • GET /api/todos/:todoId -> returns a single todo by its id
  • POST /api/todos -> creates a new todo and returns the created todo
  • PUT /api/todos/:todoId -> updates a todo by its id and returns the updated todo
  • DELETE /api/todos/:todoId -> deletes a todo by its id and returns the deleted todo

The data format for a todo is as follows.

{
"id": 1,
"name": "Write a TODO app",
"created_at": "2025-01-01T12:34:56.000Z"
}

If you have not done so, create a file .env.development to the client-side project, add add the environment variable PUBLIC_API_URL to it. Set the value of the variable to the base URL of your server-side API (e.g., http://localhost:8000). Furthermore, if you haven’t already done so, add CORS middleware to your server-side API. With these changes, you can test the application wikth the API locally.

Modifying the shared state

Then, modify the shared state for todos so that it uses the API module to interact with the todos and provides functions for initiating the todos from the API.

Verifying and modifying the pages

Finally, modify and verify the pages for listing todos and showing an individual todo. Their functionality should be as follows.

  • /todos — lists all todos and shows the form for adding a todo.

    • The todos should be fetched from the API when the page is loaded.
    • Adding a todo should create the todo through the API and then update the shared state accordingly.
    • Deleting a todo should delete the todo through the API and then update the shared state accordingly.
    • Clicking a todo should navigate to the individual todo page.
  • /todos/[todoId] — shows the details of a single todo.

    • The individual todo should be fetched from the API when the page is loaded.
    • The earlier synchronous functionality for showing the todo should be replaced with functionality that can handle asynchronous changes to the todos (i.e., retrieving from the API).
    • The task implementation should remain the same as in the earlier exercise “Todos and tasks with forms” (i.e., tasks are stored in the shared state and localstorage).

Important! Do not change the phrasing of the text in the components from your earlier version of the application. The tests expect the same formatting and text as before — now, the todos are just fetched from an API instead of being created and stored locally.

Submission format

Once ready, zip the contents of the folder client/src and return the zip here. The zip should not contain the client/src folder, but the contents of the client/src folder. The contents of the zip should be as follows.

Terminal window
.
├── lib
├── apis
└── todosApi.js
├── components
└── todos
├── Task.svelte
├── TaskForm.svelte
├── TaskList.svelte
├── Todo.svelte
├── TodoForm.svelte
└── TodoList.svelte
└── states
| ├── taskState.svelte.js
| └── todoState.svelte.js
└── routes
├── todos
├── [todoId]
├── tasks
└── [taskId]
└── +page.svelte
└── +page.svelte
└── +page.svelte
└── +page.svelte