Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Shared state: useState

Sometimes the same data is needed in different parts of the site: the signed-in user's name appears both in the header and on the profile page. useState creates such a shared value under a key. All components that use the same key get the same reactive ref.

A minimal example

<!-- app/pages/index.vue -->
<script setup lang="ts">
const visits = useState('visits', () => 0)
</script>
 
<template>
  <section>
    <h2>Home</h2>
    <button @click="visits++">Clicked: {{ visits }}</button>
  </section>
</template>

The first argument is the key, the second is a function that sets the initial value. Click the button a few times, go to /about and come back: the counter keeps its value, because it is stored outside the component.

A composable with shared state

It is handy to hide the key in a composable so you do not repeat it in every file. Create app/composables/useCurrentUser.ts:

// app/composables/useCurrentUser.ts
export function useCurrentUser() {
  return useState<string | null>('current-user', () => null)
}

The type in angle brackets says the value is a string, or null when nobody is signed in.

Using it in two places

The layout header shows the name:

<!-- app/layouts/default.vue -->
<script setup lang="ts">
const user = useCurrentUser()
</script>
 
<template>
  <div>
    <header>
      <NuxtLink to="/">Home</NuxtLink>
      <span v-if="user">Hi, {{ user }}</span>
      <NuxtLink v-else to="/login">Sign in</NuxtLink>
    </header>
    <slot />
  </div>
</template>

And the login page changes it:

<!-- app/pages/login.vue -->
<script setup lang="ts">
const user = useCurrentUser()
const name = ref('')
 
function login() {
  user.value = name.value
  navigateTo('/')
}
</script>
 
<template>
  <form @submit.prevent="login">
    <input v-model="name" placeholder="Your name">
    <button>Sign in</button>
  </form>
</template>

Enter "Anna" at /login and press "Sign in": the header shows "Hi, Anna". This is not a real login yet: after a page reload the name is gone. Signing in with a password check is covered in the authentication lesson.

Why not a plain ref

ApproachWhat happens
ref inside a componentEach component has its own value
ref in a file outside a functionOn the server, one value for all visitors
useStateA separate value per request, passed from the server to the browser

A value set on the server is sent to the browser together with the HTML, so it is not lost after hydration.

Common mistake

Declaring shared state like export const user = ref(null) in a separate file. It works in the browser, but on the server such a ref is one for all visitors at once: a name saved during Anna's request is also seen by Oleg. Use useState or Pinia for shared data. And keep only plain data in useState, such as strings, numbers, arrays and objects, but not functions or class instances: they cannot be sent to the browser.

Thanks for reading!

Donate to the site — we work hard for you.