Learn how useState works in Nuxt by building your own version from scratch. This article explores state management and hydration in Nuxt, helping you understand the inner workings of this essential composable.
Get notified when we release new tutorials, lessons, and other expert Nuxt content.
Do you know how useState
in Nuxt works?
You probably use this composable all the time, but have you ever taken the time to understand how it works?
In this article, we'll build our own simple version of useState
, step by step.
You’ll gain insight into the fundamentals of state management and hydration in Nuxt.
By the end, you’ll be among the few who can break down and recreate this foundational composable.
Let’s dive into the inner workings of useState and see what it takes to make it yourself.
Okay, so we'll start with just scaffolding out a basic state management composable as our starting point:
import { ref, type Ref } from 'vue';
// Super basic, just returning a ref
export function useMyState<T>(key: string): Ref<T> {
const state = ref<T>(null as T);
return state as Ref<T>;
}
Here, we're not even using the key
parameter, but we will in a second.
We're just creating a ref
, and then using TypeScript generics to type it correctly as T
. This lets us use the composable like this:
const myState = useMyState<number>('count');
myState.value = 34;
Next, we'll add in the ability to pass in a init
function so we can initialize the value:
import { ref, type Ref } from 'vue';
// Now we can use a function to initialize the state
export function useMyState<T>(
key: string,
init?: () => T
): Ref<T> {
const initialValue = init ? init() : null;
const state = ref<T>(initialValue as T);
return state as Ref<T>;
}
This init
parameter is optional. If we have it, we run it and use the returned value to initialize our state
ref.
You might be wondering why we use a function instead of a direct value, and this has two answers:
useState
does, and we're re-building a version of itThis new version can be used like this:
const myState = useMyState<number>('count', () => 0);
console.log(myState.value) // 0
Now that we've laid the groundwork, it's time to add in the interesting stuff: support for hydration! First, we'll tackle saving the server generated value and sending it over to the client.
With Nuxt, this is actually pretty straightforward, because of how extensible Nuxt is:
import { ref, type Ref } from 'vue';
// Add in hydration support using payload and nuxtApp
// But it only supports primitives and plain objects
export function useMyState<T>(
key: string,
init?: () => T
): Ref<T> {
// Already scoped per request
const nuxtApp = useNuxtApp();
// Save initialized value to payload
const initialValue = init ? init() : null;
if (import.meta.server) {
if (!nuxtApp.payload.state[key]) {
nuxtApp.payload.state[key] = initialValue;
}
}
const state = ref<T>(initialValue as T);
return state as Ref<T>;
}
On the server, we need to initialize the value and then save it somewhere. The place we'll save it is the Nuxt payload
, a special object that gets sent to the client alongside the HTML of the page.
First, we get the current Nuxt instance with useNuxtApp
:
const nuxtApp = useNuxtApp();
Then, if we're on the server, we save the initialized value to this payload
object:
// Initialize the value
const initialValue = init ? init() : null;
// Check if we're on the server
if (import.meta.server) {
// Save the value if we haven't yet
if (!nuxtApp.payload.state[key]) {
nuxtApp.payload.state[key] = initialValue;
}
}
We have this check in there to make sure that the value hasn't already been initialized, which is important.
One of the features of useState
(and useMyState
) is that it can be used as a global state management tool. You can use it to access the same value in many different components or composables:
// ComponentA initializes to zero
const myState = useMyState<number>('count', () => 10);
// ComponentB just reads the value without overriding
const myState = useMyState<number>('count');
Once we're in the client, we have to then hydrate this value back from the payload
back into the useMyState
composable so it can be used in our component.
We've got the first half of the hydration equation, now we just need to get the state back into our useMyState
composable:
import { ref, type Ref } from 'vue';
// Add in hydration support using payload and nuxtApp
// But it only supports primitives and plain objects
export function useMyState<T>(
key: string,
init?: () => T
): Ref<T> {
// Already scoped per request
const nuxtApp = useNuxtApp();
// Save initialized value to payload
const initialValue = init ? init() : null;
if (import.meta.server) {
if (!nuxtApp.payload.state[key]) {
nuxtApp.payload.state[key] = initialValue;
}
}
const state = ref<T>(initialValue as T);
// Restore state on client-side
if (import.meta.client) {
if (nuxtApp.payload.state[key] !== undefined) {
state.value = nuxtApp.payload.state[key];
}
}
return state as Ref<T>;
}
Here, we're essentially just going in the reverse direction.
We get the current Nuxt instance, which gives us access to the payload
, just like before. But when we're on the client, we read the value from the payload
and use it to set our ref
:
state.value = nuxtApp.payload.state[key];
Now we have full hydration support!
This one might take a bit to wrap your brain around, because we’ve created an isomorphic composable — it works on both the server and the client.
We need to behave nearly the same, but have only a slight tweak in logic depending on where it's run. To do this, we're using the import.meta.server
and import.meta.client
values to check our environment.
There's a slight performance bug in this implementation, which might take a little refactoring to fix.
If we pass in a init
function, it will always be called, even if we're on the client and we're relying on the hydrated value.
Of course, this is a simplified version, so there are other edge cases that aren't covered here.
We've now built a simple version of useState
that supports SSR and hydration.
This composable is a foundational piece of Nuxt, and now you have a deeper understanding of how it works under the hood.
If you enjoyed this article, please consider sharing it with others who might find it helpful!