Skip to content

0.6 to 0.7 Migration Guide

This guide will help you migrate your code from rstore v0.6 to v0.7. There are a lot of breaking changes, so please read the entire guide carefully and test your code thoroughly after the migration.

Those changes are necessary to prepare the library for the 1.0 release. 🙇‍♂️

Model renamed to Collection

VueVue
NuxtNuxt

The term model has been replaced with collection throughout the rstore codebase and API to better reflect its purpose and avoid confusion with Vue's own defineModel function and v-model API.

ts
// Schema
defineDataModel({ name: 'todos' }) 
defineCollection({ name: 'todos' }) 

defineItemType<Todo>().model({ name: 'todos' }) 
withItemType<Todo>().defineCollection({ name: 'todos' }) 

// Advanced APIs
store.$models.find(m => m.name === 'Users') 
store.$collections.find(c => c.name === 'Users') 

store.$model('Users').peekFirst('some-id') 
store.$collection('Users').peekFirst('some-id') 

store.$getModel({ __typename: 'Users ', id: 'some-id'}) 
store.$getCollection({ __typename: 'Users ', id: 'some-id'}) 

// Plugins
hook('fetchFirst', async ({ model, key, params }) => { 
hook('fetchFirst', async ({ collection, key, params }) => { 
  // ...
})

TIP

The main APIs such as store.Users.peekFirst() are not impacted because the collection name is used directly.

Store setup

defineDataModel renamed to defineCollection

VueVue
NuxtNuxt

Why this change?

The term model has been replaced with collection throughout the rstore codebase and API to better reflect its purpose and avoid confusion with Vue's own defineModel function and v-model API.

js
import { defineDataModel } from '@rstore/vue'
import { defineCollection } from '@rstore/vue'

const todoCollection = defineDataModel({ 
const todoCollection = defineCollection({ 
  name: 'todos',
})

defineItemType renamed to withItemType

VueVue
NuxtNuxt

Why this change?

The defineItemType function has been renamed to withItemType (and its method from model to defineCollection) to better reflect its purpose of defining the collection with the defineCollection method that is chained to it.

ts
import { defineItemType } from '@rstore/vue'
import { withItemType } from '@rstore/vue'

interface Todo {
  id: string
  title: string
  completed: boolean
}

const todoCollection = defineItemType<Todo>().model({ 
const todoCollection = withItemType<Todo>().defineCollection({ 
  name: 'todos',
})

Use RStoreSchema in Nuxt

VueVue
NuxtNuxt

You need to use the RStoreSchema auto-imported namespace to define collections and relations.

Why this change?

This change was made to avoid potential naming conflicts with other libraries or your own code. It also makes it clearer that those functions are part of the rstore library to define your store schema.

ts
const Users = defineItemType<User>().model({ 
const Users = RStoreSchema.withItemType<User>().defineCollection({ 
  name: 'users',
})

models renamed to schema

VueVue
NuxtNuxt

Why this change?

Since it now contains both collections (previously models) and relations (see below) - and maybe other objects in the future, the models option has been renamed to schema.

ts
const store = await createStore({
  models: [ 
  schema: [ 
    { name: 'todos' },
    { name: 'users' },
  ],
  plugins: [],
})

Use the RstorePlugin

VueVue
NuxtNuxt

You should now use the RstorePlugin from @rstore/vue to install the store in your Vue app. This will automatically provide the store to all components and make it available via the new useStore function from @rstore/vue.

Why this change?

This change simplifies the store setup and makes it easier to use the store in your components. It also makes it possible to use the new useStore function from @rstore/vue directly without having to create your own.

ts
import type { VueStore } from '@rstore/vue'
import type { InjectionKey } from 'vue'
import { RstorePlugin } from '@rstore/vue'

const injectStoreKey = Symbol('rstore') as InjectionKey<VueStore<typeof schema>> 

export async function rstore(app: App) {
  const store = await createStore({
    // ...
  })

  app.provide(injectStoreKey, store) 
  app.use(RstorePlugin, { store })  
}

export function useStore () { 
  const store = inject(injectStoreKey, null) 
  if (store == null) { 
    throw new Error('No rstore provided.') 
  } 
  return store 
} 

// Augment the `useStore` type
declare module '@rstore/vue' { 
  export function useStore(): VueStore<typeof schema> 
} 

useStore()

VueVue
NuxtNuxt

You can now use the new useStore function from @rstore/vue directly instead of creating your own.

Why this change?

This change simplifies the store usage in your components and makes it easier to get the store instance without having to create your own useStore function.

ts
import { useStore } from 'your-own-path/rstore'
import { useStore } from '@rstore/vue'

Relation definitions shape changed

VueVue
NuxtNuxt

Instead of defining the related fields using on and eq that were confusing, you now define the related fields using an on object that maps the foreign fields to the local fields with the name of the collections to make it clearer.

Why this change?

This new syntax is more explicit and easier to understand. It also allows for more complex relations such as mapping multiple fields or using a custom filter function.

ts
import { defineItemType } from '@rstore/vue'

interface User {
  id: string
  name: string
}

const userCollection = defineItemType<User>().defineCollection({
  name: 'users',
  relations: {
    receivedMessages: {
      to: {
        messages: {
          on: 'recipientId', // Message.recipientId
          eq: 'id', // User.id
          on: { 
            'messages.recipientId': 'users.id', 
          }, 
        },
      },
      many: true,
    },
    sentMessages: {
      to: {
        messages: {
          on: 'senderId', // Message.senderId
          eq: 'id', // User.id
          on: { 
            'messages.senderId': 'users.id', 
          }, 
        },
      },
      many: true,
    },
  },
})

interface Message {
  id: string
  content: string
  senderId: string
  recipientId: string
}

const messageCollection = defineItemType<Message>().defineCollection({
  name: 'messages',
  relations: {
    sender: {
      to: {
        users: {
          on: 'senderId', // Message.senderId
          eq: 'id', // User.id
          on: { 
            'users.id': 'messages.senderId', 
          }, 
        },
      },
    },
    recipient: {
      to: {
        users: {
          on: 'recipientId', // Message.recipientId
          eq: 'id', // User.id
          on: { 
            'users.id': 'messages.recipientId', 
          }, 
        },
      },
    },
  },
})

This new syntax also allows you to map multiple fields on a single relation:

ts
const myCollection = {
  name: 'things',
  relations: {
    relatedItems: {
      to: {
        otherCollection: {
          on: {
            'otherCollection.type': 'things.relatedItemType', 
            'otherCollection.subType': 'things.relatedItemSubType', 
          },
        },
      },
    },
  },
}

Or specify a custom filter function:

ts
const myCollection = {
  name: 'things',
  relations: {
    relatedItems: {
      to: {
        otherCollection: {
          on: {
            // 'otherCollection.type': 'things.relatedItemType',
          },
          filter: (item, relationItem) =>
            item.relatedStatusList.includes(relationItem.status), 
        },
      },
    },
  },
}

If you use TypeScript, it is now recommended to use the new defineRelations helper to define your relations with proper typing:

ts
import type { StoreSchema } from '@rstore/vue'
import { defineItemType, defineRelations } from '@rstore/vue'

interface User {
  id: string
  name: string
}

const userCollection = defineItemType<User>().defineCollection({
  name: 'users',
})

interface Message {
  id: string
  content: string
  senderId: string
  recipientId: string
}

const messageCollection = defineItemType<Message>().defineCollection({
  name: 'messages',
})

const userRelations = defineRelations(userCollection, ({ collection }) => ({
  receivedMessages: {
    to: collection(messageCollection, {
      on: {
        'messages.recipientId': 'users.id', // Type checked!
      },
    }),
    many: true,
  },
  sentMessages: {
    to: collection(messageCollection, {
      on: {
        'messages.senderId': 'users.id', // Type checked!
      },
    }),
    many: true,
  },
}))

const messageRelations = defineRelations(messageCollection, ({ collection }) => ({
  sender: {
    to: collection(userCollection, {
      on: {
        'users.id': 'messages.senderId', // Type checked!
      },
    }),
  },
  recipient: {
    to: collection(userCollection, {
      on: {
        'users.id': 'messages.recipientId', // Type checked!
      },
    }),
  },
}))

export const schema = [
  userCollection,
  messageCollection,
  userRelations,
  messageRelations,
] as const satisfies StoreSchema

Example of the typechecking in action:

Typechecking example

Nuxt module now looks for exported consts

VueVue
NuxtNuxt

Instead of using the default export as an array of collections, the Nuxt module now looks for all exported consts in the files and merges them into a single array.

Why this change?

The export default [...] syntax at the end of the files was not very ergonomic and could lead to mistakes (for example forgetting to add the new collection to the array).

ts
const Users = defineItemType<User>().model({ 
export const Users = defineItemType<User>().defineCollection({ 
  name: 'users',
})

const Messages = defineItemType<Message>().model({ 
export const Messages = defineItemType<Message>().defineCollection({ 
  name: 'messages',
})

export default [ 
  Users, 
  Messages, 
] 

Nuxt StoreResolvedModdelItem renamed to StoreResolvedCollectionItem

VueVue
NuxtNuxt
ts
import type { StoreResolvedModelItem } from '#imports'; 
import type { StoreResolvedCollectionItem } from '#imports'; 

export type UserResolvedItem = StoreResolvedModelItem<'users'>; 
export type UserResolvedItem = StoreResolvedCollectionItem<'users'>; 

Query

Methods don't accept Ref or getter anymore

VueVue
NuxtNuxt

The following methods no longer accept a Ref or a getter function as argument. You must now always pass a plain object (or string/number for single key):

  • peekFirst
  • peekMany
  • findFirst
  • findMany
ts
store.User.peekFirst(() => 'toto') 
store.User.peekFirst('toto') 

store.User.peekFirst(userIdRef) 
store.User.peekFirst(userIdRef.value) 

Why this change?

This change was made to simplify the API and avoid confusion. Those methods are not reactive and will not update automatically when the Ref or the result of the getter function changes. If you want to use a Ref or a getter function, you can use the query or liveQuery methods instead.

New Query API

VueVue
NuxtNuxt

The following methods have been removed:

  • queryFirst
  • queryMany
  • liveQueryFirst
  • liveQueryMany

They have been replaced by the following methods:

  • query
  • liveQuery

Those two new methods accept a function that receives a query builder as argument. You can then use the first or many methods of the query builder to build your query.

Why this change?

This new syntax allows TypeScript to fully type check the entire options object, including verifying for excess properties in deeply nested objects. The old syntax was not type checked properly and your code could easily contain typos or mistakes that would not be caught by the compiler. This is due to a limitation in TypeScript (see microsoft/TypeScript#241).

ts
const { data: user } = await store.User.queryFirst('toto') 
const { data: user } = await store.User.query(q => q.first('toto')) 
ts
const { data: user } = await store.User.queryFirst(() => ({ 
const { data: user } = await store.User.query(q => q.first({ 
  key: userId.value,
  include: { receivedMessages: true },
}))
ts
const { data: messages } = await store.Message.queryMany(() => ({ 
const { data: messages } = await store.Message.query(q => q.many({ 
  filter: m => m.recipientId === userId.value,
}))
ts
const { data: messages } = await store.Message.liveQueryMany(() => ({ 
const { data: messages } = await store.Message.liveQuery(q => q.many({ 
  filter: m => m.recipientId === userId.value,
}))

WARNING

Since the type checking is now stricter, you might encounter new type errors in your code that you didn't have before. This means that your code was not fully correct and you should fix the errors. If somethig looks wrong, please open an issue. 🙏

Subscribe method now accepts query builder

VueVue
NuxtNuxt

The subscribe method now only accepts a function that receives a query builder as argument. You then have to call the query builder function with the options object.

ts
const { unsubscribe } = store.Message.subscribe({ 
const { unsubscribe } = store.Message.subscribe(q => q({ 
  params: {
    recipientId: 'user-123',
  },
}) 
})) 
ts
const { unsubscribe } = store.Message.subscribe(() => ({ 
const { unsubscribe } = store.Message.subscribe(q => q({ 
  params: {
    recipientId: userId.value,
  },
}))

Why this change?

This new syntax allows TypeScript to fully type check the entire options object, including verifying for excess properties in deeply nested objects. The old syntax was not type checked properly and your code could easily contain typos or mistakes that would not be caught by the compiler. This is due to a limitation in TypeScript (see microsoft/TypeScript#241).

WARNING

Since the type checking is now stricter, you might encounter new type errors in your code that you didn't have before. This means that your code was not fully correct and you should fix the errors. If somethig looks wrong, please open an issue. 🙏

Customizing types

VueVue
NuxtNuxt

The types in rstore changed, so you need to update your type augmentations for:

  • CustomCacheState
  • CustomCollectionMeta (renamed from CustomModelMeta)
  • CustomFilterOption
  • CustomHookMeta
  • CustomParams
  • CustomPluginMeta
  • CustomSortOption
  • FindOptions

Why this change?

Model has been renamed to Collection and Model List to Schema throughout the rstore codebase.

ts
import { Model, ModelDefaults, ModelList } from '@rstore/core'
import { Collection, CollectionDefaults, StoreSchema } from '@rstore/vue'

declare module '@rstore/vue' {
  export interface FindOptions<
    TModel extends Model, 
    TModelDefaults extends ModelDefaults, 
    TModelList extends ModelList, 
    TCollection extends Collection, 
    TCollectionDefaults extends CollectionDefaults, 
    TSchema extends StoreSchema, 
  > extends FindOptionsBase<TModel, TModelDefaults, TModelList> { 
  > extends FindOptionsBase<TCollection, TCollectionDefaults, TSchema> { 
    // Your custom properties here
  }
}

export {}

Modules

New Module definition syntax

VueVue
NuxtNuxt

The way modules are defined has changed:

  • defineModule now accepts the module name as first argument and a setup function as second argument.
  • The setup function receives a payload as the first argument that contains:
    • store: the store instance
    • defineState: function to define reactive state (automatically hydrated after SSR)
    • defineMutation: function to define mutations
    • onResolve: function to register a (optionally async) callback to be called when the module is resolved
  • resolve function has been removed. You can now simply return the module's public API from the setup function.
  • createModule has been removed. You can now use the store instance and the defineState function directly in your module from the setup function parameter.
  • You must now setup the store with the RStorePlugin from @rstore/vue (see above) so the store is correctly injected.

Why this change?

This new syntax is much simpler and more consistent with other libraries such as pinia.

ts
import { useStore } from 'path/to/store'
import { defineModule, createModule } from '@rstore/vue'
import { defineModule } from '@rstore/vue'

export const useAuth = defineModule(() => { 
export const useAuth = defineModule('auth', ({ 
  store, 
  defineState, 
  defineMutation, 
  onResolve, 
}) => { 
  const store = useStore() 

  const { state, resolve, onResolve, defineMutation } = createModule(store, { 
    name: 'auth', 
    state: { 
      currentUserKey: null as string | null, 
    }, 
  }) 

  const state = defineState({ 
    currentUserKey: null as string | null, 
  }) 

  const currentUser = store.User.query(q => q.first(state.currentUserKey
    ? {
        key: state.currentUserKey,
      }
    : {
        enabled: false,
      }))

  const requestFetch = useRequestFetch()

  async function initCurrentUser() {
    try {
      const user = await requestFetch('/api/auth/me')
      if (user) {
        state.currentUserKey = user.id
        store.User.writeItem({
          ...user,
          createdAt: new Date(user.createdAt),
        })
      }
      else {
        state.currentUserKey = null
      }
    }
    catch (e) {
      console.error('Failed to init current user', e)
    }
  }

  const login = defineMutation(async (email: string, password: string) => {
    const result = await $fetch('/api/auth/login', {
      method: 'POST',
      body: {
        email,
        password,
      },
    })
    state.currentUserKey = result.userId
  })

  const logout = defineMutation(async () => {
    await $fetch('/api/auth/logout', {
      method: 'POST',
    })
    state.currentUserKey = null
  })

  onResolve(async () => {
    await initCurrentUser()
  })

  return resolve({ 
  return { 
    currentUser,
    login,
    logout,
    loggedIn: computed(() => !!state.currentUserKey),
  }) 
  } 
})

Module mutation state not longer Refs

VueVue
NuxtNuxt

The mutations created with defineMutation have a few useful properties to track their state such as $loading and $error. Previously, those properties were Refs, but they are now plain values (while still reactive).

Why this change?

This change was made to simplify the usage of those properties and avoid the need to access the .value property.

vue
<script setup lang="ts">
const auth = useAuth() // A module defined with defineModule
</script>

<template>
  <UAlert
    v-if="auth.login.$error.value"
    v-if="auth.login.$error"
    :description="auth.login.$error.value.message"
    :description="auth.login.$error.message"
    color="error"
  />
  <UButton
    type="submit"
    :loading="auth.login.$loading.value"
    :loading="auth.login.$loading"
  />
</template>

Plugins

model renamed to collection in payload

VueVue
NuxtNuxt

Why this change?

The term model has been replaced with collection throughout the rstore codebase and API to better reflect its purpose and avoid confusion with Vue's own defineModel function and v-model API.

ts
hook('fetchFirst', async ({ model, key, params }) => { 
hook('fetchFirst', async ({ collection, key, params }) => { 
  // ...
})

Advanced APIs

$models renamed to $collections

VueVue
NuxtNuxt
ts
store.$models.find(m => m.name === 'Users') 
store.$collections.find(c => c.name === 'Users') 

$model renamed to $collection

VueVue
NuxtNuxt
ts
store.$model('Users').peekFirst('some-id') 
store.$collection('Users').peekFirst('some-id') 

$getModel renamed to $getCollection

VueVue
NuxtNuxt
ts
store.$getModel({ __typename: 'Users ', id: 'some-id'}) 
store.$getCollection({ __typename: 'Users ', id: 'some-id'}) 

$cache.clearModel renamed to $cache.clearCollection

VueVue
NuxtNuxt
ts
store.$cache.clearModel('Users') 
store.$cache.clearCollection('Users') 

addModel and removeModel renamed to addCollection and removeCollection

VueVue
NuxtNuxt
ts
import { addModel, removeModel } from '@rstore/vue'
import { addCollection, removeCollection } from '@rstore/vue'

addModel(store, { name: 'newModel' }) 
addCollection(store, { name: 'newCollection' }) 

removeModel(store, 'oldModel') 
removeCollection(store, 'oldCollection')