Publicado · en mejora
Guía de Vue.js · Nuxt · 4/6
Por ahora, este capítulo solo está disponible en inglés.
As an app grows, two questions matter more and more: where state should live, and how server data gets loaded. This chapter covers reusing logic with composables, managing global state with Pinia, and exchanging data through Nuxt's useFetch, useAsyncData and server routes.
| Scope of the state | Where to keep it |
|---|---|
| Inside one component | The component's ref or reactive |
| Between parent and child | Props and emits, or provide and inject |
| Shared across screens | A Pinia store |
| Data from the server | useFetch or useAsyncData (Nuxt), or a composable |
Rather than putting everything in a global store from day one, keep state as close as possible to where it is used and lift it up only when sharing becomes necessary.
A composable is a function, named with a use prefix, that bundles reactive state and logic and returns them. Reach for one when several components need the same behavior.
// composables/useCounter.ts
import { ref, computed } from "vue";
export function useCounter(initial = 0) {
const count = ref(initial);
const double = computed(() => count.value * 2);
const increment = () => count.value++;
const reset = () => (count.value = initial);
return { count, double, increment, reset };
}In a component, write const { count, increment } = useCounter(10);. Every call creates independent state. In Nuxt, functions in the composables/ folder are auto-imported.
Pinia is the official state management library for Vue. Stores are defined with defineStore, and the setup syntax looks just like a composable: ref becomes state, computed becomes a getter, and functions become actions.
// stores/cart.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
type Item = { id: number; name: string; price: number; qty: number };
export const useCartStore = defineStore("cart", () => {
const items = ref<Item[]>([]);
const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0));
function add(item: Omit<Item, "qty">) {
const found = items.value.find((i) => i.id === item.id);
if (found) found.qty++;
else items.value.push({ ...item, qty: 1 });
}
return { items, total, add };
});Destructuring state straight off a store breaks reactivity, so use . Actions can be destructured as they are.
storeToRefs<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useCartStore } from "@/stores/cart";
const cart = useCartStore();
const { items, total } = storeToRefs(cart);
</script>
<template>
<p>{{ items.length }} items, total {{ total }}</p>
<button @click="cart.add({ id: 1, name: 'Book', price: 15 })">Add to cart</button>
</template>In Nuxt, add the @pinia/nuxt module with npx nuxi module add pinia; state created during server rendering is then transferred safely to the client.
Files in server/api/ become API endpoints. Adding .get or .post before the extension restricts the HTTP method.
// server/api/todos.get.ts
export default defineEventHandler(async (event) => {
const { done } = getQuery(event);
const todos = [
{ id: 1, text: "Learn Vue", done: true },
{ id: 2, text: "Deploy Nuxt", done: false },
];
return done === undefined ? todos : todos.filter((t) => String(t.done) === done);
});
// server/api/todos.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody<{ text: string }>(event);
if (!body?.text) {
throw createError({ statusCode: 400, statusMessage: "text is required" });
}
return { id: Date.now(), text: body.text, done: false };
});Returned objects are serialized as JSON. In a real service, this is where you call a database or an external API, reading secrets with useRuntimeConfig(event).
Use useFetch to load data for a page. During server rendering the result is fetched once and shipped with the page, so the client does not repeat the request.
<script setup lang="ts">
const { data: todos, status, error, refresh } = await useFetch("/api/todos");
async function addTodo(text: string) {
await $fetch("/api/todos", { method: "POST", body: { text } });
await refresh();
}
</script>
<template>
<p v-if="status === 'pending'">Loading...</p>
<p v-else-if="error">Error: {{ error.message }}</p>
<ul v-else>
<li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
</ul>
</template>useFetch is shorthand for useAsyncData(key, () => $fetch(url)). Call useAsyncData directly when you combine several requests or call something other than $fetch, such as a database SDK. For requests triggered by user actions, like a button click, use $fetch. For simple state shared between server and client, useState("key", () => initialValue) creates an SSR-safe ref.
storeToRefs when pulling state out of a Pinia store.server/api/ create endpoints with defineEventHandler.useFetch or useAsyncData, and send user-triggered requests with $fetch.
0 comentarios
Iniciar sesión · Inicia sesión para dejar un comentario.
Sé el primero en comentar.