Advanced State Management

Introduction & Lesson Overview

Welcome back! In the last lesson, you explored the Vue component lifecycle and learned how to use lifecycle hooks like onMounted and onUnmounted to manage setup and cleanup in your composables. You also saw how Vue’s watch function works with these hooks to keep your app’s data and side effects in sync, using real examples like the useLocalStorage composable. These concepts are essential for building reliable and efficient Vue applications.

Today, you will take the next step by learning about advanced state management in Vue. As your applications grow, you will often need to share state and logic across multiple components. Managing this shared state in a clean and scalable way is critical for keeping your code organized and your app easy to maintain. In this lesson, you will see how to use composables to create a shared, persistent state for your to-do app and how to connect this state to different parts of your user interface. By the end, you will understand how to build Vue apps that feel seamless and consistent, no matter how many components are involved.

Understanding Shared State In Vue Composables

You have already seen how composables help you organize and reuse logic in Vue. Now, let’s talk about how composables can also be used to manage shared state across your entire app. This is done using a pattern called the singleton pattern.

In Vue, if you define your state (like a ref or reactive object) at the top level of a composable file — outside the composable function itself — this state is created only once. Every time you import and use the composable in different components, they all get access to the same piece of state. This is what makes it a singleton: there is only one instance of the state, and it is shared everywhere.

This approach has several benefits. It allows you to centralize your app’s important data, making it easy to update and access from any component. It also helps you avoid bugs that can occur when different parts of your app have their own copies of the same data. By using a singleton composable, you get reusability, centralized control, and a clear way to manage global state without needing a separate state management library.

Deep Dive Into The useTodos Composable

Let’s look closely at the useTodos composable from your project. This composable is responsible for managing the list of to-dos, the current filter, and several useful computed properties. Here is a simplified version of the code:

import { ref, computed } from 'vue';
import { useLocalStorage } from './useLocalStorage';

// State is defined at the module level, making it a singleton
const todos = ref([]);
const filter = ref('all');

export function useTodos() {
  useLocalStorage('my-todos-app', todos);

  const nextTodoId = computed(() => (todos.value.length ? Math.max(...todos.value.map(t => t.id)) + 1 : 1));
  const filteredTodos = computed(() => {
    if (filter.value === 'active') return todos.value.filter(t => !t.completed);
    if (filter.value === 'completed') return todos.value.filter(t => t.completed);
    return todos.value;
  });
  const activeCount = computed(() => todos.value.filter(t => !t.completed).length);
  const completedCount = computed(() => todos.value.filter(t => t.completed).length);

  function addTodo(todoText) {
    todos.value.push({ id: nextTodoId.value, text: todoText, completed: false });
  }
  function handleToggleComplete(todoId) {
    const todo = todos.value.find(t => t.id === todoId);
    if (todo) todo.completed = !todo.completed;
  }
  function handleDeleteTodo(todoId) {
    todos.value = todos.value.filter(t => t.id !== todoId);
  }

  return { todos, filter, filteredTodos, activeCount, completedCount, addTodo, handleToggleComplete, handleDeleteTodo };
}

Notice how todos and filter are defined outside the useTodos function. This means that every component using useTodos will share the same list of to-dos and filter state. The computed properties like nextTodoId, activeCount, and completedCount automatically update whenever the underlying state changes. For example, activeCount always reflects the number of to-dos that are not completed, and completedCount shows how many have been finished.

The action functions — addTodo, handleToggleComplete, and handleDeleteTodo — let you update the shared state from any component. If you add a new to-do in one part of your app, every other component using useTodos will see the change right away. This is the power of shared, reactive state in Vue.

Implementing Persistent State With useLocalStorage

A key feature of modern web apps is the ability to remember user data even after a page reload. In your to-do app, this is handled by the useLocalStorage composable. When you call useLocalStorage('my-todos-app', todos); inside useTodos, you are telling Vue to keep the todos list in sync with the browser’s localStorage.

Here’s how it works: when your app loads, useLocalStorage checks if there is any saved data in localStorage under the key 'my-todos-app'. If it finds data, it loads it into your todos state. Then, every time you add, complete, or delete a to-do, the composable automatically updates localStorage with the new list. This means your to-dos are always saved, even if you close the browser or refresh the page.

For example, if you add a to-do called "Finish homework" and then reload the page, the to-do will still be there:

Total: 1 | Active: 1 | Completed: 0

This persistence makes your app feel more professional and user-friendly, and it is all handled with just a few lines of code in your composable.

Real-World Application: The TodoSummary Component

Now let’s see how a component can use this shared, persistent state. The TodoSummary component is a simple example that displays the total number of to-dos, as well as how many are active and how many are completed. Here is the code:

<script setup>
import { useTodos } from '../composables/useTodos.js';

const { todos, activeCount, completedCount } = useTodos();
</script>

<template>
  <div class="todo-summary">
    <span>Total: {{ todos.length }}</span> |
    <span>Active: {{ activeCount }}</span> |
    <span>Completed: {{ completedCount }}</span>
  </div>
</template>

When you use useTodos in this component, you get access to the same shared state as every other part of your app. If you add a new to-do or mark one as completed, the summary updates instantly. For example, if you have three to-dos and one is completed, the output will look like this:

Total: 3 | Active: 2 | Completed: 1

This shows how easy it is to build components that reflect the current state of your app, thanks to the singleton pattern in your composable.

Integrating Components In The Main Layout

To make your app feel cohesive, it is helpful to display important information in a central place. In your project, the TodoSummary component is added to the main layout in App.vue, right below the navigation bar. Here is the relevant part of the code:

<template>
  <div id="layout">
    <header>
      <nav>
        <router-link to="/">Home</router-link> |
        <router-link to="/about">About</router-link>
      </nav>
      <!-- Add the summary component to the main layout -->
      <TodoSummary />
    </header>
    <main>
      <router-view />
    </main>
  </div>
</template>

By placing TodoSummary in the layout, you ensure that the summary is always visible, no matter which page the user is on. This is a great example of how a centralized layout can improve the user experience and make important state visible throughout your app. It also demonstrates how easy it is to connect different components to the same shared state using composables.

Recap & Preparation For Practice Exercises

In this lesson, you learned how to manage advanced state in Vue using composables and the singleton pattern. You saw how to set up shared, persistent state in the useTodos composable and how to use computed properties and action functions to keep your app’s data organized and reactive. You also explored how the useLocalStorage composable keeps your data safe across page reloads, and how components like TodoSummary can display up-to-date information by consuming the shared state. Finally, you saw how to integrate these components into your main layout for a seamless user experience.

You are now ready to put these ideas into practice. In the next exercises, you will work with shared state, persistence, and component integration in your own code. This will help you build Vue apps that are robust, maintainable, and user-friendly. Keep going — each step brings you closer to mastering modern Vue development!

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