Wat is localStorage?

localStorage is een ingebouwde browser-feature die strings opslaat die bewaard blijven na een refresh of een herstart van de browser.

  • Gebruikersnaam onthouden
  • Dark mode voorkeur
  • Todo-lijst die niet verdwijnt bij refresh
  • Login-token bewaren (voor demo's — voor productie liever HTTP-only cookies)

Belangrijk om te weten:

  • Strings only — voor objecten gebruik je JSON.stringify()
  • Per domein — elke site heeft zijn eigen localStorage
  • ±5MB ruimte — meer dan genoeg voor normale data
  • Niet veilig voor gevoelige data — gebruiker kan het lezen via devtools

Lezen & schrijven — de basis

Opslaan

localStorage.setItem('username', 'Anna')

Lezen

const name = localStorage.getItem('username')
console.log(name)   // 'Anna' (of null als er niks staat)

Verwijderen

localStorage.removeItem('username')

// Of alles wissen:
localStorage.clear()

Let op: getItem() geeft null terug als de key niet bestaat. Houd daar rekening mee met een fallback:

const name = localStorage.getItem('username') || 'Gast'

Objecten & arrays opslaan

localStorage slaat alleen strings op. Voor objecten en arrays gebruik je JSON.stringify() (object → string) en JSON.parse() (string → object).

Opslaan

const user = { name: 'Anna', age: 25 }
localStorage.setItem('user', JSON.stringify(user))

const todos = ['Boodschappen', 'Vue leren']
localStorage.setItem('todos', JSON.stringify(todos))

Lezen

const stored = localStorage.getItem('user')
const user = stored ? JSON.parse(stored) : null
console.log(user.name)   // 'Anna'

const todosString = localStorage.getItem('todos')
const todos = todosString ? JSON.parse(todosString) : []

Beginnersfout:

localStorage.setItem('user', user)              // ❌
// Opgeslagen als de string "[object Object]"

Altijd JSON.stringify voor objecten en arrays.

Patroon in Vue — lezen bij start, schrijven bij wijziging

In Vue kombineer je localStorage met refs op twee plekken:

  1. Bij setup: lees de opgeslagen waarde en zet hem in de ref
  2. Bij wijziging: gebruik een watch die naar localStorage schrijft
<script setup>
import { ref, watch } from 'vue'

// 1. Lees bij start (of gebruik default)
const username = ref(localStorage.getItem('username') || '')

// 2. Schrijf bij elke wijziging
watch(username, (newName) => {
  localStorage.setItem('username', newName)
})
</script>

<template>
  <input v-model="username" />
  <p>Hallo, {{ username || 'Gast' }}!</p>
</template>

Wat heb je nu? Typ in het inputveld → de waarde wordt automatisch opgeslagen. Refresh de pagina → de waarde staat er nog. Pure two-way sync tussen je ref en de browser-storage.

Compleet voorbeeld — Todo-app

Een todo-lijst die persistent is. Items blijven staan na een refresh.

<script setup>
import { ref, watch } from 'vue'

// Lees bij start (parse JSON)
const stored = localStorage.getItem('todos')
const todos = ref(stored ? JSON.parse(stored) : [])

const input = ref('')

const addTodo = () => {
  if (input.value.trim()) {
    todos.value.push({
      id: Date.now(),
      text: input.value,
      done: false
    })
    input.value = ''
  }
}

const removeTodo = (id) => {
  todos.value = todos.value.filter(t => t.id !== id)
}

// Sync naar localStorage bij elke wijziging (deep voor object-mutaties)
watch(todos, (newTodos) => {
  localStorage.setItem('todos', JSON.stringify(newTodos))
}, { deep: true })
</script>

<template>
  <form @submit.prevent="addTodo">
    <input v-model="input" placeholder="Nieuwe taak..." />
    <button type="submit">Voeg toe</button>
  </form>

  <ul>
    <li v-for="todo in todos" :key="todo.id">
      <input v-model="todo.done" type="checkbox" />
      <span :class="{ done: todo.done }">{{ todo.text }}</span>
      <button @click="removeTodo(todo.id)">×</button>
    </li>
  </ul>
</template>

<style scoped>
.done { text-decoration: line-through; color: #999; }
</style>

Let op { deep: true }: zonder dit detecteert de watcher alleen als todos.value wordt vervangen — niet als je binnen een todo de done aanvinkt. Met deep kijkt hij ook naar geneste wijzigingen.

Als composable — herbruikbaar

Doe je dit patroon vaker? Trek het uit naar een composable:

// src/composables/useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const initial = stored !== null ? JSON.parse(stored) : defaultValue

  const value = ref(initial)

  watch(value, (newVal) => {
    localStorage.setItem(key, JSON.stringify(newVal))
  }, { deep: true })

  return value
}

Gebruik

<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'

const username = useLocalStorage('username', '')
const todos = useLocalStorage('todos', [])
const isDark = useLocalStorage('dark-mode', false)
</script>

Eén regel per "ding dat persistent moet zijn." Of gebruik useLocalStorage uit VueUse — dezelfde API, al af.

Belangrijke Regels

  • Alleen strings — gebruik JSON.stringify voor objecten/arrays
  • getItem kan null geven — altijd een fallback
  • Lezen bij setup (in de ref initialisatie), schrijven via watch
  • Voor objecten/arrays: { deep: true } in de watcher
  • Niet voor gevoelige data — gebruiker kan het lezen via devtools

Veelgemaakte Fouten

Fout — object opslaan zonder stringify:

const user = { name: 'Anna' }
localStorage.setItem('user', user)            // ❌
console.log(localStorage.getItem('user'))     // "[object Object]"

Goed:

localStorage.setItem('user', JSON.stringify(user))
const stored = JSON.parse(localStorage.getItem('user'))

Fout — null niet afvangen bij parse:

const user = JSON.parse(localStorage.getItem('user'))
// ❌ als de key niet bestaat: JSON.parse(null) → null (toevallig OK)
// Maar bij andere ongeldige strings → SyntaxError

Goed — check eerst:

const stored = localStorage.getItem('user')
const user = stored ? JSON.parse(stored) : null

Fout — vergeten deep: true voor object-refs:

const todos = ref([])

watch(todos, (val) => {
  localStorage.setItem('todos', JSON.stringify(val))
})
// ❌ todos.value.push(...) wordt NIET gedetecteerd

Goed:

watch(todos, (val) => {
  localStorage.setItem('todos', JSON.stringify(val))
}, { deep: true })   // ✅