Efé's Journey

Persistent rune storage using Svelte 5 and localStorage.

Welcome back, while I was working on my 1st web app, dodoop, I've encountered a mind-boggling issue. I had to make a to-do list, and you can make that by just creating a new array in a state, such as:

let todos = $state([])

The problem with this approach is when the user refreshes the page, the to-do list could be, and will be, gone. If you want to make it "persistent", modern browsers like Chromium and Firefox supports a function called "local storage".

In theory, you could just make it like this, where you can write and get value from the local storage:

if (localStorage.getItem("todos") !== null) {
  localStorage.setItem("todos", [])
}

This, in theory, will check if an item with the key todos exist, and if it doesn't, then it will create it with the default, []. But the only problem is, localStorage only contains key to string values. This means that everything that we define to "key", (in our case to-do) will be stringed.

One common workaround to this problem is using JSON.parse() and JSON.stringify(). The idea here is we literally pass a JSON 'string' to the localStorage. This leads us to:

if (localStorage.getItem("todos") !== null) {
  localStorage.setItem("todos", JSON.stringify([]))
}

let todoList = JSON.parse(localStorage.getItem("todos"))
console.log(todoList)

// Updating the todo list
localStorage.setItem("todos", JSON.stringify(["The new todo", JSON.parse(localStorage.getItem("todos"))]))

Notice how it looks cluttered? Because it is. There are several ways to solve this problem and many other problems. First, if you are using Svelte, you are probably doing something reactive and this approach is not reactive. Instead, we can do something like this:

import { onMount } from 'svelte'

const localItem = (key, initPair) => {
    let val = $state(initPair) //typeOf<Any>

    onMount(() => {
        if (localStorage.getItem("todos") !== null) {
            localStorage.setItem("todos", JSON.stringify(val))
        } else {
            val = localStorage.getItem("todos")
        }
    })

    return {
        get value() {
            return val;
        },
        set value(v) {
            val = v
            if (val != null) {
                localStorage.setItem(key, JSON.stringify(val))
            } else {
                localStorage.removeItem(key)
            }
        }
    }
}

This is mostly a compilation of what I saw on the web and my own code, but if this isn't working for you, I can suggest the userunes's take on this, which is similar to mine. Almost same, even. The difference is this code is understandable for a beginner like me.

This snippet can be used like below:

let todos = localItem("todos", [])

todos.value = ["new item", todos.value]

the "todos" is also reactive, because it is synced with a rune (at least it's how I understood.)

This was mostly a note to myself. I am sure that there is a way better way of doing this instead of my way, but I don't care because I am on my way.

#2025 #dev