hyeogeon
hyeogeon

Reputation: 33

Why a memory leak occurs when I get the data stored in indexedDB inside the useSWR fetcher

I got the data stored in indexedDB inside the fetcher of useSWR. Then, the memory usage increased every time auto refresh occurred.

When I got the data stored in indexedDB only once from outside of fetcher and passed it to the fetcher instead of getting it inside the fetcher, the memory usage did not increase.

Please tell me why a memory leak occurs when I get the data stored in indexedDB inside the fetcher.

I used idb with the indexedDB wrapper package.

package version

react: 18.2.0
next: 14.0.1
node.js: 20.18.0
swr: 2.2.4
idb: 8.0.0
// indexedDB.ts
import { DBSchema, IDBPDatabase, openDB } from 'idb'
import { StoreKey, StoreNames, StoreValue } from 'idb/build/entry'

import { Data } from './types'

const DB_NAME = 'temp-indexed-database'

// Define database schema
interface IndexedDB extends DBSchema {
  myData: {
    key: string
    value: Data
    indexes: {
      transactTime: string
    }
  }
}

const getDB = (() => {
  let dbPromise: Promise<IDBPDatabase<IndexedDB>> | null = null

  return async () => {
    if (typeof window === 'undefined') return null
    if (!dbPromise) {
      dbPromise = openDB<IndexedDB>(DB_NAME)
    }
    return dbPromise
  }
})()


export const getAllData = async <S extends StoreNames<IndexedDB>>(
  storeName: S,
  query?: StoreKey<IndexedDB, S> | IDBKeyRange | null,
  count?: number
) => {
  const db = await getDB()
  if (!db) return []
  return db.getAll(storeName, query, count)
}

export const addData = async <S extends StoreNames<IndexedDB>>(
  storeName: S,
  data: StoreValue<IndexedDB, S> | StoreValue<IndexedDB, S>[],
  key?: StoreKey<IndexedDB, S> | IDBKeyRange
) => {
  const db = await getDB()
  if (!db) return
  const transaction = db.transaction(storeName, 'readwrite')
  const store = transaction.objectStore(storeName)
  if (Array.isArray(data)) {
    await Promise.all(data.map((item) => store.put(item, key)))
  } else {
    await store.put(data, key)
  }
}

// hook.ts
import useSWR from 'swr'

import { Data } from './types'

export const useSWRData = (params) => {
  const [initialData, setInitialData] = useState<Data[]>(
    []
  )

  useEffect(() => {
    getAllData('myData').then(setInitialData)
  }, [])

  const {
    data,
    isValidating,
    error,
  } = useSWR<Data[]>(
    { key: 'fetchData', params },
    fetchAndSyncData
  )

  return useMemo(
    () => ({
      data: data ?? initialData,
      isValidating,
      error,
    }),
    [data, initialData, isValidating, error]
  )
}

// service.ts
export const fetchAndSyncData = async (params: {params: AnyRecord}) => {
  try {
    const indexedData= await getAllData('myData')
    const lastDate = getLastDate(indexedData)
    const newData = await fetchData({ params, startDate: lastDate })
    if (newData.length > 0) {
      await addData('myData', newData)
    }
    return sortObjArrByKey(
      [...(newData ?? []), ...(indexedData ?? [])],
      'timestamp'
    )
  } catch (error) {
    console.error('Error fetching and syncing data:', error)
    return []
  }
}

Upvotes: 2

Views: 132

Answers (1)

Ali Heydari
Ali Heydari

Reputation: 160

In the fetchAndSyncData function, you call getAllData every time the fetcher runs. Since useSWR revalidates data periodically (auto refreshes), each fetcher call triggers a new promise to access IndexedDB, leading to increased memory usage over time.

You can update your code for handle your getAllData function:

export const useSWRData = (params) => {
  const [initialData, setInitialData] = useState<Data[]>([])

  useEffect(() => {
    // Fetch IndexedDB data once, outside the fetcher
    getAllData('myData').then(setInitialData)
  }, [])

  const { data, isValidating, error } = useSWR<Data[]>(
    { key: 'fetchData', params, initialData }, // Pass initial data to fetcher
    fetchAndSyncData
  )

  return useMemo(() => ({
    data: data ?? initialData, // Use initialData if no fresh data
    isValidating,
    error,
  }), [data, initialData, isValidating, error])
}

Now refactor your fetcher:

export const fetchAndSyncData = async ({ params, initialData }: { params: AnyRecord, initialData: Data[] }) => {
  try {
    const lastDate = getLastDate(initialData) // Use pre-fetched IndexedDB data
    const newData = await fetchData({ params, startDate: lastDate })

    if (newData.length > 0) {
      await addData('myData', newData) // Sync new data to IndexedDB
    }

    // Combine new and old data
    return sortObjArrByKey([...newData, ...initialData], 'timestamp')
  } catch (error) {
    console.error('Error fetching and syncing data:', error)
    return initialData // Return cached data if fetch fails
  }
}

Upvotes: 1

Related Questions