Nesting Components and Passing Properties
Learning Objectives
- You know how to use components within components.
- You know how to pass properties to components.
At the end of the last chapter, we had a component that could be used to add todos to a list, to modify the todo state of todos, and to remove todos from the list. The component looked as follows.
<script>
let todos = $state([]);
const addTodo = (e) => {
const todo = Object.fromEntries(new FormData(e.target));
todo.id = crypto.randomUUID();
todos.push(todo);
e.target.reset();
e.preventDefault();
};
const removeTodo = (todo) => {
todos = todos.filter((t) => t.id !== todo.id);
};
</script>
<h1>Todos</h1>
<h2>Add Todo</h2>
<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>
<h2>Existing todos</h2>
<ul>
{#each todos as todo}
<li>
<input type="checkbox" bind:checked={todo.done} id={todo.id} />
<label for={todo.id}>
{todo.name} ({todo.done ? "done" : "not done"})
</label>
<button onclick={() => removeTodo(todo)}>Remove</button>
</li>
{/each}
</ul>
Although we did not explicitly label the component when we worked on it, we will call it Todos.svelte
for simplicity. The component has quite a bit of code, and it could be broken down to smaller components. As an example, we could create a separate component for the form that is used to add todos and a separate component for showing a todo item in the list of todos.
In this case, we would have the following three components:
Todos.svelte
— the main component that contains the list of todos and the form for adding todos.TodoForm.svelte
— a component that contains the form for adding todos.TodoItem.svelte
— a component that displays a single todo item in the list of todos.
Nesting components
Svelte applications are often created by nesting components, or in other words, by placing components within other components. This allows for building complex user interfaces by combining smaller components, which can make the codebase easier to understand and to maintain.
As an example, we can create a new component called TodoForm.svelte
and extract the form and functionality for adding todos from Todos.svelte
to the new component. The TodoForm.svelte
component could look as follows.
<script>
const addTodo = (e) => {
const todo = Object.fromEntries(new FormData(e.target));
todo.id = crypto.randomUUID();
todos.push(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>
As you might notice, the function addTodo
above references a variable todos
that is not defined in the component. The variable is defined in the Todos.svelte
component, and we’ll get back to this in a moment.
The Todos.svelte
component would then import the TodoForm.svelte
component and use it in the template area. After the modification, the Todos.svelte
component would look as follows.
<script>
import TodoForm from "./TodoForm.svelte";
let todos = $state([]);
const removeTodo = (todo) => {
todos = todos.filter((t) => t.id !== todo.id);
};
</script>
<h1>Todos</h1>
<h2>Add Todo</h2>
<TodoForm />
<h2>Existing todos</h2>
<ul>
{#each todos as todo}
<li>
<input type="checkbox" bind:checked={todo.done} id={todo.id} />
<label for={todo.id}>
{todo.name} ({todo.done ? "done" : "not done"})
</label>
<button onclick={() => removeTodo(todo)}>Remove</button>
</li>
{/each}
</ul>
Now, the application is structured in a way that the form for adding todos is a separate component. Let’s continue with the decomposition — or breaking the component into smaller components — and create a new component called TodoItem.svelte
that displays a single todo item in the list of todos and allows removing a todo.
Or, more specifically, it tries to allow removing a todo, as we’ll soon learn.
The TodoItem.svelte
component could look as follows.
<script>
const removeTodo = (todo) => {
todos = todos.filter((t) => t.id !== todo.id);
};
</script>
<input type="checkbox" bind:checked={todo.done} id={todo.id} />
<label for={todo.id}>
{todo.name} ({todo.done ? "done" : "not done"})
</label>
<button onclick={() => removeTodo(todo)}>Remove</button>
With the TodoItem.svelte
component, we can modify the Todos.svelte
component to use the new component. After the change, the Todos.svelte
would look as follows.
<script>
import TodoForm from "./TodoForm.svelte";
import TodoItem from "./TodoItem.svelte";
let todos = $state([]);
</script>
<h1>Todos</h1>
<h2>Add Todo</h2>
<TodoForm />
<h2>Existing todos</h2>
<ul>
{#each todos as todo}
<li>
<TodoItem />
</li>
{/each}
</ul>
At this point, the form does not work, and we are not seeing any todos in the list. Let’s start fixing the issue.
Component properties
Components can be passed information through properties. Properties are values that are passed to a component when it is used. In Svelte, component properties are declared in the script area of a template. They are declared using let { name } = $props();
, where name
is the name of the property. If there are multiple properties, they are separated using commas.
For example, for the TodoForm.svelte
, we could declare a property called todos
that would be passed from the Todos.svelte
component. The TodoForm.svelte
component would then look as follows.
<script>
let { todos } = $props();
const addTodo = (e) => {
const todo = Object.fromEntries(new FormData(e.target));
todo.id = crypto.randomUUID();
todos.push(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>
Now, the component has a property todos
that is used in the addTodo
function.
The todos
property needs to be passed to the TodoForm.svelte
component from the Todos.svelte
component. Passing a property works by adding an attribute to the component to which the property is passed to, using the property name as the attribute name and the passed value as the attribute value.
As an example, if we would pass the todos
property to the TodoForm.svelte
component, the Todos.svelte
component would look as follows.
<script>
import TodoForm from "./TodoForm.svelte";
import TodoItem from "./TodoItem.svelte";
let todos = $state([]);
</script>
<h1>Todos</h1>
<h2>Add Todo</h2>
<TodoForm {todos} />
<h2>Existing todos</h2>
<ul>
{#each todos as todo}
<li>
<TodoItem />
</li>
{/each}
</ul>
Now, the TodoForm.svelte
component has access to the todos
property, and we can add todos to the list. Adding todos to the list does not work, however, as the TodoItem.svelte
component is presently broken.
The line
<TodoForm {todos} />
is equivalent to<TodoForm todos={todos} />
.
Even though the TodoItem.svelte
component is broken, we can test the functionality for adding todo items by removing the TodoItem.svelte
component from Todos.svelte
, and listing e.g. the ids of the created todos in the list.
<script>
import TodoForm from "./TodoForm.svelte";
import TodoItem from "./TodoItem.svelte";
let todos = $state([]);
</script>
<h1>Todos</h1>
<h2>Add Todo</h2>
<TodoForm todos={todos} />
<h2>Existing todos</h2>
<ul>
{#each todos as todo}
<li>
{todo.id}
</li>
{/each}
</ul>
Next, we can pass the todo
property to the TodoItem.svelte
component. The TodoItem.svelte
component would then look as follows.
<script>
let { todo } = $props();
const removeTodo = (todo) => {
todos = todos.filter((t) => t.id !== todo.id);
};
</script>
<input type="checkbox" bind:checked={todo.done} id={todo.id} />
<label for={todo.id}>
{todo.name} ({todo.done ? "done" : "not done"})
</label>
<button onclick={() => removeTodo(todo)}>Remove</button>
Next, we need to pass the todo
item as a property to the TodoItem.svelte
component from Todos.svelte
. After the change, the Todos.svelte
would be as follows.
<script>
import TodoForm from "./TodoForm.svelte";
import TodoItem from "./TodoItem.svelte";
let todos = $state([]);
</script>
<h1>Todos</h1>
<h2>Add Todo</h2>
<TodoForm {todos} />
<h2>Existing todos</h2>
<ul>
{#each todos as todo}
<li>
<TodoItem {todo} />
</li>
{/each}
</ul>
Now, adding and listing todos works. However, when we try to remove a todo, removing the todo does not work.
Failed intuition in passing down a property
Let’s first approach the problem by passing the list of todos to the TodoItem.svelte
component. This seems intuitive, but as we soon observe, it does not work.
To pass the list of todos from the Todos.svelte
component to the TodoItem.svelte
component, we would modify the Todos.svelte
component as follows.
<script>
import TodoForm from "./TodoForm.svelte";
import TodoItem from "./TodoItem.svelte";
let todos = $state([]);
</script>
<h1>Todos</h1>
<h2>Add Todo</h2>
<TodoForm {todos} />
<h2>Existing todos</h2>
<ul>
{#each todos as todo}
<li>
<TodoItem {todo} {todos} />
</li>
{/each}
</ul>
And then modify the TodoItem.svelte
component to accept the todos
property.
<script>
let { todo, todos } = $props();
const removeTodo = (todo) => {
todos = todos.filter((t) => t.id !== todo.id);
};
</script>
<input type="checkbox" bind:checked={todo.done} id={todo.id} />
<label for={todo.id}>
{todo.name} ({todo.done ? "done" : "not done"})
</label>
<button onclick={() => removeTodo(todo)}>Remove</button>
Now, when we try to remove a todo, the todo is not removed. In addition, when we open up the browser console, we see the following notification.
[svelte] ownership_invalid_mutation
src/lib/components/TodoForm.svelte mutated a value owned by
src/lib/components/Todos.svelte. This is strongly discouraged.
Consider passing values to child components with `bind:`, or
use a callback instead.
The notification is a warning from Svelte that the TodoItem.svelte
component is trying to modify a value that is owned by the Todos.svelte
component. The todos
variable is defined in the Todos.svelte
component, and it should be modified only in the Todos.svelte
component.
In effect, what is happening here is that the TodoItem.svelte
assigns a new value to the todos
variable which it receives as a property. When the new value is assigned to the todos variable, the todos
variable in TodoItem.svelte
is no longer the same variable as the todos
variable in the Todos.svelte
component.
Thus, even though the list changes in the TodoItem.svelte
component, the list in the Todos.svelte
component remains the same, and the user interface is not updated.
Passing a function as a property
In addition to variables, it is possible to pass down functions as properties. This allows for child components to call functions in parent components.
Functions are first-class citizens in JavaScript, and they can be passed to other functions, assigned to variables, and so on.
Let’s modify the Todos.svelte
component so that it contains the removeTodo
function and that it passes the function to the TodoItem.svelte
component. After the mofication, the Todos.svelte
component looks as follows.
<script>
import TodoForm from "./TodoForm.svelte";
import TodoItem from "./TodoItem.svelte";
let todos = $state([]);
const removeTodo = (todo) => {
todos = todos.filter((t) => t.id !== todo.id);
};
</script>
<h1>Todos</h1>
<h2>Add Todo</h2>
<TodoForm {todos} />
<h2>Existing todos</h2>
<ul>
{#each todos as todo}
<li>
<TodoItem {todo} {removeTodo} />
</li>
{/each}
</ul>
Similarly, we modify the TodoItem.svelte
component to accept the removeTodo
function as a property. After the change, the component looks as follows.
<script>
let { todo, removeTodo } = $props();
</script>
<input type="checkbox" bind:checked={todo.done} id={todo.id} />
<label for={todo.id}>
{todo.name} ({todo.done ? "done" : "not done"})
</label>
<button onclick={() => removeTodo(todo)}>Remove</button>
Now, when we try to remove a todo, the removeTodo
function from Todos.svelte
is called, and the todo is removed from the list.
Technically, the
todos
list is recreated and reassigned also inTodos.svelte
, but as the list has been also instantiated there, this is ok.
To summarize, child components can communicate with parent components by using functions passed as properties. The child component can then call the function whenever an event happens.