From c5ba5416297dec36ab030f77d85b6aaedcdcc456 Mon Sep 17 00:00:00 2001 From: Aanchal Jain Date: Tue, 2 Sep 2025 10:14:32 -0500 Subject: [PATCH 1/2] feat: integrate TanStack Query for species list caching --- package-lock.json | 62 ++++++++++++++++++++++-- package.json | 1 + src/context/SpeciesContext.js | 88 ++++++++++++----------------------- src/index.js | 10 +++- yarn.lock | 16 ++++++- 5 files changed, 111 insertions(+), 66 deletions(-) diff --git a/package-lock.json b/package-lock.json index f9c918944..9fc433029 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "treetracker-admin-client", - "version": "1.107.2", + "version": "1.108.0-hotfix-v1-107.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "treetracker-admin-client", - "version": "1.107.2", + "version": "1.108.0-hotfix-v1-107.1", "dependencies": { "@date-io/date-fns": "^1.3.13", "@material-ui/core": "^4.9.10", @@ -16,6 +16,7 @@ "@material-ui/styles": "^4.3.0", "@material-ui/system": "^4.3.2", "@rematch/core": "*", + "@tanstack/react-query": "^5.85.5", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^13.2.1", @@ -43,11 +44,11 @@ "npm": "*", "os": "npm:os-browserify", "prop-types": "*", - "react": "^18", + "react": "^18.2.0", "react-autosuggest": "^10.0.2", "react-chartjs-2": "^4.0.1", "react-csv": "^2.0.3", - "react-dom": "^18", + "react-dom": "^18.2.0", "react-fast-compare": "^3.2.0", "react-infinite": "*", "react-redux": "*", @@ -6312,6 +6313,30 @@ "node": ">=0.10.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz", + "integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz", + "integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==", + "dependencies": { + "@tanstack/query-core": "5.85.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.14.0.tgz", @@ -22420,6 +22445,8 @@ }, "node_modules/npm/node_modules/debug/node_modules/ms": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "inBundle": true, "license": "MIT" }, @@ -23249,6 +23276,8 @@ }, "node_modules/npm/node_modules/node-gyp/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "inBundle": true, "license": "ISC", "dependencies": { @@ -23710,6 +23739,8 @@ }, "node_modules/npm/node_modules/rimraf/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "inBundle": true, "license": "ISC", "dependencies": { @@ -23787,6 +23818,8 @@ }, "node_modules/npm/node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "inBundle": true, "license": "ISC", "dependencies": { @@ -39557,6 +39590,19 @@ } } }, + "@tanstack/query-core": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz", + "integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==" + }, + "@tanstack/react-query": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz", + "integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==", + "requires": { + "@tanstack/query-core": "5.85.5" + } + }, "@testing-library/dom": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.14.0.tgz", @@ -52010,6 +52056,8 @@ "dependencies": { "ms": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "bundled": true } } @@ -52607,6 +52655,8 @@ }, "glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "bundled": true, "requires": { "fs.realpath": "^1.0.0", @@ -52929,6 +52979,8 @@ }, "glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "bundled": true, "requires": { "fs.realpath": "^1.0.0", @@ -52974,6 +53026,8 @@ "dependencies": { "lru-cache": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "bundled": true, "requires": { "yallist": "^4.0.0" diff --git a/package.json b/package.json index d4eb44b72..a96852ec3 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@material-ui/styles": "^4.3.0", "@material-ui/system": "^4.3.2", "@rematch/core": "*", + "@tanstack/react-query": "^5.85.5", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^13.2.1", diff --git a/src/context/SpeciesContext.js b/src/context/SpeciesContext.js index 3f9af0600..2b4f18e56 100644 --- a/src/context/SpeciesContext.js +++ b/src/context/SpeciesContext.js @@ -1,6 +1,7 @@ -import React, { useState, useEffect, createContext } from 'react'; +import React, { useState, createContext } from 'react'; import api from '../api/treeTrackerApi'; import * as loglevel from 'loglevel'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; const log = loglevel.getLogger('../context/SpeciesContext'); @@ -9,7 +10,6 @@ export const SpeciesContext = createContext({ speciesList: [], speciesInput: '', setSpeciesInput: () => {}, - loadSpeciesList: () => {}, onChange: () => {}, isNewSpecies: () => {}, createSpecies: () => {}, @@ -19,44 +19,26 @@ export const SpeciesContext = createContext({ combineSpecies: () => {}, }); -export function SpeciesProvider(props) { - const [speciesList, setSpeciesList] = useState([]); - const [speciesInput, setSpeciesInput] = useState(''); // only used by Species dropdown and Verify - const [isLoading, setIsLoading] = useState(false); +export function SpeciesProvider({ children }) { + const [speciesInput, setSpeciesInput] = useState(''); + const queryClient = useQueryClient(); - useEffect(() => { - const abortController = new AbortController(); - loadSpeciesList({ signal: abortController.signal }); - return () => abortController.abort(); - }, []); - - // EVENT HANDLERS - - const loadSpeciesList = async (abortController) => { - setIsLoading(true); - const species = await api.getSpecies(abortController); - setSpeciesList(species); - setIsLoading(false); - }; + // --- Query for species list --- + const { data: speciesList = [], isLoading } = useQuery({ + queryKey: ['species'], + queryFn: () => api.getSpecies(), // api already has getSpecies + staleTime: 1000 * 60 * 5, // cache for 5 mins + refetchOnWindowFocus: false, + }); // only used by Species dropdown - const onChange = async (text) => { + const onChange = (text) => { console.log('on change:"', text, '"'); setSpeciesInput(text); }; - // only used by Verify const isNewSpecies = () => { - //check input is valid and doesn't already exist - if (!speciesInput) { - log.debug('empty species, false'); - return false; - } - log.debug( - 'to find species %s in list:%d', - speciesInput, - speciesList.length - ); + if (!speciesInput) return false; return speciesList.every( (c) => c.name.toLowerCase() !== speciesInput.toLowerCase() ); @@ -64,45 +46,35 @@ export function SpeciesProvider(props) { const createSpecies = async (payload) => { const species = await api.createSpecies( - payload || { - name: speciesInput, - desc: '', - } + payload || { name: speciesInput, desc: '' } ); console.debug('created new species:', species); - setSpeciesList([species, ...speciesList]); + // update cache + queryClient.setQueryData(['species'], (old = []) => [species, ...old]); }; - //to get the species id according the current speciesInput const getSpeciesId = () => { if (speciesInput) { - return speciesList.reduce((a, c) => { - if (a) { - return a; - } else if (c.name === speciesInput) { - return c.id; - } else { - return a; - } - }, undefined); + return speciesList.find((c) => c.name === speciesInput)?.id; } }; - const editSpecies = async (payload) => { - const { id, name, desc } = payload; + const editSpecies = async ({ id, name, desc }) => { const editedSpecies = await api.editSpecies(id, name, desc); console.debug('edit old species:', editedSpecies); + // refetch species list after editing + queryClient.invalidateQueries(['species']); }; - const deleteSpecies = async (payload) => { - const { id } = payload; - const deletedSpecies = await api.deleteSpecies(id); - console.debug('delete outdated species:', id, deletedSpecies); + const deleteSpecies = async ({ id }) => { + await api.deleteSpecies(id); + console.debug('delete outdated species:', id); + queryClient.invalidateQueries(['species']); }; - const combineSpecies = async (payload) => { - const { combine, name, desc } = payload; + const combineSpecies = async ({ combine, name, desc }) => { await api.combineSpecies(combine, name, desc); + queryClient.invalidateQueries(['species']); }; const value = { @@ -110,7 +82,6 @@ export function SpeciesProvider(props) { speciesList, speciesInput, setSpeciesInput, - loadSpeciesList, onChange, isNewSpecies, createSpecies, @@ -119,9 +90,8 @@ export function SpeciesProvider(props) { deleteSpecies, combineSpecies, }; + return ( - - {props.children} - + {children} ); } diff --git a/src/index.js b/src/index.js index 014093385..a27f259c3 100644 --- a/src/index.js +++ b/src/index.js @@ -5,4 +5,12 @@ import App from './App'; import 'typeface-roboto'; import './index.css'; -ReactDOM.createRoot(document.getElementById('root')).render(); +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const queryClient = new QueryClient(); + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +); diff --git a/yarn.lock b/yarn.lock index 807ae11f4..75bac1fca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3232,6 +3232,18 @@ "@svgr/plugin-svgo" "^5.5.0" "loader-utils" "^2.0.0" +"@tanstack/query-core@5.85.5": + "integrity" "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==" + "resolved" "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz" + "version" "5.85.5" + +"@tanstack/react-query@^5.85.5": + "integrity" "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==" + "resolved" "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz" + "version" "5.85.5" + dependencies: + "@tanstack/query-core" "5.85.5" + "@testing-library/dom@^8.0.0": "integrity" "sha512-m8FOdUo77iMTwVRCyzWcqxlEIk+GnopbrRI15a0EaLbpZSCinIVI4kSQzWhkShK83GogvEFJSsHF3Ws0z1vrqA==" "resolved" "https://registry.npmjs.org/@testing-library/dom/-/dom-8.14.0.tgz" @@ -15189,7 +15201,7 @@ "node-dir" "^0.1.10" "strip-indent" "^3.0.0" -"react-dom@^18": +"react-dom@^18.2.0": "integrity" "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==" "resolved" "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" "version" "18.2.0" @@ -15443,7 +15455,7 @@ "loose-envify" "^1.4.0" "prop-types" "^15.6.2" -"react@^18": +"react@^18.2.0": "integrity" "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==" "resolved" "https://registry.npmjs.org/react/-/react-18.2.0.tgz" "version" "18.2.0" From 61b38b370394e16e407982abf7957173223bb54c Mon Sep 17 00:00:00 2001 From: Aanchal Jain Date: Thu, 18 Sep 2025 00:14:20 -0500 Subject: [PATCH 2/2] feat(home): prefetch species on Home mount to improve login performance --- src/components/Home/Home.js | 12 +++++++++++- src/context/SpeciesContext.js | 13 +++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/Home/Home.js b/src/components/Home/Home.js index 1a469f3ef..f44994840 100644 --- a/src/components/Home/Home.js +++ b/src/components/Home/Home.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useContext } from 'react'; - +import { useQueryClient } from '@tanstack/react-query'; +import api from '../../api/treeTrackerApi'; import FilterListIcon from '@material-ui/icons/FilterList'; import Grid from '@material-ui/core/Grid'; import Paper from '@material-ui/core/Paper'; @@ -71,6 +72,15 @@ function Home(props) { } loadUpdateTime(); }, []); + const queryClient = useQueryClient(); + useEffect(() => { + queryClient.prefetchQuery({ + queryKey: ['species'], + queryFn: () => api.getSpecies(), + staleTime: 1000 * 60 * 5, // cache for 5 mins + refetchOnWindowFocus: false, + }); + }, [queryClient]); const timeRange = [ { range: 30, text: 'Last Month' }, diff --git a/src/context/SpeciesContext.js b/src/context/SpeciesContext.js index 2b4f18e56..acf3c650c 100644 --- a/src/context/SpeciesContext.js +++ b/src/context/SpeciesContext.js @@ -23,18 +23,23 @@ export function SpeciesProvider({ children }) { const [speciesInput, setSpeciesInput] = useState(''); const queryClient = useQueryClient(); - // --- Query for species list --- - const { data: speciesList = [], isLoading } = useQuery({ + const { data: speciesList = [], isLoading, refetch } = useQuery({ queryKey: ['species'], - queryFn: () => api.getSpecies(), // api already has getSpecies - staleTime: 1000 * 60 * 5, // cache for 5 mins + queryFn: () => api.getSpecies(), + staleTime: 1000 * 60 * 5, refetchOnWindowFocus: false, + enabled: false, // don’t auto-fetch, rely on Home preload }); // only used by Species dropdown const onChange = (text) => { console.log('on change:"', text, '"'); setSpeciesInput(text); + + // ✅ Fallback: if species list is empty (not prefetched yet), trigger fetch + if (!speciesList.length) { + refetch(); + } }; const isNewSpecies = () => {