Skip to content

Store & Normalization

Nuxt Zenstack uses a centralized, normalized store to manage your application's data. This ensures that your state remains consistent, prevents duplicate data from being stored, and makes UI updates reactive across your entire application.

Important Limitations

  • Data models should have a unique field named id, it can be a number or string.
  • Data structures returned by the server are serialized via superjson, so dates for example are not converted to strings.
  • The zenstack generated files should be located at project root under zenstack folder.
  • When zenstack schema is re-generated the app should be restarted.
  • The useZenstackUpdate and useZenstackCreate composables do not allow relational fields.
  • Update and delete operations triggered by cascading operations are not handled by realtime subscriptions, and the store does not get updated automatically.

The useZenstackStore Composable

At the core of data management is the useZenstackStore composable. Whenever you fetch, create, update, or delete data using the provided CRUD composables, the changes are automatically reflected in this centralized store.

typescript
const { state, getOne, getMany } = useZenstackStore()

The store provides methods to interact with the raw normalized data, though you will typically interact with it through the higher-level CRUD composables (like useZenstackRead or useZenstackCreate).

How Normalization Works

When data is fetched from your API (which often contains nested relationships), Nuxt Zenstack processes it before storing it.

  1. Flattening with normalizr: The nested JSON response is flattened into a dictionary of entities, grouped by their model name and ID. If multiple queries return the same entity (e.g., a specific User), only one copy of that entity is kept in the store.
  2. Merging Data: When new data is fetched or a mutation occurs, the store intelligently merges the incoming data with the existing state, preserving existing fields while updating changed ones.

Structure of the Store

The underlying state structure looks like this:

typescript
{
  "User": {
    "user_1": { id: "user_1", name: "Alice", posts: ["post_1", "post_2"] }
  },
  "Post": {
    "post_1": { id: "post_1", title: "Hello World", authorId: "user_1" },
    "post_2": { id: "post_2", title: "Nuxt is awesome", authorId: "user_1" }
  }
}

Advanced Features

Bidirectional Relationship Linking

Normalizing data usually requires the payload to explicitly contain both sides of a relationship. Nuxt Zenstack goes a step further by automatically linking relationships based on your ZenStack schema.

For example, if you create a new Post and only provide the authorId, the store will automatically:

  1. Set the author field on the Post.
  2. Push the new Post ID into the posts array of the corresponding User.

This means that any UI components displaying the user's posts will immediately re-render with the new post, without needing a full refetch.

Circular Reference Handling

Because relationships are inherently circular (a User has Posts, and a Post has a User), returning raw denormalized data can cause issues with Vue's reactivity system and JSON serialization (such as during SSR context transfer).

When you read data from the store, Nuxt Zenstack automatically breaks these circular references safely, ensuring your app won't crash when rendering complex relationship trees.

Fetch History Tracking

The store also keeps track of all data fetching operations (fetchHistory), differentiating between requests made on the server (SSR) and on the client. This is useful for debugging and optimizing your data-loading strategies.