react-redux-query
Advanced tools
Comparing version 0.4.0 to 0.6.0
@@ -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", |
178
query.js
@@ -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; |
191
query.ts
@@ -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, | ||
) | ||
} |
177
README.md
@@ -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 @@ } |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
165097
9
15
855
204