Selector
A Selector combines providers or other selectors (what we call "dependencies"), and returns a new result.
Selectors have the same interface than providers, so views don't need to know if they are using a provider or a selector. Selectors can also be queried, and you can use the query value inside the selector dependencies.
Whenever a dependency cache is cleaned, the selector cache will also be cleaned, and will be recalculated when it is read again.
Still using v2? Then you should read the migrating from v2 to v3 guide, as the Selector dependencies API was changed in v3.
Selector(...dependencies, [options])
Arguments
...dependencies
(arguments): Dependencies can be defined in different ways. They can be providers, other selectors instances, functions returning other dependencies, promises, an array of dependencies, acatchDependency
method, which are special methods that allows to catch dependencies errors, etc. The full dependencies API is described in the chapter bellow.options
(Object): An object containing options for the selector, whose properties can be:id
(String): Id for the provider instance. It is used internally as namespace in the store. It is also useful for debugging purposes.initialState
(Object): Object containingloading
,loaded
,error
anddata
properties, which will define the initial state of the selector, before itsread
method is executed for the first time. This is useful to give a default value for the data, so you don't have to make extra format checks in your views (data && data.map
). It is also useful to define the initial loading state, which can be defined as true, which will save extra renders (as the read method is executed normally by the views theirself, the first time a selector is read it should haveloading
state as false, then immediatelytrue
, thenfalse
when data is retrieved. SettinginitialState.loading
property totrue
will avoid that extra render in the initialization). A function can be also provided, then, it will receive the currentqueryValue
as argument, and the returned object will be used asinitialState
.cleanCacheThrottle
(Number): Milliseconds. Avoids cleaning the cache more than once during this period of time. If thecleanCache
orcleanDependenciesCache
methods are called one or multiple times while they are "throttled", it will clean the cache again when the period expires. This option will be ignored if clean cache methods are called with the option{ force: true}
. Setting this option to null will remove previously defined value. This option can be changed also using the config method of the selector instance.readAgainMaxTime
(Number): Milliseconds. The default behavior of the Selectors is to read again a dependency if its cache is cleaned while the Selector is still reading another dependencies in order to provide always the last data available and not to lose any data update. This may produce a Selector never being resolved if a dependency cache is cleaned with a high frequency and dependencies take much time to be resolved. By default, Selectors will stop rereading dependencies after 5000ms. in order to avoid this possible issue. This time can be changed using this option.
Returns
A selector
instance, which methods are described in the providers and selectors methods page of these docs.
Apart of the common methods, selectors have also next getter:
dependencies
(Array) - Getter returning an array with the selector dependencies. Useful for testing purposes.
Example
import { Selector } from "@data-provider/core";
import { tasks } from "./providers"
export const completedTasks = new Selector(
tasks, // dependency as a provider instance
(queryValue, tasksResults) => tasksResults.filter(task => task.completed === false), // dependency as a function
{ // options
id: "completed-tasks",
initialState: {
loading: true,
data: []
}
}
);
console.log(completedTasks.id);
// completed-tasks
console.log(completedTasks.state);
// { loading: true, data: [], loaded: false, error: null }
This is a very basic example. Read Selector dependencies section bellow for a more detailed explanation.
Tips
- Use clear identifiers in your selectors. It will improve the development experience, as Data Provider and addons usually use them when printing messages into the console. If you do not provide one, Data Provider will assign one
uuid
automatically. - When an
id
is duplicated, Data Provider will automatically append a suffix to it and will print a warning. - Define always the
initialState
, it will save you extra format checks in your views, and will avoid an initial extra render, as described in the Arguments API.
Selector dependencies
Dependencies of selectors can be defined:
As a provider or selector instance
Use directly other selectors or providers as dependencies of your selector. You can also query them.
const tasksWithUserData = new Selector(
tasks.query({ queryString: { completed: false }}), // dependency as queried provider instance
users, // dependency as provider instance
(queryValue, tasksResults, usersResults) => tasksResults.map(
task => ({
...task,
user: usersResults.find(user.id === task.user )
})
)
);
As a function returning a dependency
You can define a dependency as a function returning another dependency in any of the formats described in this chapter. This is very powerful, because you can apply different queries to your dependencies based on your own query value, or even based on the results of the previous dependencies, which makes the Selector very composable.
The provided function should follow the next API:
dependency(queryValue, [...previousDependenciesResults])
Arguments
queryValue
(Object): Object containing the value of the query applied to the selector (undefined
when it is not queried).previousDependenciesResults
(Arguments): Arguments containing the results of the previous dependencies, in the same order they were read. When dependencies are defined as an array, their results will be received also as an array in the correspondent position.
Returns
Can return a dependency in any of the formats described in this chapter, or a plain value.
const booksWithAuthorNameInTheTitle = new Selector(
queryValue => authors.query({ urlParam: { id: queryValue.author }}),
(queryValue, authorData) => books.query({ queryString: { title_contains: authorData.name }}),
(queryValue, authorData, booksContainingAuthorNameInTheTitle) => booksContainingAuthorNameInTheTitle
);
booksWithAuthorNameInTheTitle.query({ author: 5 }).read();
// Will fetch /authors/5, then /books?title-contains=shakespeare, then return list of books
As a catchDependency
method
You can define selector dependencies using the method catchDependency
exported by the library. When used, dependencies errors will be catched and the provided catch
method result will be returned instead.
catchDependency(dependency, catchMethod)
dependency
(selector dependency): A dependency defined in any of the formats described in this chapter.catchMethod
(Function): Function that will be executed in case of the dependency is rejected with an error.
The provided catchMethod
function should follow the next API:
catchMethod(error, queryValue, [...previousDependenciesResults])
Arguments
error
(Error): Error causing the rejection of the dependency read method.queryValue
(Object): Object containing the value of the query applied to the selector (undefined
when it is not queried).previousDependenciesResults
(Arguments): Arguments containing the results of the previous dependencies, in the same order they were read. When dependencies are defined as an array, their results will be received also as an array in the correspondent position.
Returns
Can return any other dependency defined in any of the formats described in this chapter.
If you throw an error inside the catch function, or return a rejected Promise, then the dependency will be considered as errored, but with your error instead of with the original one.
import { catchDependency } from "@data-provider/core";
const selector = new Selector(
catchDependency(tasks, () => []), // returns an empty array when dependency tasks fail
catchDependency(users, () => anotherUsersOrigin),// retrieve users from other origin when users fails
(queryValue, tasksResults, usersResults) => tasksResults
);
As an array of dependencies
Defining dependencies as an array will result in reading them in parallel. Take into account that you will receive the results of the dependencies also inside an array:
const selector = new Selector(
[ // arrays of dependencies are read in parallel
tasks,
users
],
(queryValue, [tasksResults, usersResults]) => tasksResults.map(
task => ({
...task,
user: usersResults.find(user.id === task.user )
})
)
);
Arrays of dependencies are in fact dependencies, and accept any type of dependencies inside, so you could combine them in any of the formats described in this chapter:
const selector = new Selector(
books, // First read books
[ // Then read tasks and authors in parallel
tasks,
queryValue => authors.query(queryValue)
],
(queryValue, booksResults, [tasksResults, usersResults]) => {
// Do your stuff here
}
);
As a Promise
You can use a promise directly as a Selector dependency. Resolved value will be the result of the dependency, and it will be considered as errored in case the Promise is rejected. Take into account that Promises can be used, but you should use them only in exceptional cases, because one of the main features of the selectors, which is cleaning their own cache when any of the dependencies cache is cleaned obviously will not work in case of native Promises.
const selector = new Selector(
new Promise(resolve => {
setTimeout(() => {
resolve("foo");
}, 3000);
}), // Will wait 3 seconds before reading tasks
tasks,
(queryValue, promiseResult, tasksResults) => {
console.log(promiseResult);
// foo
}
);
As a Promise returning a dependency
Promises can also return another dependency. Then, the received result will be the result returned by the dependency, and, in this case, if the dependency is a provider, the selector cache will also be cleaned when the dependency cache is cleaned.
const selector = new Selector(
(queryValue) => {
return somePromise(queryValue)
.then(() => tasksProvider.query({ resolved: true }))
.catch(() => tasksProvider.query({ resolved: false }))
},
(queryValue, tasksResults) => {
console.log(tasksResults);
return tasksResults;
}
);
As any value
Any value? Yes, if you provide any other value, as a String
, Number
, null
, undefined
... the "dependency" will be resolved with that value.
const selector = new Selector(
books,
(queryValue, booksResults) => {
if (!booksResults.length) {
return []; // There are no books, I don't mind the authors, return empty array.
}
return authors; // There are books. Let's read the "authors" provider.
},
(queryValue, booksResults, authorsResults) => {
return booksResults.map(book => {
return {
...book,
authorName: authorsResults.find(author => author.id === book.author)
}
});
}
);
Tips
- You can combine all described formats of dependencies as you want.
- The power of the dependencies API should allow you to retrieve all data you need using a single selector, but a better approach is to create one different selector for each level of granularity, so they can be used separately when needed. As they are composable, you can combine those selectors into another one, and so on... So, better use selectors composition instead of defining lots of dependencies in a single selector.
You have more examples available about how to use selector dependencies in the guides page.