digitalfabrik/integreat-app

View on GitHub
native/src/hooks/useLoadWithCache.ts

Summary

Maintainability
A
1 hr
Test Coverage
F
32%
import { DateTime } from 'luxon'
import { useCallback } from 'react'

import { Endpoint, fromError, ReturnType, useLoadAsync } from 'shared/api'

import { SnackbarType } from '../components/SnackbarContainer'
import dataContainer from '../utils/DefaultDataContainer'
import { determineApiUrl } from '../utils/helpers'

type Load<T extends object> = {
  cityCode: string
  languageCode: string
  createEndpoint: (baseUrl: string) => Endpoint<{ city: string; language: string }, T>
  isAvailable: (cityCode: string, languageCode: string) => Promise<boolean>
  getFromDataContainer: (cityCode: string, languageCode: string) => Promise<T>
  setToDataContainer: (cityCode: string, languageCode: string, data: T) => Promise<void>
  forceUpdate?: boolean
  showSnackbar: (snackbar: SnackbarType) => void
}

/**
 * Hook to load data either from the cache of the data container or from an endpoint if not yet available, refreshing, or too old.
 * Updates the cache after loading from the endpoint.
 * Shows a snackbar instead of returning an error if the data is available in the cache.
 */
const loadWithCache = async <T extends object>({
  cityCode,
  languageCode,
  isAvailable,
  getFromDataContainer,
  setToDataContainer,
  createEndpoint,
  showSnackbar,
  forceUpdate = false,
}: Load<T>): Promise<T | null> => {
  const cachedData = (await isAvailable(cityCode, languageCode))
    ? await getFromDataContainer(cityCode, languageCode)
    : null

  const lastUpdate = await dataContainer.getLastUpdate(cityCode, languageCode)
  const shouldUpdate = forceUpdate || !lastUpdate || lastUpdate < DateTime.utc().startOf('day')

  if (!shouldUpdate && cachedData) {
    return cachedData
  }

  try {
    const payload = await createEndpoint(await determineApiUrl()).request({
      city: cityCode,
      language: languageCode,
    })
    if (payload.data !== null) {
      await setToDataContainer(cityCode, languageCode, payload.data)
    }
    return payload.data ?? cachedData
  } catch (e) {
    if (!cachedData) {
      throw e
    }
    if (forceUpdate) {
      showSnackbar({ text: fromError(e) })
    }
  }
  return cachedData
}

const useLoadWithCache = <T extends object>(params: Load<T>): ReturnType<T> =>
  useLoadAsync<T>(
    useCallback(
      forceUpdate => loadWithCache<T>({ ...params, forceUpdate: params.forceUpdate || forceUpdate }),
      // Normally using params as dependency triggers infinite re-renders
      // eslint-disable-next-line react-hooks/exhaustive-deps
      [JSON.stringify(params)],
    ),
  )

export default useLoadWithCache