Socket
Socket
Sign inDemoInstall

async-selector

Package Overview
Dependencies
Maintainers
1
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

async-selector - npm Package Compare versions

Comparing version 1.0.16 to 1.0.18

2

package.json
{
"name": "async-selector",
"version": "1.0.16",
"version": "1.0.18",
"description": "Select values from databases using asynchronous selectors.",

@@ -5,0 +5,0 @@ "main": "./dist/index.js",

@@ -1,2 +0,2 @@

A simple, lightweight library inspired by [reselect](https://github.com/reduxjs/reselect) which allows you select data out of a remote database almost as easily as you would from local state. This library will normally be used in conjunction with redux and reselect but it has no dependencies.
A simple, lightweight library inspired by [reselect](https://github.com/reduxjs/reselect) which allows you select data using async functions. This package will normally be used in conjunction with redux and reselect but it has no dependencies.

@@ -8,21 +8,16 @@ # Installation

# What It solves
A normal (naive) approach to handling fetching data from a database in redux would be:
- Initiate a request in response to some event (app loaded, component mounted, search text changed, etc).
- dispatch an action to change a variable in state so you can render a loading message.
- handle a promise rejection in a similar way.
- handle a promise resolution by populating state with the new data.
# What it solves
Simply put, it solves the same problem normal selectors solve. It allows you to treat actions as the _implementation details_ of state. For example, if you were designing a search dropdown, the incorrect approach would be calculate the search results whenever a specific action is dispatched, say "TYPE_SEARCH_TEXT". This would be a brittle approach because if another developer adds a different action, say "CLEAR_TEXT" or "UNDO", the app will no longer function correctly. The search calculation should be done inside a selector whether it is done synchronously on the client or asynchronously on the server.
### Problems
This way has many common problems that just about everybody has experienced.
- An old request could potential overwrite the data of a newer request if you don't handle that edge case.
- During the time between the request being sent and the response received, you might be rendering stale data. In some cases, this can be a dangerous bug.
- Having many state variables/actions/reducers/etc for every query is a lot of redundant, tedious code.
- If you have requests that depend on previous responses, this could result in callback hell and/or difficulty in future reuse of the first response's data.
- There is no guarantee that multiple duplicate requests won't be made.
- Sending queries in response to user actions can result in brittle code and isn't really in the spirit of redux/reselect. For example, if you call a function in a server that uses many user inputs, you will have to write code to send a query for every input field. This scales poorly with the complexity of your application.
On that note, here are some concrete examples of problems async-selector solves:
- **Elegantly removes the boilerplate associated making an API call.** The reducers, actions, state variables, and middleware associated with getting the request status and results are replaced with a single selector.
- **Save/Reload will work without any added code.** You need to make sure no stale data coming from an API or request statuses are persisted or you risk buggy behavior. Treating this as derived data instantly solves this problem.
- **Undo/Redo will work without any added code.** Using selectors allow you to use derived state without knowing anything about the actions that effect it. This is critical for the scalability of more complex apps.
- **Helps guarantee stale data is never rendered.** A common bug occurs when a new request is made but the rendered data doesn't change until the response successfully returns.
- **Helps prevent unnecessary requests.** Since async selectors are memoized, duplicate requests won't be made.
- **Allows debouncing/throttling of expensive selectors.** This is extremely useful because the alternative is debouncing the actions themselves or making extra state variables which can result in extremely brittle, buggy code. A common use case would be calculations performed during zoom events.
- **Prevents over-rendering when a page loads.** In some complex apps, you make many queries when the page loads to construct the page. You can easily batch these re-render events which can improve performance on slower devices.
- **Avoids over-use of component lifecycle methods.** Ideally, React components are just functions of props and don't contain any logic other that needed for rendering the UI. Initializing data fetches onComponentDidMount is a brittle anti-pattern than can avoided with async selectors.
- **Allows seamless movement of code from the client to the server.** If, for example, you need to move an algorithm off the client because it is too computationally expensive, all you have to do is swap the reselect selector with an async selector. No far-reaching changes are necessary.
# Async selectors to the rescue!
Wouldn't it be awesome to be able treat data in database the same way you treat data in the local state? Well actually you can (almost)! An async selector is just like a normal selector except you pass in a function that returns a promise. Until the promise resolves, the selector returns a default value. But once it resolves, it returns the result of the promise. Any stale promises are automatically cancelled so you will always see up-to-date data.
# Example Usage

@@ -61,4 +56,11 @@ Here is a basic usage example:

const employees = createAsyncSelector(params, [getSearchText]);
console.log(employees(store.getState()));
console.log('Employees: ', employees(store.getState()));
// {
// value: [],
// previous: undefined,
// isWaiting: true,
// isResolved: false,
// isRejected: false,
// }
```

@@ -95,23 +97,81 @@ When you call the new selector there are three types of values it could return, depending on the status of the promise.

```
### Usage in redux
A serious problem with the above code is that when the promise resolves, the app doesn't re-render to show the new data. Instead of simply logging the employees in the onResolve callback, we need to dispatch an action to tell the app to re-render and call the selector and get the new value. This is a bit of a hack because nothing in the state was changed. One thing to make sure of is that the action changes state in some way or a re-render event won't be triggered. Another thing to be careful to avoid is recursion resulting from the action causing the inputs of the selector to change.
### Example of real usage
An important thing missing in the example above is a way to re-render the app with the new data when the promise resolves. Let's create a new action which, by convention, will do nothing other than trigger a re-render. You will need to have this new "RERENDER_APP" action handled in your reducer. How you choose to handle it is up to you, but it is important a reference to a new state object is returned so everything is correctly re-rendered. Here is an example of how you might handle it. It logs the current state of the async selector for debugging convenience.
```js
import { combineReducers, createStore } from 'redux'
const rootReducer = combineReducers({
// Your other sub-reducers go here ...
AsyncSelectorLog: (state, action) => {
if (action.type === 'RERENDER_APP') {
return { ...state, [action.key]: action.value };
}
return state;
},
});
export const store = createStore(rootReducer);
```
Now we need to make sure the onResolve and onReject callbacks actually dispatch that action. Duplicating the logic for dispatching this action for every selector is not ideal. For this reason, it is recommended you make an async-selector wrapper containing any logic you want all or most of your instances to share. For example, it could look like this.
```js
const triggerRerender = () => {
return {
type: 'RERENDER_APP',
};
};
import createAsyncSelector from 'async-selector';
import { store } from './store'
const params = {
sync: (searchText) => [],
async: getEmployees,
onResolve: (employees, searchText) => store.dispatch(triggerRerender()),
onReject: (error, searchText) => console.log(error),
onCancel: (promise, searchText) => console.log(promise),
// You could optionally throttle re-render events here.
function rerender(data, id='n/a') {
store.dispatch({
type: 'RERENDER_APP',
key: id,
value: data,
});
}
// You could optionally show an error popup to the user here
function rerenderError(error, id='n/a') {
store.dispatch({
type: 'RERENDER_APP',
key: id,
value: String(error),
});
}
export function createCustomAsyncSelector(params, ...selectors) {
return createAsyncSelector({
onResolve: result => rerender(result, params.id),
onReject: error => rerenderError(error, params.id),
...params,
}, ...selectors);
}
```
Now that all that is setup, here is how you can use your async selector in an actual redux app.
```js
import { createCustomAsyncSelector } from './custom-async-selector';
import { getSearchResults } from './search-api';
const getSearchText = state => state.searchText;
const getSearchResponse = createCustomAsyncSelector({
async: getSearchResults,
id: 'getSearchResponse', // id is so we can easily look up the result in state
}, [getSearchText]);
export const searchResults = createSelector(
[getSearchResponse], d => d.isResolved ? d.value : []
);
export const isWaitingForSearchResults = createSelector(
[getSearchResponse], d => d.isWaiting
)
export const searchResultsErrorMessage = createSelector(
[getSearchResponse], d => d.isRejected ? String(d.value) : null
)
// If creating 3 separate selectors for each of the 3 states seems boilerplate-y
// feel free have this logic in './custom-async-selector' to avoid duplicate code
```
### Chaining async calls
A very powerful feature of using selectors for async calls is the ability to elegantly create a dependency graph just like you would with normal selectors. The only issue is that the selector another selector is dependent on may be in the "isWaiting" state and the value is useless. This is where the "shouldUseAsync" function is useful. With it, you can make the selector not make async calls unless the inputs are valid.
A very powerful feature of using selectors for async calls is the ability to elegantly create a dependency graph just like you would with normal selectors. The only issue is that the selector may be in the "isWaiting" state and the value is useless. This is where the "shouldUseAsync" function is useful. With it, you can make the selector not make async calls unless the inputs are valid.
```js

@@ -122,7 +182,4 @@ // ....

const params2 = {
sync: (employees) => [],
async: getAgesForEmployees,
async: employees => getAgesForEmployees(employees.value),
onResolve: (ages, employees) => store.dispatch(triggerRerender()),
onReject: (error, employees) => console.log(error),
onCancel: (promise, employees) => console.log(promise),
shouldUseAsync: (employees) => employees.isResolved === true,

@@ -133,3 +190,16 @@ }

```
### Throttling queries
Often, you don't want to send queries too frequently. For example, if the user is typing into a textfield, you might only want to send a query after the user finished, so as to not spam the API. To solve this, you can use the "throttle" parameter.
```js
import { debounce } from 'lodash';
const params = {
sync: (searchText) => [],
async: getEmployees,
throttle: f => debounce(f, 250),
}
```
Internally, a debounced version of the selector is generated and it is (recursively) called every time the selector is called (if the inputs were changed).
### Custom caching
A common issue is the need to cache API calls. For example, if you are looking at last month's stock market data, then zoom to today's data, then zoom back to last month's data, you shouldn't have to query for a month's worth of data again. To handle this, simply put any custom caching logic in the async function you pass in. As long as the function returns a promise that resolves the same value given the same inputs, it is perfectly fine to do this. This caching logic can be as simple or complex as needed. A slight flaw in this approach is that it may not be desirable to dispatch an extra "RERENDER" action when the data already exists in memory. See the Recipes section for a solution to this.
### Handling stateful APIs

@@ -146,16 +216,2 @@ Generally, a basic assumption of a selector is the function is pure - the inputs fully determine the output. Unfortunately, that is an assumption can't always be made when querying a database. For example, you might have a button that allows the user to refresh the data if the user is worried the data was changed. Fortunately, this isn't actually a big issue thanks to the "forceUpdate" parameter!

### Throttling queries
Often, you don't want to send queries too frequently. For example, if the user is typing into a textfield, you might only want to send a query after the user finished, so as to not spam the API. To solve this, you can use the "throttle" parameter.
```js
import _ from 'underscore';
const params = {
sync: (searchText) => [],
async: getEmployees,
throttle: f => _.debounce(f, 250),
}
```
Internally, a debounced version of the selector is generated and it is (recursively) called every time the selector is called (if the inputs were changed).
### Avoiding over-rendering

@@ -176,3 +232,3 @@ A useful feature of using an async selector is that every time a new set of inputs are used, it immediately returns a default value. This avoids the dangerous bug of having data show up that looks correct while the promise is waiting to be resolved. However, in some cases flipping between a default value and a resolved value is undesirable. For example, if the user is typing and the search suggestions are constantly appearing and disappearing, it could be jarring. The simple solution is to use ".previous" instead ".value". "previous" is initially undefined, but otherwise it is the result of the most recent promise resolution.

```js
function createAsyncSelector(params, ...selectors) -> obj
function createAsyncSelector(params: Object, ...selectors) -> func
```

@@ -192,4 +248,4 @@ It outputs an object with the following form:

```js
function selector(state, ?props) -> object
selector.forceUpdate(state, ?props) -> object
function selector(state, ?props) -> any
selector.forceUpdate(state, ?props) -> any
```

@@ -238,2 +294,143 @@ ## params

```
This function is passed a selector and the new version of that function is recursively called every time the selector is called and the inputs were changed.
This function is passed a selector and the new version of that function is recursively called every time the selector is called and the inputs were changed.
# Recipes
Here are some patterns to can use to solve common problems
### Debouncing/Throttling reselect selectors
A very cool thing is the ability to throttle computationally expensive selectors. Here's a generic function that accomplishes the task
```js
import createAsyncSelector from 'async-selector';
import { createSelector } from 'reselect';
import { store, triggerRerender } from './somewhere';
export function throttleSelector(selector, throttleFunc) {
const asyncSelector = createAsyncSelector({
async: val => new Promise(res => res(val)),
throttle: throttleFunc,
onResolve: () => store.dispatch(triggerRerender()),
}, [selector]);
return createSelector(
[asyncSelector, selector],
(resp, val) => {
return resp.isResolved
? resp.value
: (resp.previous || val);
}
);
}
```
### Handling custom caching
To handle custom caching, you should simply handle the caching logic in your async function. A problem you may have is that even if the data is in memory, a promise will need to be resolved and re-render action will be dispatched. In most cases, this is such a small issue that it isn't worth worrying about. But you can avoid this extra re-render action by following this pattern.
```js
import api from './api'
import createAsyncSelector from 'async-selector';
import { createSelector } from 'reselect';
import { store, triggerRerender } from './somewhere';
const getSearchText = state => state.searchText;
const isCached = createSelector(
[getSearchText], text => api.isInCache(text)
);
const resultsFromCache = createSelector(
[isCached, getSearchText], (isCached, text) => {
if (!isCached) return undefined;
return api.getFromCache(text);
}
);
const searchResultsAsyncSelector = createAsyncSelector({
shouldUseAsync: (text, isCached) => !isCached,
async: text => api.getResults(text),
onResolve: () => store.dispatch(triggerRerender()),
}, [getSearchText, isCached]);
export const searchResults = createSelector(
[searchResultsAsyncSelector, isCached, resultsFromCache]
(resp, isCached, cachedResults) => {
if (isCached) return cachedResults;
return resp.isResolved ? resp.value : [];
}
);
export const isWaiting = createSelector(
[searchResultsAsyncSelector, isCached]
(resp, isCached, cachedResults) => {
if (isCached) return false;
return resp.isWaiting;
}
);
// If this is a common issue, you can put this logic into a helper function.
```
### Batching rerender events
A useful trick is to be able throttle re-render events for performance reasons. This can be safely done without risk of the app behaving incorrectly (but will be generally slightly slower). This is very simple to do in your async selector wrapper.
```js
import createAsyncSelector from 'async-selector';
import { store } from './store'
import { throttle } from 'lodash';
const rerender = throttle((data, id='n/a') => {
store.dispatch({
type: 'RERENDER_APP',
key: id,
value: data,
});
}, 100);
export function createCustomAsyncSelector(params, ...selectors) {
return createAsyncSelector({
onResolve: result => rerender(result, params.id),
...params,
}, ...selectors);
}
```
### Handling boilerplate
A common thing you want to do is render a loading message or an error message depending on the status of the request. You could simply pass the results of the async selector directly into the component. This may not be ideal because it tightly couples the component to the async-selector package. The other option is to create 3 selectors (results, isWaiting, errorMessage) for every async selector. This is better but can result in a lot of boilerplate. We can make a helper function to reduce this boilerplate.
```js
import createAsyncSelector from 'async-selector';
import { store } from './store'
const rerender = (data, id='n/a') => {
store.dispatch({
type: 'RERENDER_APP',
key: id,
value: data,
});
})
export function createCustomAsyncSelector(params, ...selectors) {
const asyncSelector = createAsyncSelector({
onResolve: result => rerender(result, params.id),
onReject: error => rerender(String(error), params.id),
...params,
}, ...selectors);
const isWaiting = createSelector(
[asyncSelector], d => d.isWaiting
);
const errorMessage = createSelector(
[asyncSelector], d => d.isRejected ? String(d.value) : null
);
const results = createSelector(
[asyncSelector], d => d.isResolved ? d.value : params.defaultValue
);
return [results, isWaiting, errorMessage];
}
// example usage
export const [
searchResults,
searchInProgress,
searchError,
] = createCustomAsyncSelector({
async: api.getSearchResults,
id: 'search',
defaultValue: [],
}, [state => state.searchText])
```
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc