Introducing State Management with Runes

Introducing State Management with Runes

Welcome to the second course in our "Building A Kanban Board" series! In our previous course, we built the foundational UI components for our Kanban board: the main layout, columns, and task cards.

Now it’s time to make our board fully functional by connecting it to dynamic data using Svelte's Runes API.

Bridging from UI to State

While our components from the previous course look great, they're currently displaying static, hardcoded tasks. In a real Kanban application, we need:

  1. A central place to store all our tasks
  2. A way to filter tasks by status (To Do, In Progress, Done)
  3. The ability to move tasks between columns
  4. A reactive system that updates the UI when tasks change

That’s where state management comes in — and Svelte’s Runes provide an elegant solution.

What is State Management

State management is a fundamental concept in modern web applications. It refers to how we organize, store, and update data that changes over time in our application. For our Kanban board, we'll need to manage tasks that can move between different columns (To Do, In Progress, and Done), and we'll need our UI to update automatically when this data changes.

Svelte 5 introduces a new way to handle reactivity called Runes. Runes are special functions prefixed with a dollar sign ($) that enhance variables with special behaviors. They replace the older reactive syntax in previous Svelte versions and provide a more intuitive way to work with reactive state.

In this lesson, we'll focus on creating a task store that will serve as the central data repository for our Kanban board application. This store will:

  1. Hold our collection of tasks.
  2. Provide filtered lists of tasks for each column.
  3. Expose functions to access these lists from our components.
  4. Connect this accessor functions to our board component

Let's get started by exploring how to create reactive state with Svelte 5's Runes!

Creating Reactive State with `$state`

The first step in building our task store is to create a reactive state variable that will hold our collection of tasks. For that we use the $state rune to make a reactive variable.

Here's how we'll create our initial tasks array:

// src/lib/taskStore.svelte.js
export const tasks = $state([
  { id: 1, title: 'Learn Svelte 5', description: 'Study the new Runes API', status: 'todo' },
  { id: 2, title: 'Design components', description: '', status: 'todo' },
  { id: 3, title: 'Build Kanban board', description: 'Create the main layout', status: 'inprogress' },
  { id: 4, title: 'Setup project', description: 'Initialize SvelteKit', status: 'done' }
]);

Let's break down what's happening here:

  • We're creating a file called taskStore.svelte.js in the src/lib directory.
  • We're using the $state rune to make our tasks array reactive.
  • We're initializing it with four sample tasks.
  • Each task has an id, title, description, and status property.
  • The status property determines which column the task belongs to.

When we use $state, Svelte automatically tracks any changes to this variable. If we add, remove, or modify tasks in this array, any components that depend on this data will automatically update.

For example, if we were to add a new task:

tasks.push({ 
  id: 5, 
  title: 'Add drag and drop', 
  description: 'Implement drag and drop functionality', 
  status: 'todo' 
});

Svelte would detect this change and update any UI elements that display our tasks. This is the power of reactivity in Svelte — we don't need to manually trigger UI updates when our data changes.

Filtering Tasks with `$derived`

Now that we have our tasks array, we need a way to filter tasks based on their status. This will allow us to display the right tasks in each column of our Kanban board.

In Svelte, we can use the $derived rune to create values that are computed from other reactive values. When the source values change, the derived values are automatically recalculated.

Let's add filtered task lists for each column:

const todoTasks = $derived(tasks.filter(task => task.status === 'todo'));
const inProgressTasks = $derived(tasks.filter(task => task.status === 'inprogress'));
const doneTasks = $derived(tasks.filter(task => task.status === 'done'));

Here, we're creating three derived values:

  • todoTasks: Contains all tasks with a status of 'todo'.
  • inProgressTasks: Contains all tasks with a status of 'inprogress'.
  • doneTasks: Contains all tasks with a status of 'done'.

These derived values will automatically update whenever the tasks array changes. For example, if we move a task from 'todo' to 'inprogress' by changing its status, the todoTasks and inProgressTasks arrays will automatically update to reflect this change.

The beauty of using $derived is that we don't need to manually recalculate these filtered lists. Svelte handles this for us, ensuring our UI always shows the most up-to-date data.

Exporting State and Accessor Functions

The final step in building our task store is to export functions that allow other parts of our application to access our filtered task lists.

In Svelte, we can't export $derived values directly. There are several important reasons for this limitation:

  1. Reactivity Boundaries: Svelte's reactivity system is designed to work within component boundaries. When you export a derived value directly, it can break the reactivity chain as it crosses these boundaries.

  2. Referential Stability: Direct exports might create new object references when used outside their original context, potentially causing unnecessary re-renders.

  3. Implementation Details: The Runes API uses internal mechanisms to track dependencies between reactive values. Exporting these values directly could expose internal implementation details that might change in future versions.

  4. Encapsulation: By using accessor functions, we create a cleaner API that hides the implementation details of our state management. This allows us to change how we store and derive our data without affecting the code that uses it.

Instead, we use accessor functions that return these values:

export const getTodoTasks = () => todoTasks;
export const getInProgressTasks = () => inProgressTasks;
export const getDoneTasks = () => doneTasks;

These functions simply return our filtered task lists. Components can call these functions to get the tasks they need to display.

For example, a component that displays the "To Do" column might use getTodoTasks() to get the list of tasks it needs to render.

Updating the Board Component to Use Dynamic Data

Now let’s connect the Board component to the task store. Since the Column and TaskCard components are already set up, we only need to replace the hardcoded tasks in the Board component with dynamic data.

Here’s the updated Board.svelte:

<script>
  import Column from './Column.svelte';
  import TaskCard from './TaskCard.svelte';
  import { getTodoTasks, getInProgressTasks, getDoneTasks } from '$lib/taskStore.svelte.js';
</script>

<div class="board">
  <Column title="To Do">
    {#each getTodoTasks() as task (task.id)}
      <TaskCard title={task.title} description={task.description} />
    {/each}
  </Column>

  <Column title="In Progress">
    {#each getInProgressTasks() as task (task.id)}
      <TaskCard title={task.title} description={task.description} />
    {/each}
  </Column>

  <Column title="Done">
    {#each getDoneTasks() as task (task.id)}
      <TaskCard title={task.title} description={task.description} />
    {/each}
  </Column>
</div>

<style>
  .board {
    display: flex;
    gap: 1rem;
    justify-content: center;
  }
  
  @media (max-width: 950px) {
    .board {
      flex-direction: column;
    }
  }
</style>

State Management Best Practices

  • Local Component State: Use local $state variables within a component when the state is only relevant to that single component.
  • Shared Store State: Use a dedicated store module when multiple components need access to the same data.
  • Atomic Updates: Keep state update functions focused on a single task.
  • Validation: Add validation in your state management functions to ensure data integrity.
  • Error Handling: Validate inputs and provide clear errors when adding or modifying tasks.
  • Performance Considerations: Minimize unnecessary component re-renders and batch updates when possible.

Summary and Next Steps

In this lesson, we've learned how to create a task store using Svelte's Runes API. Let's recap what we've covered:

  1. We used the $state rune to create a reactive tasks array that will automatically trigger UI updates when modified.
  2. We used the $derived rune to create filtered task lists for each column of our Kanban board.
  3. Connected the Board component to display dynamic tasks using accessor functions.

This task store will serve as the foundation for our Kanban board application. In the upcoming lessons, we'll build on this foundation by:

  • Creating the state to the components to display our tasks dynamically.
  • Adding a form to create new tasks.
  • Implementing functionality to move tasks between columns.

In the practice exercises that follow, you'll get hands-on experience working with Svelte 5's Runes. You'll create your own reactive state, derive values from that state, and build functions to modify your state. These exercises will help reinforce the concepts we've covered in this lesson and prepare you for the more complex state management patterns we'll explore in future lessons.

Remember, the key to mastering state management is practice. Don't worry if these concepts feel new or challenging at first — as you work through the exercises and build more of the application, they'll become more familiar and intuitive.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal