Skip to content
On this page
v2.0.0-pre.18

Common Patterns

Accessing a Store From Hooks

You can use Auto-Imports to reference a store from within hooks. This example accesses the userStore inside of the hooks:

Note about Feathers Types

Replace my-feathers-api in the below example with the package installed from your Feathers v5 Dove API. You can also provide manual types that describe the shape of your data.

ts
import type { ModelInstance } from 'feathers-pinia'
import type { User, UserData, UserQuery } from 'feathers-pinia-api'

export const useUsersConfig = () => {
  const { pinia, idField, whitelist } = useFeathersPiniaConfig()
  const servicePath = 'users'
  const service = useFeathersService<User, UserQuery>(servicePath)
  const name = 'User'

  return { pinia, idField, whitelist, servicePath, service, name }
}

export const useUserModel = () => {
  const { idField, service, name } = useUsersConfig()

  const Model = useModel(name, () => {
    const modelFn = (data: ModelInstance<User>) => {
      const defaults = {
        email: '',
        password: '',
      }
      const withDefaults = useInstanceDefaults(defaults, data)
      return withDefaults
    }
    return useFeathersModel<User, UserData, UserQuery, typeof modelFn>({ name, idField, service }, modelFn)
  })

  onModelReady(name, () => {
    service.hooks({ 
      around: { 
        all: [...feathersPiniaHooks(Model)],
        find: [
          async (context, next) => {
            // Reference the userStore through auto-imports
            const userStore = useUserStore()
            // Do something with the userStore before sending the request
            await next()
            // Do something with the userStore after the response comes back.
          }
        ]
      } 
    })
  })
  connectModel(name, () => Model, useUserStore)

  return Model
}

Handle Custom Server Response

Now that Feathers-Pinia is fully integrated into hooks, custom server responses should be handled in hooks. See the previous example, above.

Reactive Lists with Live Queries

Using Live Queries greatly simplifies app development. The find getter enables this feature. Here is how you might setup a component to take advantage of Live Queries. The next example shows how to setup two live-query lists using two getters.

ts
const Appointment = useAppointmentModel

// fetch past and future appointments
const params = reactive({ query: {} } })
const { isPending, find } = Appointment.useFind(params)

// future appointments
const futureParams = reactive({ query: { date: { $gt: new Date() } } })
const { data: futureAppointments } = Appointment.useFind(futureParams)

// past appointments
const pastParams = reactive({ query: { date: { $lt: new Date() } } })
const { data: pastAppointments } = Appointment.useFind(pastParams)

in the above example of component code, the future and pastAppointments will automatically update as more data is fetched using the find utility. New items will show up in one of the lists, automatically. feathers-pinia listens to socket events automatically, so you don't have to manually wire any of this up!

Query Once Per Record

To prevent making extra get requests, you can use one of the following queryOnce patterns:

Query Once Manual

tip

See the next example for a new short-hand syntax to implement this same pattern with store.useGetOnce.

For real-time apps, it's not necessary to retrieve a single record more than once, since feathers-pinia will automatically keep the record up to date with real-time events. You can use queryWhen to make sure you only retrieve a record once. Perform the following steps to accomplish this:

  1. Pass immediate: false in the params to prevent the initial request.
  2. Pass a function that returns a boolean to queryWhen. In this example, we return !user.value because we should query when we don't already have a user record.
  3. Manually call get, which will only trigger an API request if we don't have the record. Woot!
ts
const User = useUserModel()

interface Props {
  id: string | number
}
const props = defineProps<Props>()

const { data: user, queryWhen, get } = User.useGet(props.id, { 
  onServer: true, 
  immediate: false            // (1)
})
queryWhen(() => !user.value)  // (2)
await get()                   // (3)

The above example also shows why queryWhen is no longer passed as an argument. It's most common that queryWhen needs values returned by useGet, but those values aren't available until after useGet runs, making them unavailable to queryWhen as an argument. In short, moving queryWhen to the returned object gives us access to everything we need to productively prevent queries.

Query Once Auto

The previous pattern of only querying once is so common for real-time apps that we've built a shortcut for it at store.useGetOnce. It uses the same code as above, but built into the store method.

ts
const User = useUserModel()

interface Props {
  id: string | number
}
const props = defineProps<Props>()

const { data: user } = User.useGetOnce(props.id)

Now the same record will only be retrieved once.

Clearing Data on Logout

The best solution is to simply refresh to clear memory. If you're using localStorage, clear the localStorage, then refresh. The alternative to refreshing would be to perform manual cleanup of the service stores. Refreshing is much simpler and more practical, so it's the official solution.

Model-Level Computed Props

You can define model-level computed properties by using Object.defineProperty to create a non-enumerable, configurable, ES5 getter. Note that when you use defineProperty, you have to manually specify a union type. The line return withDefaults as typeof withDefaults & { fullName: string } lets TypeScript know that the fullName property exists.

ts
import type { Users, UsersData, UsersQuery } from 'my-feathers-api'
import { type ModelInstance } from 'feathers-pinia'

const modelFn = (data: ModelInstance<Users>) => {
  const withDefaults = useInstanceDefaults({ firstName: '', lastName: '' }, data)

  // Define a non-enumerable, configurable property
  Object.defineProperty(withDefaults, 'fullName', {
    enumerable: false,
    configurable: true,
    get() {
      return `${this.firstName} ${this.lastName}`
    }
  })
  return withDefaults as typeof withDefaults & { fullName: string }
}
const User = useBaseModel<Users, UsersQuery, typeof modelFn>({ name: 'User', idField: '_id' }, modelFn)

https://vuex.feathersjs.com/common-patterns.html#model-specific-computed-properties

Relationships Between Services

See the Model Associations page.

Working with Forms

Mutation Multiplicity Pattern

The Mutation Multiplicity (anti) Pattern is a side effect of strict mode in stores. Vuex strict mode would throw errors when editing data in the store. Thankfully, Pinia will not throw errors when you modify store data. However, it's considered an anti-pattern to modify store data directly. The one exception is that cloned records are considered safe to edit in Feathers-Pinia, despite being kept in the store. The most common (anti)pattern that beginners use to work around the "limitation" of not being able to edit store data is to

  1. Read data from the store and use it for display in the UI.
  2. Create custom actions/mutations intended to modify the data in specific ways.
  3. Use the actions/mutations wherever they apply (usually implemented as one mutation per form).

There are times when defining custom mutations is the most supportive pattern for the task, but consider them to be more rare. The above pattern can result in a huge number of mutations, extra lines of code, and increased long-term maintenance costs.

The solution to the Mutation Multiplicity Malfeasance is the Clone and Commit Pattern in Feathers-Pinia.

Clone and Commit Pattern

The "Clone and Commit" pattern provides an alternative to using a lot of actions/mutations. This patterns looks more like this:

  1. Read data from the store and use it for display in the UI. (Same as above)
  2. Create and modify a clone of the data.
  3. Use a single mutation to commit the changes back to the original record in the store.

Sending most edits through a single mutation can really simplify the way you work with store data. The BaseModel class has clone and commit instance methods. These methods provide a clean API for working with items in the store and not unsafely editing data:

ts
const Task = useTaskModel()

const task = Task({
  description: 'Plant the garden',
  isComplete: false
})

const clone = task.clone()
clone.description = 'Plant half of the garden."
clone.commit()

In the example above, modifying the task variable would unsafely modify stored data, which is a generally unsupportive practice when not done consciously. Calling task.clone() returns a reactive clone of the instance. It's safe to change clones. You can then call clone.commit() to update the original record in the store.

The clone and commit methods are used by useClone and useClones.

Feathers Client

This section reviews how to create and use Feathers Clients

Feathers Clients Manual Setup

FeathersJS v5 Dove creates a typed client for you, but you can still create Feathers Clients manually.

Here's an example feathers-socket.io client:

ts
// src/feathers.ts
import { feathers } from '@feathersjs/feathers'
import socketio from '@feathersjs/socketio-client'
import auth from '@feathersjs/authentication-client'
import io from 'socket.io-client'

const socket = io('http://localhost:3030', { transports: ['websocket'] })

// This variable name becomes the alias for this server.
export const api = feathers()
  .configure(socketio(socket))
  .configure(auth({ storage: window.localStorage }))

Multiple Feathers Clients

For additional Feathers APIs, export another Feathers client instance with a unique variable name (other than api).

Here's an example that exports a couple of feathers-rest clients:

ts
// src/feathers.ts
import { feathers } from '@feathersjs/feathers'
import rest from '@feathersjs/rest-client'
import auth from '@feathersjs/authentication-client'

const fetch = window.fetch.bind(window)

// The variable name of each client becomes the alias for its server.
export const api = feathers()
  .configure(rest('http://localhost:3030').fetch(fetch))
  .configure(auth())

export const analytics = feathers()
  .configure(rest('http://localhost:3031').fetch(fetch))
  .configure(auth())

SSG-Compatible localStorage

When doing Static Site Generation (SSG), the server doesn't usually have access to the window object, which is a browser global. Trying to access a non-existent window variable will throw an error on the server. The easiest way to get around this issue is with useStorage from the @vueuse/core package.

ts
import { createClient } from 'feathers-pinia-api'
import { useStorage } from '@vueuse/core'
import socketio from '@feathersjs/socketio-client'
import io from 'socket.io-client'

const host = import.meta.env.VITE_MYAPP_API_URL as string || 'http://localhost:3030'
const socket = io(host, { transports: ['websocket'] })

// setup SSG-compatible authentication storage
const storageKey = 'feathers-jwt'
const jwt = useStorage(storageKey, '')
const storage = {
  getItem: () => jwt.value,
  setItem: (key: string, val: string) => (jwt.value = val),
  removeItem: () => (jwt.value = null),
}

export const api = createClient(socketio(socket), { storage })

Server-Compatible Fetch

For a fetch adapter that's compatible with Static Site Generation (SSG) and Server-Side Rendering (SSR), check out the OFetch page.

Access Feathers Client

While it's possible to manually import the Feathers Client using the module system, like this:

ts
import { api } from '../feathers'

Thanks to Auto-Imports, we can decouple from the module path, completely, and define our own composable function that returns an object which contains our app's Feathers Client instances:

ts
// src/composables/use-feathers.ts
import { api } from '../feathers'

export const useFeathers = () => {
  return { api }
}

And now in our composables and components, we can access the Feathers Client by calling our composable function, no need to import it, first (assuming you're using auto-imports as shown in the setup guides). Here's what it looks like:

ts
const { api } = useFeathers()

Avoid npm Install Errors

If you're using npm to install packages and keep getting errors about vue-demi and peerDependencies, you can silence these errors by creating an .npmrc file in the root of your project with the following contents:

text
shamefully-hoist=true
strict-peer-dependencies=false
legacy-peer-deps=true

Global Configuration

We can use composables to create a global configuration function for Feathers-Pinia. It's very convenient when paired with Auto-Imports. Use a global config for consistent Models and stores. This example is found in models/feathers-pinia-config.ts in the Vite and Nuxt example applications:

ts
import { pinia } from '~/modules/pinia'

/**
 * Returns a configuration object for Feathers-Pinia
 */
export const useFeathersPiniaConfig = () => {
  return {
    pinia,
    idField: '_id',
    whitelist: ['$regex'],
  }
}
ts
/**
 * Returns a global configuration object for Feathers-Pinia
 */
export const useFeathersPiniaConfig = () => {
  const { $pinia: pinia } = useNuxtApp()
  return {
    pinia,
    idField: '_id',
    whitelist: ['$regex'],
  }
}

Now you can use the useFeathersPiniaConfig to create service-specific composables to share between the Model and store. Place this composable directly above the Model definition (in this case the User Model definition):

ts
export const useUsersConfig = () => {
  const { pinia, idField, whitelist } = useFeathersPiniaConfig()
  const servicePath = 'users'
  const service = useFeathersService<User, UserQuery>(servicePath)
  const name = 'User'

  return { pinia, idField, whitelist, servicePath, service, name }
}

And now you can call useUsersConfig() and destructure the variables needed by the Model and store, as shown in the setup examples

note

Before version 2, Feathers-Pinia included the `setupFeathersPinia` utility to enable global configuration. That API has been removed in favor of the above solution.

Many thanks go to the Vue, Vuex, Pinia, and FeathersJS communities for keeping software development FUN!