Illustration of a developer holding a green card with the Vue and Nuxt code chevron logo, on a yellow background

Understanding Nuxt 3 - Part II

blog post publisher

Roxi

Front-end Developer

Reading time: 7 min

Updated: Jul 2, 2026

Key takeaways

  • Nuxt 3 offers three ways to manage state: ref() and reactive() from the Reactivity API, the useState composable, and the Pinia library.
  • A plain ref/reactive store works client-side but struggles with server-side rendering because of state hydration.
  • The useState composable gives you SSR-friendly shared state through a simple key-based system, without prop drilling.
  • Pinia is the official Vue state management library, with stores, getters, actions, and Vue DevTools support for larger apps.
  • Choose by complexity: ref and useState suit simple sites and landing pages, while Pinia fits apps with logins and complex logic. These patterns carry over to Nuxt 4, now the current stable release.

In the first part of this series, we covered the major differences between the Composition API and the Options API and how the Reactivity API works in Nuxt 3. Now it's time to talk about state management. This concept builds on the features we discussed before. So if you haven't read the first part yet, now's the time.

A quick note on versions. Nuxt 4 is now the current stable release, and Nuxt 3 has entered maintenance mode. The state management patterns below still apply, because Nuxt 4 builds on the same foundations. For the latest details, always check the official Nuxt documentation.

State management in Nuxt 3

The variables we declare in a component are only available in that component's scope. But real apps often need to share variables across components, functions, or pages. Since we already covered composables and reactive data, I'll show three ways to handle state management in a Nuxt 3 app. First, let's see how the Reactivity API can do it.

A. Ref() and Reactive()

Code screenshot of a store.js file importing reactive from vue and exporting a reactive store object with a count property

Code screenshot of ComponentA.vue importing the shared store and rendering store.count in its template

Now whenever the store object is mutated, any component that imports it updates its views automatically. So we have a single source of truth.

This also means any component that imports the store can mutate it freely. To stop the global state from being changed at random, it's best to define methods on the store with names that express the intent of each action.

Code screenshot of a reactive store with a named method that increments the count property using this.count

The ref() and reactive() functions can manage state in client-side apps. But if you build a web application that uses server-side rendering (SSR), this pattern brings challenges, including state hydration.

With server-side rendering in Nuxt, the app first runs on the server to generate the initial HTML. There's a good chance one or more refs are used during that setup. Once the app boots on the client, though, none of those variables are set. So we have to run the setup code again to work out their values.

This is where hydration comes in. We take the state we already computed on the server and send it with the app's HTML, CSS, and other assets. Then, instead of recalculating everything, we pick up where we left off.

Sadly, ref and reactive can't do this for us. Usually that's fine. It only becomes an issue when your ref relies on server state, such as a request header or data fetched during server rendering.

B. The useState composable

That's why Nuxt provides the useState composable. It creates a reactive, SSR-friendly shared state across components. When Nuxt renders a page, it stores all the state used by useState and sends it to the client. When the client boots, it hydrates that state back in, restarting every piece of state that used useState.

The useState composable isn't a full state management library. It lets you share state with a simple key-based system. That helps you avoid prop drilling and heavy use of events.

Code screenshot using the useState composable with a user key to store an object with id, name and profile fields

As apps grow larger and more complex, so does the state. A flat state with basic keys stops making sense. That's why Pinia, the official Vue state management library, is a must-learn, just like its older sibling Vuex.

C. Pinia

Pinia offers a better developer experience than Nuxt's useState. It adds features you'll likely need as your app grows. A stubborn developer could build the same things without it. But there's no need to reinvent the wheel when the Vue team already did. Here are the features Pinia brings:

    • Stores to organise your state.
    • Integration with Vue DevTools, including in-component inspection and time-travel debugging.
    • Modifying stores without reloading your page.
    • Actions and getters for easier updating and reading of state.
    • No explicit mutations, since all changes are implicit.

Let's look at how to define a store with Pinia in Nuxt 3.

Code screenshot of a Pinia setup store using defineStore with a filtersList state, a getter, and an addValueToFilterList action

First, define the store with Pinia's defineStore function. It takes two arguments. The first is the store name, which Pinia uses to connect to the DevTools. The second is either a setup function (example above) or an Options API object (below).

Code screenshot of a Pinia options store using defineStore with state, getters and actions for a filter store

Think of state as the store's data, getters as its computed properties, and actions as its methods. Option stores are more intuitive. They give you a skeleton that's easier to work with.

The setup function version is less verbose and more concise. In it:

    • ref()s become state properties.
    • computed() functions become getters.
    • function()s become actions.

Now that you know all three ways to handle state management in Nuxt 3, the common question is which one to choose.

Diagram showing an app complexity spectrum moving from ref to useState to Pinia, with the Pinia pineapple mascot

Source: Nuxt 3 State Management: Pinia vs useState

It comes down to app complexity. Ref() and useState are enough for a simple site or a landing page. Pinia is the go-to for a web app where users log in and do more complex operations.

Hands-on: build a Nuxt 3 app

In this article, I wanted to cover the very basics of what Nuxt 3 brings. Keep in mind it has many more features. I strongly recommend starting with the official Vue and Nuxt docs before you write code. But if you're impatient and like to dig into code first, like me, here are the steps to a fresh Nuxt 3 app with Pinia. Run npx nuxi init nuxt-3-demo to set up a new project with no dependencies installed. Then move into the project with cd nuxt-3-demo and install the dependencies with yarn install. Now start the dev server with yarn dev.

If you know Nuxt 2, you'll notice the Nuxt 3 project structure is simpler. You add the folders you need, rather than having everything and enabling it with a file.

This means Nuxt 3 is much smaller and skips the router by default. There's also no pages folder by default. When you add a pages folder and put a file inside, Nuxt builds the router for you. If you only have one page, such as a landing page, you may not need the pages folder. So this is the directory structure you'll see after running the commands above.

VS Code screenshot of a fresh nuxt-3-demo project with app.vue open showing a template and the NuxtWelcome starter component

The app.vue file is the entry point of the app and acts as the index page.

Before adding pages, we adapt app.vue by replacing the <NuxtWelcome /> starter component with <NuxtPage>. This built-in component ships with Nuxt and is needed to display top-level or nested pages from the pages/ directory.

Then run npx nuxi add page about. Watch how Nuxi, the new Nuxt CLI, creates the pages folder with an about.vue file and boilerplate code. Easy as one, two, three.

Now visit http://localhost:3000/about and you'll see it. Try the index page at http://localhost:3000/, though, and you'll get a 404 error. That's because you don't have an index.vue file in your pages folder yet. So run npx nuxi add page index to add it.

Next, add Pinia to your project with yarn add @pinia/nuxt. The only change you need is to add '@pinia/nuxt' to your modules list in the nuxt.config.ts file. And you're good to go.

Conclusion

Now that we've gone through both parts and set up a Nuxt 3 app from scratch, the fundamentals are set. What's next is to get creative and play with the Nuxt modules of your choice. That's how you add more complex features to the demo site.

If you need to migrate a complex Nuxt 2 app to Nuxt 3, I recommend reading this great migration guide first.

In the end, I find Nuxt 3 and Vue 3 exciting to learn, despite the big changes. What's even better is that the Vue core team encourages the Composition API but doesn't force it. You're free to stick with the Options API if that feels more comfortable. Later, once you have a good grip and built your first website, you can challenge yourself to refactor it with the Composition API.

Want a team that ships production-grade Nuxt and Vue apps? Explore our Nuxt.js development and Vue.js development services, or see how our web development team works. Thank you for reading.

Frequently asked questions

Nuxt 3 gives you three main options. You can use the Reactivity API with ref() and reactive() to build a shared store, use the built-in useState composable for SSR-friendly shared state, or use Pinia, the official Vue state management library, for larger and more complex apps.
It depends on complexity. The useState composable is enough for a simple presentation site or landing page, since it shares state through a simple key-based system. Pinia is the better choice for a web app where users log in and perform more complex operations, because it adds stores, getters, actions, and DevTools support.
With SSR, the app first runs on the server to build the initial HTML, then reruns setup on the client where those variables are not yet set. A plain ref or reactive store cannot carry that server state across, so state that depends on request headers or server-fetched data needs hydration, which useState handles.
Initialise a project with the Nuxt CLI, move into the folder, and install the dependencies. Start the dev server, then add the Pinia Nuxt module and list it in your Nuxt config file. From there you can define stores with Pinia's defineStore function and start managing state.