Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
723 changes: 348 additions & 375 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@
},
"dependencies": {
"@auth0/auth0-react": "^1.6.0",
"@floating-ui/react": "^0.27.8",
"@material-ui/core": "^4.11.0",
"@material-ui/data-grid": "^4.0.0-alpha.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.56",
"@tanstack/react-table": "^8.20.5",
"ag-grid-community": "^23.2.1",
"ag-grid-react": "^23.2.1",
"axios": "^0.21.1",
"axios": "^0.22.0",
"axios-retry": "^3.1.8",
"body-parser": "^1.20.0",
"bootstrap": "^3.4.1",
Expand All @@ -72,10 +73,10 @@
"path": "^0.12.7",
"rc-slider": "^9.3.1",
"rc-tooltip": "^4.2.1",
"react": "^16.13.1",
"react": "^17.0.2",
"react-bootstrap": "^0.33.1",
"react-bootstrap-dialog": "^0.13.0",
"react-dom": "^16.13.1",
"react-dom": "^17.0.2",
"react-dropzone": "^11.0.2",
"react-graph-vis": "^1.0.5",
"react-icons": "^3.10.0",
Expand All @@ -89,6 +90,7 @@
"react-virtualized": "^9.21.2",
"react-widgets": "^4.5.0",
"regenerator-runtime": "^0.13.7",
"rich-textarea": "^0.26.4",
"shortid": "^2.2.15",
"slugify": "^1.4.5",
"sqlite3": "^5.1.7"
Expand Down
144 changes: 144 additions & 0 deletions src/hooks/use-query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React, {
createContext, useCallback, useContext, useEffect, useRef, useState,
} from 'react';

const CACHE_TTL_MS = 30 * 60 * 1000;
const PRUNE_INTERVAL_MS = 5 * 60 * 1000;

const QueryCacheContext = createContext(null);

export const QueryCacheProvider = ({ children }) => {
const [cache, setCache] = useState(new Map());

const get = useCallback((key) => {
const entry = cache.get(key);
if (!entry) return undefined;

const now = Date.now();
if (now - entry.timestamp > CACHE_TTL_MS) {
setCache((prev) => {
const next = new Map(prev);
next.delete(key);
return next;
});
return undefined;
}

return entry.data;
}, [cache]);

const set = useCallback((key, value) => {
setCache((prev) => {
const next = new Map(prev);
next.set(key, { data: value, timestamp: Date.now() });
return next;
});
}, []);

useEffect(() => {
const interval = setInterval(() => {
setCache((prev) => {
const now = Date.now();
const next = new Map(prev);
// eslint-disable-next-line no-restricted-syntax
for (const [key, entry] of next.entries()) {
if (now - entry.timestamp > CACHE_TTL_MS) {
next.delete(key);
}
}
return next;
});
}, PRUNE_INTERVAL_MS);

return () => clearInterval(interval);
}, []);

return (
<QueryCacheContext.Provider value={{ get, set }}>
{children}
</QueryCacheContext.Provider>
);
};

export const useQuery = ({
queryFn,
queryKey,
debounceMs = 0,
keepStaleData = false,
}) => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

const cache = useContext(QueryCacheContext);
if (!cache) throw new Error('You must use `useQuery` hook within `QueryCacheProvider`');

const timeoutRef = useRef(null);
const abortControllerRef = useRef(null);

useEffect(() => {
let ignore = false;

if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

if (abortControllerRef.current) {
abortControllerRef.current.abort();
}

timeoutRef.current = setTimeout(() => {
(async () => {
if (!keepStaleData) setData(null);
setIsLoading(true);
setError(null);

const cachedData = cache.get(queryKey);
if (cachedData) {
setData(cachedData);
setIsLoading(false);
return;
}

const controller = new AbortController();
abortControllerRef.current = controller;

try {
const d = await queryFn(controller.signal);

if (ignore) return;

cache.set(queryKey, d);
setData(d);
setIsLoading(false);
} catch (err) {
if (!controller.signal.aborted) {
setError((Boolean(err) && err.message) || 'Unknown error');
setIsLoading(false);
}
}
})();
}, debounceMs);

return () => {
ignore = true;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};

// Don't rerender if queryFn is different, this can cause an endless render loop if
// the component calling useQuery uses a closure like queryFn: () => fetchFn(someLocalVar).

// This could be fixed by wrapping the closure in a useCallback in the calling component
// or including a dependency array in this hook. All should work fine if all the queryFn
// depends on is included in the queryKey.

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryKey, cache, debounceMs]);

return { data, isLoading, error };
};
Loading
Loading