Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

react-redux-query

Package Overview
Dependencies
Maintainers
1
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-redux-query - npm Package Compare versions

Comparing version 0.4.0 to 0.6.0

.eslintrc.js

4

actions.js

@@ -30,4 +30,4 @@ "use strict";

/**
* Update query data. key is usually unique per URL path, and should
* probably be similar to URL path.
* Updates query data. key is usually unique per URL path, and should probably
* be similar to URL path.
*

@@ -34,0 +34,0 @@ * This is meant for internal use; data contains query metadata that client code

@@ -18,5 +18,5 @@ import { QueryData } from './query'

export interface Update<QR> {
export interface Update<R> {
key: string
updater: (response: QR | undefined) => QR | undefined | null
updater: (response: R | undefined) => R | undefined | null
}

@@ -30,3 +30,3 @@ /**

*/
export function update<QR extends {} = any>(payload: Update<QR>): Action {
export function update<R extends {} = any>(payload: Update<R>): Action {
return {

@@ -43,4 +43,4 @@ type: 'REACT_REDUX_QUERY_UPDATE_RESPONSE',

/**
* Update query data. key is usually unique per URL path, and should
* probably be similar to URL path.
* Updates query data. key is usually unique per URL path, and should probably
* be similar to URL path.
*

@@ -47,0 +47,0 @@ * This is meant for internal use; data contains query metadata that client code

{
"name": "react-redux-query",
"version": "0.4.0",
"version": "0.6.0",
"author": "Kyle Bebak <kylebebak@gmail.com>",

@@ -30,3 +30,3 @@ "description": "React hooks and functions for SWR-style data fetching, caching and automatic updates, backed by Redux",

"hooks": {
"pre-push": "yarn prettier-check"
"pre-push": "yarn test"
}

@@ -39,4 +39,2 @@ },

"ava": "^3.13.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.11.0",
"husky": "^4.3.0",

@@ -43,0 +41,0 @@ "node-fetch": "^2.6.1",

@@ -60,9 +60,2 @@ "use strict";

};
var __spreadArrays = (this && this.__spreadArrays) || function () {
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
for (var r = Array(s), k = 0, i = 0; i < il; i++)
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
r[k] = a[j];
return r;
};
Object.defineProperty(exports, "__esModule", { value: true });

@@ -76,8 +69,8 @@ exports.useData = exports.getData = exports.usePoll = exports.useQuery = exports.query = exports.ConfigContext = void 0;

/**
* Calls fetcher and awaits raw response. Saves response to query branch under
* Calls fetcher and awaits response. Saves response to query branch under
* key and returns response. What is saved to Redux depends on the value of
* response.queryResponse:
*
* - If response.queryResponse isn't set, save raw response
* - If response.queryResponse isn't set, and raw response is null or undefined,
* - If response.queryResponse isn't set, save response
* - If response.queryResponse isn't set, and response is null or undefined,
* don't save anything

@@ -89,19 +82,20 @@ * - If response.queryResponse is set, save queryResponse

* @param key - Key in query branch under which to store response
* @param fetcher - Function that returns raw response with optional
* queryResponse property
* @param fetcher - Function that returns response with optional queryResponse
* property
* @param options:
* dispatch - Dispatch function to send response to store
* dedupe - Don't call fetcher if there's another request in flight for key
* dedupeMs - If dedupe is true, dedupe behavior active for this many ms
* dispatch - Dispatch function to send response to store
* dedupe - Don't call fetcher if another request was recently sent for key
* dedupeMs - If dedupe is true, dedupe behavior active for this many ms
*
* @returns Raw response, or undefined if fetcher call gets deduped, or
* undefined if fetcher throws error
* @returns Response, or undefined if fetcher call gets deduped, or undefined if
* fetcher throws error
*/
function query(key, fetcher, options) {
var _a, _b;
return __awaiter(this, void 0, void 0, function () {
var dispatch, _a, dedupe, _b, dedupeMs, _c, catchError, before, fetchState, response, e_1, after, queryResponse;
return __generator(this, function (_d) {
switch (_d.label) {
var dispatch, _c, dedupe, _d, dedupeMs, _e, catchError, before, fetchState, fetchMs, inFlight, counter, requestId, _loop_1, state_1, response, error, e_1, afterMs, queryResponse;
return __generator(this, function (_f) {
switch (_f.label) {
case 0:
dispatch = options.dispatch, _a = options.dedupe, dedupe = _a === void 0 ? false : _a, _b = options.dedupeMs, dedupeMs = _b === void 0 ? 2000 : _b, _c = options.catchError, catchError = _c === void 0 ? true : _c;
dispatch = options.dispatch, _c = options.dedupe, dedupe = _c === void 0 ? false : _c, _d = options.dedupeMs, dedupeMs = _d === void 0 ? 2000 : _d, _e = options.catchError, catchError = _e === void 0 ? true : _e;
before = Date.now();

@@ -111,26 +105,45 @@ fetchState = fetchStateByKey[key];

return [2 /*return*/];
fetchStateByKey[key] = { fetchMs: before };
dispatch(actions_1.updateData({ key: key, data: { fetchMs: before } }));
_d.label = 1;
fetchMs = before;
inFlight = ((_a = fetchStateByKey[key]) === null || _a === void 0 ? void 0 : _a.inFlight) || [];
counter = 0;
requestId = '';
_loop_1 = function () {
var id = fetchMs + "-" + counter;
if (!inFlight.find(function (data) { return data.id === id; })) {
inFlight.push({ id: id, fetchMs: fetchMs });
requestId = id;
return "break";
}
counter += 1;
};
while (true) {
state_1 = _loop_1();
if (state_1 === "break")
break;
}
// Notify client that fetcher will be called
fetchStateByKey[key] = { fetchMs: fetchMs, inFlight: inFlight };
dispatch(actions_1.updateData({ key: key, data: { fetchMs: fetchMs, inFlight: inFlight } }));
response = undefined;
_f.label = 1;
case 1:
_d.trys.push([1, 3, 4, 5]);
_f.trys.push([1, 3, , 4]);
return [4 /*yield*/, fetcher()];
case 2:
response = _d.sent();
return [3 /*break*/, 5];
response = _f.sent();
return [3 /*break*/, 4];
case 3:
e_1 = _d.sent();
if (catchError) {
// If catchError is true, save error
dispatch(actions_1.updateData({ key: key, data: { error: e_1, errorMs: Date.now(), fetchMs: undefined } }));
return [2 /*return*/];
e_1 = _f.sent();
error = e_1 || {};
return [3 /*break*/, 4];
case 4:
afterMs = Date.now();
inFlight = (((_b = fetchStateByKey[key]) === null || _b === void 0 ? void 0 : _b.inFlight) || []).filter(function (data) { return data.id !== requestId; });
// If error was thrown, notify client and bail out
if (error) {
dispatch(actions_1.updateData({ key: key, data: { error: error, errorMs: afterMs, inFlight: inFlight } }));
if (catchError)
return [2 /*return*/];
throw error;
}
else
throw e_1;
return [3 /*break*/, 5];
case 4:
fetchStateByKey[key] = undefined;
return [7 /*endfinally*/];
case 5:
after = Date.now();
if (response === null || response === void 0 ? void 0 : response.hasOwnProperty('queryResponse')) {

@@ -140,7 +153,7 @@ queryResponse = response.queryResponse;

// If response.queryResponse is set and is neither null nor undefined, save it as response
dispatch(actions_1.updateData({ key: key, data: { response: __assign({}, queryResponse), responseMs: after, fetchMs: undefined } }));
dispatch(actions_1.updateData({ key: key, data: { response: __assign({}, queryResponse), responseMs: afterMs, inFlight: inFlight } }));
}
else {
// If response.queryResponse is set but is null or undefined, save response as error
dispatch(actions_1.updateData({ key: key, data: { error: __assign({}, response), errorMs: after, fetchMs: undefined } }));
dispatch(actions_1.updateData({ key: key, data: { error: __assign({}, response), errorMs: afterMs, inFlight: inFlight } }));
}

@@ -150,3 +163,3 @@ }

// If response.queryResponse isn't set, only save response if it's neither null nor undefined
dispatch(actions_1.updateData({ key: key, data: { response: __assign({}, response), responseMs: after, fetchMs: undefined } }));
dispatch(actions_1.updateData({ key: key, data: { response: __assign({}, response), responseMs: afterMs, inFlight: inFlight } }));
}

@@ -167,10 +180,10 @@ return [2 /*return*/, response];

* @param key - Key in query branch under which to store response; passing
* null/undefined ensures function is NOOP that returns undefined
* @param fetcher - Function that returns raw response with optional
* queryResponse property
* @param options - Query options options, plus:
* noRefetch - Don't refetch if there's already response at key
* noRefetchMs - If noRefetch is true, noRefetch behavior active for this
* many ms (forever by default)
* refetchKey - Pass in new value to force refetch without changing key
* null/undefined ensures function is NOOP that returns undefined
* @param fetcher - Function that returns response with optional queryResponse
* property
* @param options - Query options options and data options, plus:
* noRefetch - Don't refetch if there's already response at key
* noRefetchMs - If noRefetch is true, noRefetch behavior active for this
* many ms (forever by default)
* refetchKey - Pass in new value to force refetch without changing key
*

@@ -181,6 +194,6 @@ * @returns Query data

if (options === void 0) { options = {}; }
var _a = options.dataKeys, dataKeys = _a === void 0 ? [] : _a, _b = options.noRefetch, noRefetch = _b === void 0 ? false : _b, _c = options.noRefetchMs, noRefetchMs = _c === void 0 ? 0 : _c, refetchKey = options.refetchKey, rest = __rest(options, ["dataKeys", "noRefetch", "noRefetchMs", "refetchKey"]);
var dataKeys = options.dataKeys, compare = options.compare, _a = options.noRefetch, noRefetch = _a === void 0 ? false : _a, _b = options.noRefetchMs, noRefetchMs = _b === void 0 ? 0 : _b, refetchKey = options.refetchKey, rest = __rest(options, ["dataKeys", "compare", "noRefetch", "noRefetchMs", "refetchKey"]);
var dispatch = react_redux_1.useDispatch();
var config = react_1.useContext(exports.ConfigContext);
var data = useData.apply(void 0, __spreadArrays([key], dataKeys));
var data = useData(key, { dataKeys: dataKeys, compare: compare });
react_1.useEffect(function () {

@@ -214,7 +227,7 @@ if (data.response && noRefetch) {

* @param key - Key in query branch under which to store response; passing
* null/undefined ensures function is NOOP that returns undefined
* @param fetcher - Function that returns raw response with queryResponse
* property
* @param options - Query options, plus:
* intervalMs - Interval between end of fetcher call and next fetcher call
* null/undefined ensures function is NOOP that returns undefined
* @param fetcher - Function that returns response with optional queryResponse
* property
* @param options - Query options and data options, plus:
* intervalMs - Interval between end of fetcher call and next fetcher call
*

@@ -225,3 +238,3 @@ * @returns Query data

var _this = this;
var _a = options.dataKeys, dataKeys = _a === void 0 ? [] : _a, intervalMs = options.intervalMs, rest = __rest(options, ["dataKeys", "intervalMs"]);
var dataKeys = options.dataKeys, compare = options.compare, intervalMs = options.intervalMs, rest = __rest(options, ["dataKeys", "compare", "intervalMs"]);
var dispatch = react_redux_1.useDispatch();

@@ -256,3 +269,3 @@ var config = react_1.useContext(exports.ConfigContext);

}, [key, intervalMs]); // eslint-disable-line
return useData.apply(void 0, __spreadArrays([key], dataKeys));
return useData(key, { dataKeys: dataKeys, compare: compare });
}

@@ -266,17 +279,15 @@ exports.usePoll = usePoll;

* @param key - Key in query branch
* @param dataKeys - Keys in query data
* @param options:
* dataKeys - Keys in query data
*
* @returns Query data at key if present, or object with subset of properties
* specified by dataKeys
* @returns Query data at key, with subset of properties specified by dataKeys
*/
function getData(queryState, key) {
var dataKeys = [];
for (var _i = 2; _i < arguments.length; _i++) {
dataKeys[_i - 2] = arguments[_i];
}
function getData(queryState, key, options) {
if (options === void 0) { options = {}; }
var _a = options.dataKeys, dataKeys = _a === void 0 ? [] : _a;
if (!key)
return;
return {};
var data = queryState[key];
if (!data)
return data;
return {};
var partialData = {

@@ -288,7 +299,7 @@ response: data.response,

fetchMs: undefined,
saveMs: undefined,
inFlight: undefined,
};
// @ts-ignore
for (var _a = 0, dataKeys_1 = dataKeys; _a < dataKeys_1.length; _a++) {
var dataKey = dataKeys_1[_a];
for (var _i = 0, dataKeys_1 = dataKeys; _i < dataKeys_1.length; _i++) {
var dataKey = dataKeys_1[_i];
partialData[dataKey] = data[dataKey];

@@ -305,15 +316,16 @@ }

* @param key - Key in query branch
* @param dataKeys - Keys in query data
* @param options:
* dataKeys - Keys in query data
* compare - Equality function compares previous query data with next query
* data; if it returns false, component rerenders, else it doesn't; uses
* shallowEqual by default
*
* @returns Query data at key if present, or object with subset of properties
* specified by dataKeys
* @returns Query data at key, with subset of properties specified by dataKeys
*/
function useData(key) {
var dataKeys = [];
for (var _i = 1; _i < arguments.length; _i++) {
dataKeys[_i - 1] = arguments[_i];
}
var _a = react_1.useContext(exports.ConfigContext).branchName, branchName = _a === void 0 ? 'query' : _a;
return (react_redux_1.useSelector(function (state) { return getData.apply(void 0, __spreadArrays([state[branchName], key], dataKeys)); }, react_redux_1.shallowEqual) || {});
function useData(key, options) {
if (options === void 0) { options = {}; }
var dataKeys = options.dataKeys, compare = options.compare;
var _a = react_1.useContext(exports.ConfigContext), _b = _a.branchName, branchName = _b === void 0 ? 'query' : _b, configDataKeys = _a.dataKeys, configCompare = _a.compare;
return react_redux_1.useSelector(function (state) { return getData(state[branchName], key, { dataKeys: dataKeys || configDataKeys }); }, compare || configCompare || react_redux_1.shallowEqual);
}
exports.useData = useData;

@@ -7,3 +7,5 @@ import { createContext, useEffect, useRef, useContext } from 'react'

const fetchStateByKey: { [key: string]: { fetchMs: number } | undefined } = {}
const fetchStateByKey: {
[key: string]: { fetchMs: number; inFlight: { id: string; fetchMs: number }[] } | undefined
} = {}

@@ -15,19 +17,21 @@ export const ConfigContext = createContext<{

catchError?: boolean
dataKeys?: DataKey[]
compare?: (prev: QueryData<{}>, next: QueryData<{}>) => boolean
}>({})
interface State<QR extends {} = {}, ER = {}> {
query: QueryState<QR, ER>
interface State<R extends {} = {}> {
query: QueryState<R>
}
export interface QueryState<QR extends {} = any, ER = {}> {
[key: string]: QueryData<QR, ER> | undefined
export interface QueryState<R extends {} = any> {
[key: string]: QueryData<R> | undefined
}
export type QueryData<QR extends {} = {}, ER = {}> = {
response?: QR
export type QueryData<R extends {} = {}> = {
response?: R
responseMs?: number
error?: ER
error?: {}
errorMs?: number
fetchMs?: number
saveMs?: number
inFlight?: { id: string; fetchMs: number }[]
}

@@ -37,3 +41,3 @@

export type RawResponse<RR extends {}, QR extends {}> = RR & { queryResponse?: QR | null }
export type QueryResponse<R extends {}> = R | { queryResponse?: R | null }

@@ -46,9 +50,14 @@ export interface QueryOptions {

export interface DataOptions<R> {
dataKeys?: DataKey[]
compare?: (prev: QueryData<R>, next: QueryData<R>) => boolean
}
/**
* Calls fetcher and awaits raw response. Saves response to query branch under
* Calls fetcher and awaits response. Saves response to query branch under
* key and returns response. What is saved to Redux depends on the value of
* response.queryResponse:
*
* - If response.queryResponse isn't set, save raw response
* - If response.queryResponse isn't set, and raw response is null or undefined,
* - If response.queryResponse isn't set, save response
* - If response.queryResponse isn't set, and response is null or undefined,
* don't save anything

@@ -60,15 +69,15 @@ * - If response.queryResponse is set, save queryResponse

* @param key - Key in query branch under which to store response
* @param fetcher - Function that returns raw response with optional
* queryResponse property
* @param fetcher - Function that returns response with optional queryResponse
* property
* @param options:
* dispatch - Dispatch function to send response to store
* dedupe - Don't call fetcher if there's another request in flight for key
* dedupeMs - If dedupe is true, dedupe behavior active for this many ms
* dispatch - Dispatch function to send response to store
* dedupe - Don't call fetcher if another request was recently sent for key
* dedupeMs - If dedupe is true, dedupe behavior active for this many ms
*
* @returns Raw response, or undefined if fetcher call gets deduped, or
* undefined if fetcher throws error
* @returns Response, or undefined if fetcher call gets deduped, or undefined if
* fetcher throws error
*/
export async function query<RR extends { queryResponse?: {} | null } | {} | null | undefined>(
export async function query<R extends { queryResponse?: {} | null } | {} | null | undefined>(
key: string,
fetcher: () => Promise<RR>,
fetcher: () => Promise<R>,
options: QueryOptions & { dispatch: Dispatch },

@@ -78,2 +87,3 @@ ) {

// Bail out if dedupe is true and another request was recently sent for key
const before = Date.now()

@@ -83,21 +93,42 @@ const fetchState = fetchStateByKey[key]

let response: RR
const fetchMs = before
let inFlight = fetchStateByKey[key]?.inFlight || []
fetchStateByKey[key] = { fetchMs: before }
dispatch(updateData({ key, data: { fetchMs: before } }))
// Create unique id for in-flight request, and add it to inFlight array
let counter = 0
let requestId = ''
while (true) {
const id = `${fetchMs}-${counter}`
if (!inFlight.find((data) => data.id === id)) {
inFlight.push({ id, fetchMs })
requestId = id
break
}
counter += 1
}
// Notify client that fetcher will be called
fetchStateByKey[key] = { fetchMs, inFlight }
dispatch(updateData({ key, data: { fetchMs, inFlight } }))
// Call fetcher
let response: R = undefined as R
let error: undefined | {}
try {
response = await fetcher()
} catch (e) {
if (catchError) {
// If catchError is true, save error
dispatch(updateData({ key, data: { error: e, errorMs: Date.now(), fetchMs: undefined } }))
return
} else throw e
} finally {
fetchStateByKey[key] = undefined
error = e || {}
}
const after = Date.now()
// Remove request from inFlight array
const afterMs = Date.now()
inFlight = (fetchStateByKey[key]?.inFlight || []).filter((data) => data.id !== requestId)
// If error was thrown, notify client and bail out
if (error) {
dispatch(updateData({ key, data: { error, errorMs: afterMs, inFlight } }))
if (catchError) return
throw error
}
if (response?.hasOwnProperty('queryResponse')) {

@@ -107,10 +138,10 @@ const { queryResponse } = response as { queryResponse?: {} | null }

// If response.queryResponse is set and is neither null nor undefined, save it as response
dispatch(updateData({ key, data: { response: { ...queryResponse }, responseMs: after, fetchMs: undefined } }))
dispatch(updateData({ key, data: { response: { ...queryResponse }, responseMs: afterMs, inFlight } }))
} else {
// If response.queryResponse is set but is null or undefined, save response as error
dispatch(updateData({ key, data: { error: { ...response } as {}, errorMs: after, fetchMs: undefined } }))
dispatch(updateData({ key, data: { error: { ...response } as {}, errorMs: afterMs, inFlight } }))
}
} else if (response !== null && response !== undefined) {
// If response.queryResponse isn't set, only save response if it's neither null nor undefined
dispatch(updateData({ key, data: { response: { ...response } as {}, responseMs: after, fetchMs: undefined } }))
dispatch(updateData({ key, data: { response: { ...response } as {}, responseMs: afterMs, inFlight } }))
}

@@ -129,23 +160,23 @@

* @param key - Key in query branch under which to store response; passing
* null/undefined ensures function is NOOP that returns undefined
* @param fetcher - Function that returns raw response with optional
* queryResponse property
* @param options - Query options options, plus:
* noRefetch - Don't refetch if there's already response at key
* noRefetchMs - If noRefetch is true, noRefetch behavior active for this
* many ms (forever by default)
* refetchKey - Pass in new value to force refetch without changing key
* null/undefined ensures function is NOOP that returns undefined
* @param fetcher - Function that returns response with optional queryResponse
* property
* @param options - Query options options and data options, plus:
* noRefetch - Don't refetch if there's already response at key
* noRefetchMs - If noRefetch is true, noRefetch behavior active for this
* many ms (forever by default)
* refetchKey - Pass in new value to force refetch without changing key
*
* @returns Query data
*/
export function useQuery<RR, QR = RR>(
export function useQuery<R>(
key: string | null | undefined,
fetcher: (() => Promise<RawResponse<RR, QR>>) | null | undefined,
options: QueryOptions & { dataKeys?: DataKey[]; noRefetch?: boolean; noRefetchMs?: number; refetchKey?: any } = {},
fetcher: (() => Promise<QueryResponse<R>>) | null | undefined,
options: QueryOptions & DataOptions<R> & { noRefetch?: boolean; noRefetchMs?: number; refetchKey?: any } = {},
) {
const { dataKeys = [], noRefetch = false, noRefetchMs = 0, refetchKey, ...rest } = options
const { dataKeys, compare, noRefetch = false, noRefetchMs = 0, refetchKey, ...rest } = options
const dispatch = useDispatch()
const config = useContext(ConfigContext)
const data = useData<QR>(key, ...dataKeys)
const data = useData<R>(key, { dataKeys, compare })

@@ -178,16 +209,16 @@ useEffect(() => {

* @param key - Key in query branch under which to store response; passing
* null/undefined ensures function is NOOP that returns undefined
* @param fetcher - Function that returns raw response with queryResponse
* property
* @param options - Query options, plus:
* intervalMs - Interval between end of fetcher call and next fetcher call
* null/undefined ensures function is NOOP that returns undefined
* @param fetcher - Function that returns response with optional queryResponse
* property
* @param options - Query options and data options, plus:
* intervalMs - Interval between end of fetcher call and next fetcher call
*
* @returns Query data
*/
export function usePoll<RR, QR = RR>(
export function usePoll<R>(
key: string | null | undefined,
fetcher: (() => Promise<RawResponse<RR, QR>>) | null | undefined,
options: QueryOptions & { dataKeys?: DataKey[]; intervalMs: number },
fetcher: (() => Promise<QueryResponse<R>>) | null | undefined,
options: QueryOptions & DataOptions<R> & { intervalMs: number },
) {
const { dataKeys = [], intervalMs, ...rest } = options
const { dataKeys, compare, intervalMs, ...rest } = options
const dispatch = useDispatch()

@@ -217,3 +248,3 @@ const config = useContext(ConfigContext)

return useData<QR>(key, ...dataKeys)
return useData<R>(key, { dataKeys, compare })
}

@@ -227,13 +258,19 @@

* @param key - Key in query branch
* @param dataKeys - Keys in query data
* @param options:
* dataKeys - Keys in query data
*
* @returns Query data at key if present, or object with subset of properties
* specified by dataKeys
* @returns Query data at key, with subset of properties specified by dataKeys
*/
export function getData<QR>(queryState: QueryState<QR>, key: string | null | undefined, ...dataKeys: DataKey[]) {
if (!key) return
export function getData<R>(
queryState: QueryState<R>,
key: string | null | undefined,
options: { dataKeys?: DataKey[] } = {},
) {
const { dataKeys = [] } = options
if (!key) return {}
const data = queryState[key]
if (!data) return data
if (!data) return {}
const partialData: QueryData<QR> = {
const partialData: QueryData<R> = {
response: data.response,

@@ -244,3 +281,3 @@ responseMs: data.responseMs,

fetchMs: undefined,
saveMs: undefined,
inFlight: undefined,
}

@@ -258,12 +295,18 @@ // @ts-ignore

* @param key - Key in query branch
* @param dataKeys - Keys in query data
* @param options:
* dataKeys - Keys in query data
* compare - Equality function compares previous query data with next query
* data; if it returns false, component rerenders, else it doesn't; uses
* shallowEqual by default
*
* @returns Query data at key if present, or object with subset of properties
* specified by dataKeys
* @returns Query data at key, with subset of properties specified by dataKeys
*/
export function useData<QR>(key: string | null | undefined, ...dataKeys: DataKey[]) {
const { branchName = 'query' } = useContext(ConfigContext)
return (
useSelector((state: State<QR>) => getData<QR>(state[branchName as 'query'], key, ...dataKeys), shallowEqual) || {}
export function useData<R>(key: string | null | undefined, options: DataOptions<R> = {}) {
const { dataKeys, compare } = options
const { branchName = 'query', dataKeys: configDataKeys, compare: configCompare } = useContext(ConfigContext)
return useSelector(
(state: State<R>) => getData<R>(state[branchName as 'query'], key, { dataKeys: dataKeys || configDataKeys }),
compare || configCompare || shallowEqual,
)
}

@@ -16,6 +16,168 @@ # react-redux-query

Coming soon
RRQ's main hook, `useQuery`, fetches data, throws it into Redux, and rerenders your components whenever data changes.
It take 3 arguments, `(key: string, fetcher: () => Promise<{}>, options: {})`, and returns the cached data in Redux under `key`. It connects your component to Redux with `useSelector`, so it subscribes to data changes whenever they occur. This means your component always rerenders with the most recently fetched data saved at `key`.
```ts
import { useQuery } from 'react-redux-query'
function Profile() {
const { response } = useQuery('user', service.getLoggedInUser)
if (!response) return <div>Loading...</div>
return <div>Hello {response.data.name}!</div>
}
```
If you want to make sure you don't throw an error response into Redux and overwrite good data with bad, you can have your fetcher return `null` or `undefined`:
```ts
function Profile() {
const { response } = useQuery('user', async () => {
const res = await service.getLoggedInUser()
return res.status === 200 ? res : null
})
// ...
}
```
Or you can set the `queryResponse` property in the object returned by `fetcher`:
```ts
function Profile() {
const { response, error } = useQuery(
'user',
async () => {
const res = await service.getLoggedInUser()
return { ...res, queryResponse: res.status === 200 ? res : null }
},
{ dataKeys: ['error'] },
)
// ...
}
```
This way you can still return the entire response from your fetcher, even if it's a "bad" response, while instructing RRQ to not overwrite your `response` data in Redux. In this case, the `error` property would contain the response for status codes other than `200`, or an error object if fetcher throws an error.
### `usePoll`
`usePoll` is the same as `useQuery`, except that it requires an `intervalMs` property in the options. After `fetcher` returns, it's called again after `intervalMs`. The actual polling interval depends on how long fetcher takes to return, which means polling interval adapts to network and server speed.
### Setup
RRQ uses Redux to cache fetched data, and allows components to subscribe to changes in fetched data. To use RRQ in your app, you need to use [Redux](https://react-redux.js.org/).
```ts
import { combineReducers, createStore } from 'redux'
import { reducer as queryReducer } from 'react-redux-query'
const rootReducer = combineReducers({ ...myOtherReducers, query: queryReducer })
const store = createStore(rootReducer, {})
// ...
const App = () => {
return (
<Provider store={store}>
<MyApp />
</Provider>
)
}
```
> The default name of the RRQ branch in your Redux state tree is `query`. [See below](#custom-config-context) how to use a custom branch name.
### `query` function
RRQ also exports a lower-level async `query` function that has the same signature as the hooks `(key: string, fetcher: () => Promise<{}>, options: {})`.
This function is used by the `useQuery` and `usePoll` hooks. It calls `fetcher`, awaits the response, throws it into Redux if appropriate, and returns the response as-is.
You should use this function wherever you want to fetch and cache data outside of the render lifecycle. For example, in a save user callback:
```ts
import { query } from 'react-redux-query'
const handleSaveUser = async (userId) => {
await saveUser(userId)
const res = await query(`user/${userId}`, () => fetchUser(userId), { dispatch })
if (res.status !== '200') {
handleError()
}
}
```
The `options` object must contain a `dispatch` property with the Redux dispatch function (dispatch is used to throw the response into Redux). Feel free to write a wrapper around `query` that passes in `dispatch` for you if you don't want to pass it in every time.
### `useData` hook
If you just want to subscribe to data changes without sending a request, use the `useData` hook (which is used by `useQuery` and `usePoll` under the hood).
It takes a `key` and an `options` object (it omits the `fetcher`). It [connects your component to Redux](https://react-redux.js.org/api/hooks#useselector) and returns the data object at `key`, with a subset of properties specified by `options.dataKeys`. To avoid unnecessary rerenders, only `response` and `responseMs` are included by default.
You can pass an array of additional keys (`'error'`, `'errorMs'`, `'fetchMs'`, `'inFlight'`) to subscribe to changes in these metadata properties as well.
To control whether your component rerenders when data changes, you can pass in a custom equality comparator using `options.compare`. This function takes previous data and next data as args. If it returns false, your connected component rerenders, else it doesn't. It uses `shallowEqual` by default, which means any change in the `data.response` object triggers a rerender.
### Redux actions
RRQ ships with the following [Redux actions](https://redux.js.org/faq/actions):
- `save`: stores fetcher response
- `update`: like save, but takes an updater function, which receives the response at key and must return a response, `undefined`, or `null`; returning `undefined` is a NOOP, while returning `null` removes data at key from query branch
- `updateData`: updates data (mainly for internal use, because it can modify query metadata)
These are really action creators (functions that return action objects). You can use the first two to overwrite the response at a given key in the query branch. For example, in a save user callback:
```ts
import { update } from 'react-redux-query'
const handleSaveUser = async (userId, body) => {
const res = await saveUser(userId, body)
dispatch(
update({
key: `user/${userId}`,
updater: (prevRes) => {
return { ...prevRes, data: { ...prevRes.data, ...res.data } }
},
}),
)
}
```
### Custom config context
RRQ's default behavior can be configured using `ConfigContext`, which has the following properties:
```ts
branchName?: string
dedupe?: boolean
dedupeMs?: number
catchError?: boolean
dataKeys?: DataKey[]
compare?: (prev: QueryData<{}>, next: QueryData<{}>) => boolean
```
Import `ConfigContext`, and wrap any part of your render tree with `ConfigContext.Provider`:
```ts
import { ConfigContext } from 'react-redux-query'
// ...
;<ConfigContext.Provider value={{ branchName: 'customQueryBranchName', catchError: true }}>
<MyApp />
</ConfigContext.Provider>
```
`ConfigContext` uses React's context API. This config applies to all hooks in your app under the context provider.
### Full API
For thorough doc comments, function signatures and type definitions, [see here](./query.ts).
For action creators, [see here](./actions.ts).
### TypeScript
**react-redux-query** works great with TypeScript (it's written in TypeScript).
Make sure you enable `esModuleInterop` if you're using TypeScript to compile your application. This option is enabled by default if you run `tsc --init`.

@@ -25,4 +187,9 @@

Coming soon
Why not SWR or React Query?
- uses Redux for data persistence and automatic updates; performant, community-standard solution for managing application state; easy to modify and subscribe to stored data, and easy to extend RRQ's read/write behavior by writing your own selectors/actions
- `queryResponse` property makes it easy to transform fetcher response before caching it, or instruct RRQ not to cache response at all, without changing shape of response or making it null
- first class TypeScript support; RRQ is written in TypeScript, and hook return types are seamlessly inferred from fetcher return types
- small and simple codebase; RRQ weighs less than 3kb minzipped
## Dependencies

@@ -34,2 +201,6 @@

Coming soon
Clone the repo, then `yarn`, then `yarn test`. This runs tests that on the vanilla JS parts of RRQ, but none of the React hooks.
To test the React hooks, run `cd test_app`, then `yarn`, then `yarn prepare`.
Then run `yarn start` or `yarn test` to run React test app or to run tests on test app.

@@ -38,7 +38,7 @@ "use strict";

if (state === void 0) { state = {}; }
var saveMs = Date.now();
var responseMs = Date.now();
switch (action.type) {
case 'REACT_REDUX_QUERY_SAVE_RESPONSE': {
var _e = action.payload, key = _e.key, response = _e.response;
return __assign(__assign({}, state), (_a = {}, _a[key] = __assign(__assign({}, state[key]), { response: __assign({}, response), saveMs: saveMs }), _a));
return __assign(__assign({}, state), (_a = {}, _a[key] = __assign(__assign({}, state[key]), { response: __assign({}, response), responseMs: responseMs }), _a));
}

@@ -54,7 +54,7 @@ case 'REACT_REDUX_QUERY_UPDATE_RESPONSE': {

}
return __assign(__assign({}, state), (_b = {}, _b[key] = __assign(__assign({}, state[key]), { response: __assign({}, res), saveMs: saveMs }), _b));
return __assign(__assign({}, state), (_b = {}, _b[key] = __assign(__assign({}, state[key]), { response: __assign({}, res), responseMs: responseMs }), _b));
}
case 'REACT_REDUX_QUERY_UPDATE_DATA': {
var _j = action.payload, key = _j.key, data = _j.data;
return __assign(__assign({}, state), (_c = {}, _c[key] = __assign(__assign(__assign({}, state[key]), data), { saveMs: saveMs }), _c));
return __assign(__assign({}, state), (_c = {}, _c[key] = __assign(__assign({}, state[key]), data), _c));
}

@@ -61,0 +61,0 @@ default:

@@ -0,3 +1,3 @@

import { Action } from './actions'
import { QueryState } from './query'
import { Action } from './actions'

@@ -14,3 +14,3 @@ /**

export default function reduce(state: QueryState = {}, action: Action): QueryState {
const saveMs = Date.now()
const responseMs = Date.now()

@@ -23,3 +23,3 @@ switch (action.type) {

...state,
[key]: { ...state[key], response: { ...response }, saveMs },
[key]: { ...state[key], response: { ...response }, responseMs },
}

@@ -40,3 +40,3 @@ }

...state,
[key]: { ...state[key], response: { ...res }, saveMs },
[key]: { ...state[key], response: { ...res }, responseMs },
}

@@ -50,3 +50,3 @@ }

...state,
[key]: { ...state[key], ...data, saveMs },
[key]: { ...state[key], ...data },
}

@@ -53,0 +53,0 @@ }

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