Nuxt makes sharing data across components a snap. Let’s look at useState, useLocalState and useCookie.
I’ve been obsessed with useState
in Nuxt since the first time I saw it. Other frameworks do not seem to share data as easily (looking at you, React Context). Nuxt has many options to store state, and it definitely makes these things easy. I love it so much, I even wrote versions for other frameworks.
Nuxt is the easiest framework to share data across components. You can use useState
, provide
and inject
, or persist data across cookies with useCookie
. Either way, no other framework makes it this easy.
DO NOT confuse Nuxt’s useState
with React’s useState
. React’s version is much weaker. Nuxt’s useState, on the other hand, is awesome!
Let’s say we want to share data across components. In other frameworks, including Vue itself, you have to prop drill shared data. It is not fun. Context, on the other hand, allows you to share data by setting a “key/value” pair, and using it anywhere. Nuxt
does this automatically.
useState('count', () => 0)
The first parameter is the key. In any framework with a type of Context API you must set a key to get a value. The second parameter is the fallback. The fallback is used when there is no value belonging to that key; however, it is also used to store the initial value itself.
The recommended way to use it is to create a custom hook, or a custom composable.
export function useCount() {
return useState('count', () => 0)
}
This returns a reactive counter that can be used anywhere!
I know what you’re thinking. Why not just use a normal reactive variable?
export function useCount() {
return ref(0)
}
This seems logical, but if you use useCount
in multiple components, the state does not get shared. You will have new counters for each implementation.
// component 1
<script setup lang="ts">
const count = useCount()
</script>
<template>
<p>Count: {{ count }}</p>
<button
@click="
() => {
count++;
}
"
>
+
</button>
</template>
// component 2
<script setup lang="ts">
const count = useCount()
</script>
<template>
<div> {{ count }} <div>
</template>
If we click the button, we want the counter to increment everywhere.
I got so obsessed with the simplicity, I researched the source code.
Nuxt has a global store. You can get the values anywhere using useNuxtApp()
.
export function useGlobalState<T>(key: string, init?: (() => T | Ref<T>)) {
const nuxtApp = useNuxtApp()
const state = toRef(nuxtApp.payload.state, key)
if (state.value === undefined && init) {
const initialValue = init()
state.value = initialValue
}
return state
}
I created a very simplified, but working, version of useState()
. You get the value from the global store, make it reactive and return it. If it doesn’t exist, create it, save it, return it.
export function useCount() {
return useGlobalState('count', () => 0)
}
This will work the exact same way as useState
. Because it runs only once, it is called a singleton.
But what if you need a local version?
Sometimes you don’t want to save every state value to the global store. This can make it hard to do individual component testing, create portable or isolated components, and keep data encapsulated.
Inspired by Angular, you can set and get a context easily in Vue / Nuxt by providing and injecting.
provide(key, value)
inject(key, fallback_value)
Sometimes you may see the patterns:
export function setCount(initialValue: number) {
const count = ref(initialValue)
provide('count', count)
return count
}
export function getCount() {
return inject<Ref<number>>('count');
}
Generally, you set and get the items in different components. However, I don’t see why we can’t do the same pattern as useState
here as well.
export function useLocalState<T>(key: string, init?: (() => T | Ref<T>)) {
const state = inject(key, undefined)
if (state === undefined && init) {
const initialValue = toRef(init())
provide(key, initialValue)
return initialValue
}
return state
}
Now we can set any state locally, just like useState
.
The state must be able to talk to the other state in a line. This means it must be nested. If I create two child components, and I set the first component with the counter, the second component WILL NOT be able to communicate with the other child component. Context is shared hierarchically, not between siblings.
<Count />
<!-- Doesn't have access to count -->
<Count />
However, you can initialize the counter in any parent so that they both have access to it.
<script setup lang="ts">
useLocalState("count", () => 0);
</script>
<template>
<Count />
<Count />
</template>
Now you can use useLocalState('count')
anywhere in the children.
Nuxt also has a magical way to get and set cookies on the frontend just like any other signal with ref
.
const count = useCookie("count");
count.value = 10;
This will save the value to 10
and create the cookie.
You can use the value just like any other reference. The beauty of this is: it will persist! You’re not only saving a global value, you’re saving it to the cookie.
There are a bunch of cookie options you can set, but to set the default value, you set a function in default
just like you would with useState
.
const count = useCookie("count", {
default: () => 10
});
Cookies are a complex creature, but for the sake of explanation, I thought I would create a pseudocode version from the source code. It would require something as complex as the original to work correctly, but you get the gist.
export function useCookie(key, { default: init() }) {
if (server) {
const cookie = getServerCookie()
if (cookie) {
return getCookie(key)
}
const state = init()
setCookie(key, state)
return state
}
// browser
const cookie = getClientCookie()
if (cookie) {
return getCookie(key)
}
const state = init()
setCookie(key, state)
return toRef(state)
// !!! NOT REAL CODE, WILL NOT WORK
Saving data has never been so easy!
Jonathan Gamble has been an avid web programmer for more than 20 years. He has been building web applications as a hobby since he was 16 years old, and he received a post-bachelor’s in Computer Science from Oregon State. His real passions are language learning and playing rock piano, but he never gets away from coding. Read more from him at https://code.build/.