Sharing State and APIs
Learning Objectives
- You know how to use APIs when sharing state.
At the end of the chapter Sharing State Between Components in the part Client-Side Development, we discussed sharing state between components in Svelte. At the end, we showed a pattern for storing the state in localstorage. The key part for sharing state was the file todoState.svelte.js
, which looked as follows.
import { browser } from "$app/environment";
const TODOS_KEY = "todos";
let initialTodos = [];
if (browser && localStorage.hasOwnProperty(TODOS_KEY)) {
initialTodos = JSON.parse(localStorage.getItem(TODOS_KEY));
}
let todoState = $state(initialTodos);
const saveTodos = () => {
localStorage.setItem(TODOS_KEY, JSON.stringify(todoState));
};
const useTodoState = () => {
return {
get todos() {
return todoState;
},
add: (todo) => {
todoState.push(todo);
saveTodos();
},
changeDone: (id) => {
const todo = todoState.find((todo) => todo.id === id);
todo.done = !todo.done;
saveTodos();
},
remove: (id) => {
todoState = todoState.filter((todo) => todo.id !== id);
saveTodos();
},
};
};
export { useTodoState };
The useTodoState
function is creates a reactive store for todos, storing the todos in the localstorage. The saveTodos
function is used to save the todos to the localstorage. The add
, changeDone
, and remove
functions are used to manipulate the todos.
To replace localstorage with an API, we would first need to create an API. As an example, we could have an API that provides todos as follows.
import { Hono } from "jsr:@hono/hono@4.6.5";
import { cors } from "jsr:@hono/hono@4.6.5/cors";
const app = new Hono();
app.use("/*", cors());
let todos = [{
id: 1,
name: "Eat cookies",
done: false,
}];
app.get("/todos", async (c) => {
return c.json(todos);
});
app.post("/todos", async (c) => {
const todo = await c.req.json();
todo.id = todos.length + 1;
todos.push(todo);
return c.json(todo);
});
app.delete("/todos/:id", async (c) => {
const id = Number(c.req.param("id"));
const deletedTodo = todos.find((todo) => todo.id === id);
todos = todos.filter((todo) => todo.id !== id);
return c.json(deletedTodo);
});
export default app;
Now, if the API would be running on a server at the environment variable PUBLIC_API_URL
, we could use the API in the client-side. First, we would wish to create an abstraction for working with the API. As discussed in the chapter Fetch API and Client-Side Requests, we could create a folder called apis
in the lib
folder, and create a file todo-api.js
that abstracts the API.
One possible version of the todo-api.js
file could look as follows (it’s almost the same as the one in the chapter “Fetch API and Client-Side Requests”; the key difference is that the readTodo
function has been replaced with the deleteTodo
function).
import { PUBLIC_API_URL } from "$env/static/public";
const createTodo = async (name, done) => {
const data = { name, done };
const response = await fetch(`${PUBLIC_API_URL}/todos`, {
headers: {
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify(data),
});
return await response.json();
};
const readTodos = async () => {
const response = await fetch(`${PUBLIC_API_URL}/todos`);
return await response.json();
};
const deleteTodo = async (id) => {
const response = await fetch(`${PUBLIC_API_URL}/todos/${id}`, {
method: "DELETE",
});
return await response.json();
};
export { createTodo, deleteTodo, readTodos };
Now, to use the API in the useTodoState
function of the useTodoState.svelte.js
, we would import the API and replace the localstorage with the API. The key changes include reading the todos from the API when the application starts, and using the API to add, change, and remove todos. The methods would also be asynchronous, as they would need to wait for the API to respond.
import { browser } from "$app/environment";
import * as todoApi from "$lib/apis/todo-api.js";
let todoState = $state([]);
if (browser) {
todoState = await todoApi.readTodos();
}
const useTodoState = () => {
return {
get todos() {
return todoState;
},
add: async (todo) => {
const newTodo = await todoApi.createTodo(todo.name, todo.done);
todoState.push(newTodo);
},
changeDone: async (id) => {
// TODO :)
},
remove: async (id) => {
const removedTodo = await todoApi.deleteTodo(id);
todoState = todoState.filter((todo) => todo.id !== removedTodo.id);
},
};
};
export { useTodoState };
The function changeDone has been left empty — try implementing it yourself.
Finally, when using the methods exposed from the useTodoState
object, we need to remember that they are asynchronous, and we need to use await
when calling them. For example, we would modify the TodoForm.svelte
to use the new API as follows.
<script>
import { useTodoState } from "$lib/states/todoState.svelte.js";
let todoState = useTodoState();
const addTodo = async (e) => {
const todo = Object.fromEntries(new FormData(e.target));
todo.id = crypto.randomUUID();
await todoState.add(todo);
e.target.reset();
e.preventDefault();
};
</script>
<form onsubmit={addTodo}>
<label for="name">Todo</label>
<input id="name" name="name" type="text" placeholder="Enter a new todo" />
<div>
<input id="done" name="done" type="checkbox" />
<label for="done">Done</label>
</div>
<input type="submit" value="Add Todo" />
</form>
Similarly, the TodoItem.svelte
component would also need to be changed to use the new API. The following example shows how the component would be used to remove a todo.
<script>
import { useTodoState } from "$lib/states/todoState.svelte.js";
let todoState = useTodoState();
let { todo } = $props();
</script>
<input
type="checkbox"
onchange={/* do something here*/}
id={todo.id}
/>
<label for={todo.id}>
{todo.name} {todo.id} ({todo.done ? "done" : "not done"})
</label>
<button onclick={async () => await todoState.remove(todo.id)}>Remove</button>
The onchange
event would need to be implemented to change the done status of the todo. Try implementing it yourself.