🎁 Upgrade to Feathers-Pinia 2.0 🎁
Feathers-Pinia 2.0 is almost ready for final release. Read the new documentation.The useFind Utility
The useFind function is a Vue Composition API utility that takes the work out of retrieving lists of records from the store or API server.
Overview of Features
In version 1.0, the useFind utility has been completely rewritten from scratch. It is a workflow-driven utility, which makes it a pleasure to use. Here's an overview of its features:
- Intelligent Fall-Through Caching - Like SWR, but way smarter.
- Live Queries - For server data, reactive records. For client-side data, reactive lists and records. No need to manually refresh data.
- Client-Side Pagination - Built in, sharing the same logic with
usePagination. - Server-Side Pagination - Also built in and made super easy.
- Infinite Pagination Support - Bind to
allDataand tell it when to load more data. - Declarative Workflow Support - Compose computed params and let query as they change.
- Imperative Workflow Support - Pass reactive params for imperative control.
tip
To lighten the burden of migrating from Feathers-Vuex, the old useFind utility is now provided as useFindWatched. It is recommended that all new code be written with the new useFind API.
Usage
There are two ways to use useFind: from the store (recommended) or standalone.
Recommended
You can call useFind directly from the store. the advantage being that you don't have to provide the store in the params, as shown here:
import { useMessages } from '../store/messages'
interface Props {
userId: string | number
}
const props = defineProps<Props>()
const messageStore = useMessages()
const query = computed(() => ({ userId: props.userId }))
// client-side pagination with manual fetch
const { data, prev, next, find } = messageStore.useFind({ query })
await find() // retrieve data for the current query
await next() // show the next page of results
await prev() // show the previous page of results
// server-side pagination with auto fetch
const { data, prev, next } = messageStore.useFind({ query, onServer: true })
await next() // retrieve the next page of results
await prev() // retrieve the previous page of results
Standalone
In standalone mode, you have to import useFind and provide the store option in the params object, as shown here:
import { useMessages } from '../store/messages'
import { useFind } from 'feathers-pinia'
interface Props {
userId: string | number
}
const props = defineProps<Props>()
const messageStore = useMessages()
const query = computed(() => ({ userId: props.userId }))
// client-side pagination with manual fetch
const { data, prev, next, find } = useFind({ query, store: messageStore })
await find() // retrieve data for the current query
await next() // show the next page of results
await prev() // show the previous page of results
// server-side pagination with auto fetch
const { data, prev, next } = useFind({ query, store: messageStore, onServer: true })
await next() // retrieve the next page of results
await prev() // retrieve the previous page of results
API
useFind(params)
paramscan be areactivearefor acomputedobject of the following structure:query{Object} required a Feathers query object.store{Store} conditionally required a Feathers-Pinia service store. It is required in order to useuseFindin standalone mode.useFindcan also be find on any service store by callingstore.useFind(params). When called from the store, you do not pass the store object in the params.qid{string} an identifier for this query. Allows pagination data to be tracked separately.onServer{boolean} when enabled, the internalfindInStoregetter will return only the results that match the current query in thepaginationobject for this store.immediate{boolean = true} whenonServeris set, by default it will make an initial request. Setimmediate: falseto prevent the initial request.watch{boolean = false} enable this to automatically query whenreactiveorrefparams are changed. This does not apply tocomputedparams, since they are automatically watched.
Returned Object
The useFind function is actually a factory function that returns an instance of the Find class. So when you call useFind you get back an object with the following properties:
Params & Config
params{Ref Object} are an internal,refcopy of the initially-provided params.store{Store} is a reference to the associated store.onServer{boolean} indicates whether this instance makes requests to the API server. Defaults tofalse.isSsr{Computed boolean} will be true ifisSsrwas passed into thedefineStoreoptions for this service store. Useful for awaint therequestduring SSR.qid{Ref string} the query identifier. Causes this query's pagination data to be stored under its ownqidinstore.pagination.
Data
data{Ref Array} the array of results.allData{Ref Array} all results for the matching query or server-retrieved data. WithonServer, will return the correct results, even with custom query params that the store does not understand.total{Computed number} One of two things: For client-side results, the total number of records in the store that match the query. ForonServerresults, the total number of records on the server that match the query.limit{Ref number} the pagination$limit. Updating this value will change the internal pagination and the returneddata.skip{Ref number} the pagination$skip. Updating this value will change the internal pagination and the returneddata.
Query Tracking
currentQuery{Computed Object} an object containing the following:qid{string} the query identifierids{number[]} the ids in this page of dataitems{Record[]} the items in this page of datatotal{number} the total number of items matching this queryqueriedAt{number} the timestamp when this page of data was retrieved. Useful when used withqueryWhento prevent repeated queries during a period of time.queryState{Object} a pagination object from the store
latestQuery{Computed Object} an object containing the following:pageId{string} stable stringified page paramspageParams{Object} the page paramsqueriedAt{number} timestamp when this page of records was received from the server.query{Object} the query params, including $limit and $skip.queryId{string} stable-stringified query paramsqueryParams{Object} the query params, excluding $limit and $skip.total{number} the total number of records matching the query.
previousQuery{Computed Object} an object with the same format aslatestQuery.
Data Retrieval & Watching
find{Function} the same asstore.find.request{Ref Promise} the promise for the current request.requestCount{Computed number} the number of requests sent by thisFindinstance.queryWhen{Function} pass a function that returns a boolean intoqueryWhenand that function will be run before retrieving data. If it returns false, the query will not happen.findInStore{Function} the same asstore.findInStore.
Request State
isPending{Computed boolean} returns true if the currentrequestis pending.haveBeenRequested{Computed boolean} returns true if any request has been sent by thisFindinstance. Never resets for the life of the instance.haveLoaded{Computed boolean} essentially the same purpose, but opposite ofisPending. Returns true once the request finishes.error{Computed error} will contain any error. The error will be cleared when a new request is made or when manually callingclearError.clearError{Function} call this function to clear theerror.
Pagination Utils
pageCount{Computed number} the number of pages for the current query params.currentPage{Ref number} the current page number. Can be set to change to that page, or usetoPage(pageNumber).canPrev{Computed boolean} returns true if there is a previous page.canNext{Computed boolean} returns true if there is a next page.next{Function} moves to the next page of data.prev{Function} moves to the previous page of data.toStart{Function} moves to the first page of data.toEnd{Function} moves to the last page of data.toPage(pageNumber){Function}
Declarative vs. Imperative Flow
We stated earlier that the new useFind supports both declarative and imperative workflows. What's the difference and what does it mean in the code? The short definitions are these:
- Imperative code gives commands at each step and expects to be obeyed. The figurative verbal summary would be "Do this. Now do this. Now do that."
- Declarative code gives a full specification of how to act based on conditions. You sort of teach the code correct principles and let it govern itself. The figurative verbal summary would be "Here are instructions of how to respond to different conditions. Watch for those conditions and act accordingly."
So imperative code is like pushing instructions to the computer one line at a time. Declarative code is more like having the computer pull from a set of instructions based on conditions.
In Vue, the declarative APIs include computed and watch and other APIs like watchEffect that run by watching other values.
Declarative Example
To implement useFind declaratively, we can use computed params. The below example creates four declarative queries which watch a value called date. Suppose you have a set of tasks related to features which users can upvote. Tasks have an isCompleted attribute, an upvotes count and a dueDate property. Now let's suppose we're going to build a tasks dashboard. You want to see various types of task lists all based on a chosen date. So let's pretend that these are our requirements:
- The 5 most-upvoted tasks for the day
- The 5 least-upvoted tasks for the day
- Twenty completed tasks for the day
- The 10 most-upvoted, incomplete tasks for the day
import { useTasks } from '../stores/tasks'
const taskStore = useTasks()
const date = ref(new Date())
// 5 most-upvoted tasks for the day
const paramsMostUpvoted = computed(() => ({
query: {
dueDate: date.value,
$sort: { upvotes: -1 },
$limit: 5,
},
onServer: true
}))
const { data: mostUpvoted } = taskStore.useFind(paramsMostUpvoted)
// 5 least-upvoted tasks for the day
const paramsLeastUpvoted = computed(() => ({
query: {
dueDate: date.value,
$sort: { upvotes: 1 },
$limit: 5,
},
onServer: true
}))
const { data: leastUpvotedTasks } = taskStore.useFind(paramsLeastUpvoted)
// Twenty completed tasks for the day
const paramsComplete = computed(() => ({
query: {
dueDate: date.value,
isCompleted: true,
$limit: 20,
},
onServer: true
}))
const { data: completedTasks } = taskStore.useFind(paramsComplete)
// Ten most-voted-for, incomplete tasks for the day
const paramsIncomplete = computed(() => ({
query: {
dueDate: date.value,
isCompleted: false,
$sort: { upvotes: -1 },
$limit: 10,
},
onServer: true
}))
const { data: incompleteTasks } = taskStore.useFind(paramsIncomplete)
In the above scenario, we can bind to the task lists in the template and display the four reports. Now, what code do we need to write to show data for a different date? Let's see what a handler looks like when we have written declarative code.
Declarative Handler
With declarative code, we only need to change the date variable. The computed properties will tell useFind to fetch new data, ✨automagically✨. There's no need to manually fetch. When the data returns, the lists will update on their own. As long as your template is rendering correctly, there's no more work to do.
// A handler to change the date from the UI
const setDate = (newDate) => {
date.value = newDate
}
Imperative Example
To write the example as imperative-focused code, we only need to replace the computed properties with reactive ones. A reactive object will not autmoatically update when sub-values like date change, so we just have to pass the date to each query. Now we have more repetition. Notice how the same date is specified four times.
import { useTasks } from '../stores/tasks'
const taskStore = useTasks()
// 5 most-upvoted tasks for the day
const paramsMostUpvoted = reactive({
query: {
dueDate: new Date(),
$sort: { upvotes: -1 },
$limit: 5,
},
onServer: true
})
const { data: mostUpvoted, find: findMostUpvoted } = taskStore.useFind(paramsMostUpvoted)
// 5 least-upvoted tasks for the day
const paramsLeastUpvoted = reactive({
query: {
dueDate: new Date(),
$sort: { upvotes: 1 },
$limit: 5,
},
onServer: true
})
const { data: leastUpvotedTasks, find: findLeastUpvoted } = taskStore.useFind(paramsLeastUpvoted)
// Twenty completed tasks for the day
const paramsComplete = reactive({
query: {
dueDate: new Date(),
isCompleted: true,
$limit: 20,
},
onServer: true
})
const { data: completedTasks, find: findComplete } = taskStore.useFind(paramsComplete)
// Ten most-voted-for, incomplete tasks for the day
const paramsIncomplete = reactive({
query: {
dueDate: new Date(),
isCompleted: false,
$sort: { upvotes: -1 },
$limit: 10,
},
onServer: true
})
const { data: incompleteTasks, find: findIncomplete } = taskStore.useFind(paramsIncomplete)
Imperative Handler
What does a handler look like for an imperative-minded example of our test scenario? Let's take a look. First, we have to update each set of params, since they can't automatically compute themselves (that's what computed properties are for). Then we have to manually tell useFind to request the new data.
// A handler to change the date for each query
const setDate = async (newDate) => {
paramsMostUpvoted.query.date = newDate
paramsLeastUpvoted.query.date = newDate
paramsComplete.query.date = newDate
paramsIncomplete.query.date = newDate
// fetch data for the new date
await Promise.all([
findMostUpvoted()
findLeastUpvoted()
findComplete()
findIncomplete()
])
}
Look how much longer the imperative code is! We had to manually tell useFind to update the date in each set of params. Then we had to manually command each one to fetch the new data. With declarative-minded code, we can change the date as the source of truth. When it receives a computed property, useFind knows to re-fetch when changes occur.
So is it better to write declarative code? The answer is usually yes. It often makes the most sense to write declarative code, but some situations will work better with imperative code. When writing in Vue, sometimes declarative code will lead to infinite loops. If you have three computed variables that watch each other, they will run forever. This code would create an infinite loop:
const a = computed(() => c.value + 1)
const b = computed(() => a.value + 1)
const c = computed(() => b.value + 1)
Can you see the loop? It will start as soon as you try to read any of the variables.
- When reading
ait will try to readcbefore adding1. - Reading
cwill cause it to try to readbbefore adding1to the return value ofb. - But when reading
b, it will try to readaagain.
None of the variables will ever return a value because they'll keep reading each other in a loop. The loop will go on until the allocated memory space for tracking current operations is too full, also known as a "stack overflow".
Declarative queries can work exactly the same way. When queries re-run based on other data and that logic goes in a loop, you'll end up with an asynchronous stack overflow. In order to fix the problem, you can switch one of them to imperative to break the automated flow. That's why useFind supports both workflows.
tip
In the above scenario, if you use the feathers-batch plugins on the client and server, it will automatically group all queries into a single request. It really speeds up your API with almost zero effort on your part. Give it a try!