Client-Side Development

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 in Todos.svelte, but as the list has been also instantiated there, this is ok.

Children and events

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.


Loading Exercise...