Common Patterns
Common Pitfalls
See the Common Pitfalls page for solutions to common issues.
Accessing a Store From Hooks
First, get the app
instance from context
. Then lookup a service and use its methods:
async (context: HookContext, next: NextFunction) => {
const { app } = context
// use service methods
app.service('messages').findInStore()
// directly read from the store
app.service('messages').store.items
await next()
}
Handle Custom Methods
See the FeathersJS documentation for to to use custom methods.
To handle the response from a custom method, either customize the store or use store composition.
Customize the Store
Using customizeStore
The customizeStore
global- and service-level configuration options allow you to return an object with additional state, computed properties, and functions. See an example on the Feathers-Pinia Client page.
Composing Stores
Pinia's setup
stores allow a really clean way to layer functionality with Store Composition. Here's an example of how to create a feature store that references a Feathers-Pinia v3 store.
export const useFeatureStore = defineStore('my-feature-store', () => {
const { api } = useFeathers()
const usersNamedFred = computed(() => {
return api.service('users').findInStore({ query: { name: 'Fred' } })
})
return { usersNamedFred }
})
You can use any of the Feathers-Pinia service methods in composed stores. Read more about Pinia Store Composition
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.
// fetch past and future appointments
const params = computed(() => {
return { query: {} }
})
const { find } = api.service('appointments').useFind(params)
// future appointments
const futureParams = computed(() => ({ query: { date: { $gt: new Date() } } }))
const futureAppointments$ = api.service('appointments').useFind(futureParams)
// past appointments
const pastParams = computed(() => ({ query: { date: { $lt: new Date() } } }))
const pastAppointments$ = api.service('appointments').useFind(pastParams)
in the above example of component code, the futureAppointments$.data
and pastAppointments$.data
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
The simplest way to only query once per record is to set the skipGetIfExists
option to true
during configuration.
You can also use the useGetOnce
method to achieve the same behavior for individual requests.
Clearing Data on Logout
The best solution is to simply refresh to clear memory. If you're using localStorage, clear the localStorage, then call window.location.reload()
. 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.
Data-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.
import type { Users, UsersData, UsersQuery } from 'my-feathers-api'
function setupInstance(data: 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 }
}
Relationships Between Services
Use Object.defineProperties
to create relationships in the setupInstnace
method of each service.
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
- Read data from the store and use it for display in the UI.
- Create custom actions/mutations intended to modify the data in specific ways.
- 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:
- Read data from the store and use it for display in the UI. (Same as above)
- Create and modify a clone of the data.
- 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:
const task = api.service('tasks').new({
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.
Feathers Client
This section reviews how to create and use Feathers Clients
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:
// 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.
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),
}
const feathersClient = createClient(socketio(socket), { storage })
export const api = createPiniaClient(feathersClient, { idField: '_id'})
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:
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:
// src/composables/use-feathers.ts
import { api } from '../feathers'
export function 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:
const { api } = useFeathers()