S-009 – Gestor de Stores y menú de App, aplicación SPA en Svelte

Al igual que hice en React, tengo intención de hacer una aplicación que contemple páginas publicas, páginas privadas, gestión de acceso, etc.

Soy consciente, que una solución completa conlleva bastante código y que esos ejemplos, para los que se inician no son prácticos, pues con tanto código no se sabe por dónde empezar, pero si que es un buen ejercicio para aquellos que desean realizar un aplicativo completo.

En este artículo se va a utilizar 2 soluciones que se requieren para ese tipo de desarrollo y así, en estos ejemplos espero que sean más sencillos para el aprendizaje de estos concepto.

Objetivo

  1. Utilización de Stores y Memoria local, para guardar datos de sesión. En esta solución nos basaremos para almacenar el TOKEN que acreditará la autorización de acceso de los usuarios. El concepto de Store es mucho más amplio que la autenticación y se puede utilizar en muchos otros aspectos.
    DEMO: https://fhumanes.com/my-store-app/

    Podéis probar con admin/admin o con fernando/1234
  2. Disponer de una solución de menú de aplicación para aplicaciones SPA (sólo una página), que sea muy simple para poderlo adaptar a nuestras necesidades.
    DEMO:
    https://fhumanes.com/my-nav-app/

Solución Técnica

En estos ejemplos no se ha utilizado ningún componente a Svelte, es todo estándar del producto.

¿Qué es un store en Svelte 5?

En Svelte 5, un store es una estructura reactiva que permite compartir y gestionar estado entre distintos componentes de tu aplicación de forma sencilla y eficiente. Es una solución elegante para evitar el paso de props de forma excesiva o el uso de librerías externas para manejar el estado global.

Un store actúa como una fuente de datos reactiva: cuando su valor cambia, todos los componentes que lo usan se actualizan automáticamente.

¿Para qué sirve un store?

Los stores son útiles en múltiples escenarios:

  • Estado global: Ideal para datos que deben estar disponibles en varios componentes, como el usuario autenticado, el tema de la interfaz, o el contenido de un carrito de compras.
  • Comunicación entre componentes: Facilita el intercambio de información entre componentes que no están directamente relacionados.
  • Persistencia de datos: Puedes combinar stores con localStorage o sessionStorage para mantener datos entre sesiones.
  • Reactividad avanzada: Svelte 5 permite usar runes como $state y $derived para crear stores más expresivos y potentes.

El código del Store (como he explicado además del Store se utiliza el «localStorage» => en navegador, para mantener el contenido), es:

import { writable } from 'svelte/store';

// Leer desde localStorage al iniciar
const stored = localStorage.getItem('auth');
const initial = stored
  ? JSON.parse(stored)
  : { isAuthenticated: false, user: null }; // guardamos todos los datos en un solo objeto en formato JSON
                                            // Lo guardamos en "localStorage" porque está en el navegador y no se pierde al recargar la página
export const auth = writable(initial);

// Suscribirse para guardar cambios automáticamente
auth.subscribe(value => {
  localStorage.setItem('auth', JSON.stringify(value));
});

// Función para iniciar sesión
export function login(username, password) {
  const users = [
    { username: 'fernando', password: '1234' },
    { username: 'admin', password: 'admin' }
  ];

  const user = users.find(u => u.username === username && u.password === password);
  if (user) {
    auth.set({ isAuthenticated: true, user: user.username });
    return true;
  }
  return false;
}

// Función para cerrar sesión
export function logout() {
  auth.set({ isAuthenticated: false, user: null });
}

El ejemplo tiene 3 páginas, la de inicio «app.svelte» y una que se presenta cuando no hay identificación «Login.svelte» y otra cuando ya está identificado «Dashboard.svelte».

App.svelte
<script>
  import Login from './Login.svelte';
  import Dashboard from './Dashboard.svelte';
  import { auth } from './stores/auth.js';
  import { onDestroy } from 'svelte';

  let isAuthenticated = false;

  const unsubscribe = auth.subscribe(value => {
    isAuthenticated = value.isAuthenticated;
  });

  onDestroy(unsubscribe);
</script>

{#if isAuthenticated}
  <Dashboard />
{:else}
  <Login />
{/if}

 

Login.svelte
<script>
  import { login } from './stores/auth.js';

  let username = '';
  let password = '';
  let error = '';

  function handleLogin() {
    const success = login(username, password);
    if (!success) {
      error = 'Credenciales incorrectas';
    }
  }
</script>

<h2>Login</h2>
<input bind:value={username} placeholder="Usuario" />
<input bind:value={password} type="password" />
<button on:click={handleLogin}>Entrar</button>
{#if error}
  <p style="color: red">{error}</p>
{/if}
Dashboard.svelte
<script>
  import { auth, logout } from './stores/auth.js';
  let user;

  auth.subscribe(value => {
    user = value.user;
  });
</script>

<h2>Bienvenido, {user}</h2>
<button on:click={logout}>Cerrar sesión</button>

Podéis ver cómo se cargan los valores del Store a través de una función del propio Store «login()», que está en el fichero «login.svelte» y en «App.svelte» y «Dashboard.svelte», cómo se accede a los valores del store con la instrucción de subscribe. Podéis apreciar que se almacena los 2 valores en un localStore construyendo una estructura de información en formato JSON.

¿Qué es una aplicación SPA?

Una SPA (Single Page Application) es una aplicación web que carga una única página HTML y actualiza dinámicamente su contenido sin necesidad de recargar toda la página desde el servidor. Esto se logra mediante JavaScript, que gestiona la navegación interna y el estado de la aplicación.

Características clave de una SPA:

  • Carga inicial única: El navegador descarga todos los recursos esenciales al principio.
  • Navegación rápida: Al cambiar de vista, solo se actualiza el contenido necesario.
  • Experiencia fluida: Se asemeja a una aplicación nativa, sin interrupciones por recargas.
  • Gestión del estado en el cliente: El frontend controla la lógica de navegación y datos.

Frameworks como Svelte, React, Vue y Angular son ideales para construir SPAs.

¿Qué función tienen los menús en una SPA?

Los menús de navegación son esenciales en una SPA porque permiten al usuario moverse entre distintas vistas o secciones sin salir de la página principal.

Funciones principales:

  • Organizar el contenido: Agrupan las funcionalidades o secciones de la app.
  • Facilitar la navegación: Ayudan al usuario a orientarse y acceder rápidamente a lo que necesita.
  • Reflejar el estado actual: Pueden mostrar qué sección está activa o si hay notificaciones pendientes.

Menús adaptativos: Escritorio vs. Móvil

Una SPA moderna debe tener un diseño responsive, lo que implica que los menús se adapten al espacio disponible del navegador o dispositivo.

En el ejemplo, hay 3 tipos de ficheros, la página principal y que es la que se carga al principio «App.svelte», la página de la cabecera «Header.svelte» y las páginas de las distintas opciones.

App.svelte
<script>
    // import { Router, Route } from "svelte-routing";
    import Header from "./components/Header.svelte";
    import Inicio from "./pages/Inicio.svelte";
    import Acerca from "./pages/Acerca.svelte";
    import Contacto from "./pages/Contacto.svelte";
    import Login from "./pages/Login.svelte";

    let basepath = "/my-store-app"; // Ajusta esto según tu configuración de despliegue

    // Para gestión del Menú
    let menuAbierto = false;
    let vistaActual = 'inicio';

    function toggleMenu() {
        menuAbierto = !menuAbierto;
    }

    function cambiarVista(vista) {
        vistaActual = vista;
        menuAbierto = false;
    }
</script>

  <Header {menuAbierto} {vistaActual} {toggleMenu} {cambiarVista} />

<main> 
    {#if vistaActual === 'inicio'}
        <Inicio {cambiarVista} />
    {:else if vistaActual === 'acerca'}
        <Acerca {cambiarVista} />
    {:else if vistaActual === 'contacto'}
        <Contacto {cambiarVista} />
    {:else if vistaActual === 'login'}
        <Login {cambiarVista} />
    {/if}
</main> 
  
<style>
  main {
    margin-top: 60px;
    padding: 1rem;
  }
</style>
Header.svelte
<script>
let  {menuAbierto, vistaActual, toggleMenu, cambiarVista} = $props(); // Recibimos las props desde nav.svelte
</script>

<style>
  /* ✅ Corrección global para evitar desbordamientos */
  * {
    box-sizing: border-box;
  }

  nav {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    background-color: #333;
    color: white;
    padding: 0.5rem 0.5rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    z-index: 1000;
  }

  .logo {
    font-weight: bold;
    font-size: 1.2rem;
  }

  .menu-toggle {
    font-size: 1.5rem;
    cursor: pointer;
    background: none;
    border: none;
    color: white;
  }

  ul {
    list-style: none;
    display: flex;
    gap: 1rem;
    margin: 0;
    padding: 0;
  }

  li {
    display: flex;
  }

  button.nav-btn {
    background: none;
    border: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
    padding: 0.3rem 0.6rem;
    border-radius: 4px;
    text-align: left;
  }

  button.nav-btn:hover {
    background-color: #555;
  }

  button.nav-btn.activo {
    background-color: #007acc;
  }

  @media (max-width: 768px) {
    ul {
      flex-direction: column;
      align-items: flex-start;
      padding-left: 1rem;
      position: absolute;
      top: 100%;
      left: 0;
      width: 100%;
      background-color: #444;
      display: none;
    }

    ul.abierto {
      display: flex;
    }
  }

  .menu-toggle {
    display: none;
    }
  @media (max-width: 768px) {
    .menu-toggle {
        display: block;
    }
  }
</style>

<nav>
  <div class="logo">MiApp</div>
  <button class="menu-toggle" onclick={toggleMenu} aria-label="Abrir menú" type="button">
    ☰
  </button>
  <ul class:abierto={menuAbierto}>
    <li>
      <button
        class="nav-btn"
        class:activo={vistaActual === 'inicio'}
        onclick={() => cambiarVista('inicio')}
      >
        Inicio
      </button>
    </li>
    <li>
      <button
        class="nav-btn"
        class:activo={vistaActual === 'acerca'}
        onclick={() => cambiarVista('acerca')}
      >
        Acerca
      </button>
    </li>
    <li>
      <button
        class="nav-btn"
        class:activo={vistaActual === 'contacto'}
        onclick={() => cambiarVista('contacto')}
      >
        Contacto
      </button>
    </li>
    <li>
      <button
        class="nav-btn"
        class:activo={vistaActual === 'login'}
        onclick={() => cambiarVista('login')}
      >
        Ingresar
      </button>
    </li>
  </ul>
</nav>
Acerca.svelte
<p>Bienvenido a la página de Acerca de.</p>

Cómo podéis apreciar desde APP.svelte, se pasan parámetros a Header.svelte y este, a su vez, utilizando una función, se pasa la selección de la opción del menú a App.svelte. Es algo que ya hemos visto en el artículo anterior.

Como siempre, os dejo los fuentes para que los instaléis y hagáis los cambios que consideréis oportunos.

Adjuntos

Archivo Tamaño de archivo Descargas
zip my-store-app 17 KB 1
zip my-nav-app 18 KB 2

Blog personal para facilitar soporte gratuito a usuarios de React y PHPRunner