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
216 changes: 150 additions & 66 deletions packages/use-dataloader/src/DataLoaderProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,87 +7,158 @@ import React, {
useContext,
useMemo,
useRef,
useState,
} from 'react'
import { KEY_IS_NOT_STRING_ERROR, StatusEnum } from './constants'
import DataLoader from './dataloader'
import { OnErrorFn, PromiseType } from './types'

type RequestQueue = Record<string, DataLoader>
type CachedData = Record<string, unknown>
type Reloads = Record<string, () => Promise<void | unknown>>

type UseDataLoaderInitializerArgs<T = unknown> = {
enabled?: boolean
key: string
status?: StatusEnum
method: () => PromiseType<T>
pollingInterval?: number
keepPreviousData?: boolean
}

interface Context {
addCachedData: (key: string, newData: unknown) => void;
addReload: (key: string, method: () => Promise<void>) => void;
cacheKeyPrefix: string;
addCachedData: (key: string, newData: unknown) => void
addReload: (key: string, method: () => Promise<void | unknown>) => void
addRequest: (key: string, args: UseDataLoaderInitializerArgs) => DataLoader
cacheKeyPrefix: string
onError?: (error: Error) => void | Promise<void>
clearAllCachedData: () => void;
clearAllReloads: () => void;
clearCachedData: (key?: string | undefined) => void;
clearReload: (key?: string | undefined) => void;
getCachedData: (key?: string | undefined) => unknown;
getReloads: (key?: string | undefined) => (() => Promise<void>) | Reloads;
reload: (key?: string | undefined) => Promise<void>;
reloadAll: () => Promise<void>;
clearAllCachedData: () => void
clearAllReloads: () => void
clearCachedData: (key?: string) => void
clearReload: (key?: string) => void
getCachedData: (key?: string) => unknown | CachedData
getReloads: (key?: string) => (() => Promise<void | unknown>) | Reloads
getRequest: (key: string) => DataLoader | undefined
reload: (key?: string) => Promise<void>
reloadAll: () => Promise<void>
}

type CachedData = Record<string, unknown>
type Reloads = Record<string, () => Promise<void>>

// @ts-expect-error we force the context to undefined, should be corrected with default values
export const DataLoaderContext = createContext<Context>(undefined)

const DataLoaderProvider = ({ children, cacheKeyPrefix, onError }: {
children: ReactNode, cacheKeyPrefix: string, onError: (error: Error) => void | Promise<void>
const DataLoaderProvider = ({
children,
cacheKeyPrefix,
onError,
}: {
children: ReactNode
cacheKeyPrefix: string
onError: OnErrorFn
}): ReactElement => {
const cachedData = useRef<CachedData>({})
const [requestQueue, setRequestQueue] = useState({} as RequestQueue)
const [cachedData, setCachedDataPrivate] = useState<CachedData>({})
const reloads = useRef<Reloads>({})

const setCachedData = useCallback((compute: CachedData | ((data: CachedData) => CachedData)) => {
if (typeof compute === 'function') {
cachedData.current = compute(cachedData.current)
} else {
cachedData.current = compute
}
}, [])
const computeKey = useCallback(
(key: string) => `${cacheKeyPrefix ? `${cacheKeyPrefix}-` : ''}${key}`,
[cacheKeyPrefix],
)

const setReloads = useCallback((compute: Reloads | ((data: Reloads) => Reloads)) => {
if (typeof compute === 'function') {
reloads.current = compute(reloads.current)
} else {
reloads.current = compute
}
}, [])
const setCachedData = useCallback(
(compute: CachedData | ((data: CachedData) => CachedData)) => {
if (typeof compute === 'function') {
setCachedDataPrivate(current => compute(current))
} else {
setCachedDataPrivate(compute)
}
},
[],
)

const setReloads = useCallback(
(compute: Reloads | ((data: Reloads) => Reloads)) => {
if (typeof compute === 'function') {
reloads.current = compute(reloads.current)
} else {
reloads.current = compute
}
},
[],
)

const addCachedData = useCallback(
(key: string, newData: unknown) => {
if (key && typeof key === 'string' && newData) {
setCachedData(actualCachedData => ({
...actualCachedData,
[key]: newData,
}))
if (newData) {
if (key && typeof key === 'string') {
setCachedData(actualCachedData => ({
...actualCachedData,
[computeKey(key)]: newData,
}))
} else throw new Error(KEY_IS_NOT_STRING_ERROR)
}
},
[setCachedData],
[setCachedData, computeKey],
)

const addReload = useCallback(
(key: string, method: () => Promise<void>) => {
if (key && typeof key === 'string' && method) {
setReloads(actualReloads => ({
...actualReloads,
[key]: method,
(key: string, method: () => Promise<void | unknown>) => {
if (method) {
if (key && typeof key === 'string') {
setReloads(actualReloads => ({
...actualReloads,
[computeKey(key)]: method,
}))
} else throw new Error(KEY_IS_NOT_STRING_ERROR)
}
},
[setReloads, computeKey],
)

const addRequest = useCallback(
(key: string, args: UseDataLoaderInitializerArgs) => {
if (key && typeof key === 'string') {
const notifyChanges = (updatedRequest: DataLoader) => {
setRequestQueue(current => ({
...current,
[computeKey(updatedRequest.key)]: updatedRequest,
}))
}
const newRequest = new DataLoader({ ...args, notify: notifyChanges })
newRequest.addOnSuccessListener(result => {
if (result !== undefined && result !== null)
addCachedData(key, result)
})
setRequestQueue(current => ({
...current,
[computeKey(key)]: newRequest,
}))

addReload(key, newRequest.launch)

return newRequest
}
throw new Error(KEY_IS_NOT_STRING_ERROR)
},
[setReloads],
[computeKey, addCachedData, addReload],
)

const getRequest = useCallback(
(key: string) => requestQueue[computeKey(key)],
[computeKey, requestQueue],
)

const clearReload = useCallback(
(key?: string) => {
if (key && typeof key === 'string') {
setReloads(actualReloads => {
const tmp = actualReloads
delete tmp[key]
delete tmp[computeKey(key)]

return tmp
})
}
} else throw new Error(KEY_IS_NOT_STRING_ERROR)
},
[setReloads],
[setReloads, computeKey],
)

const clearAllReloads = useCallback(() => {
Expand All @@ -99,70 +170,83 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix, onError }: {
if (key && typeof key === 'string') {
setCachedData(actualCachedData => {
const tmp = actualCachedData
delete tmp[key]
delete tmp[computeKey(key)]

return tmp
})
}
} else throw new Error(KEY_IS_NOT_STRING_ERROR)
},
[setCachedData],
[setCachedData, computeKey],
)
const clearAllCachedData = useCallback(() => {
setCachedData({})
}, [setCachedData])

const reload = useCallback(async (key?: string) => {
if (key && typeof key === 'string') {
await (reloads.current[key] && reloads.current[key]())
}
}, [])
const reload = useCallback(
async (key?: string) => {
if (key && typeof key === 'string') {
await reloads.current[computeKey(key)]?.()
} else throw new Error(KEY_IS_NOT_STRING_ERROR)
},
[computeKey],
)

const reloadAll = useCallback(async () => {
await Promise.all(
Object.values(reloads.current).map(reloadFn => reloadFn()),
)
}, [])

const getCachedData = useCallback((key?: string) => {
if (key) {
return cachedData.current[key] || undefined
}
const getCachedData = useCallback(
(key?: string) => {
if (key) {
return cachedData[computeKey(key)]
}

return cachedData.current
}, [])
return cachedData
},
[computeKey, cachedData],
)

const getReloads = useCallback((key?: string) => {
if (key) {
return reloads.current[key] || undefined
}
const getReloads = useCallback(
(key?: string) => {
if (key) {
return reloads.current[computeKey(key)]
}

return reloads.current
}, [])
return reloads.current
},
[computeKey],
)

const value = useMemo(
() => ({
addCachedData,
addReload,
addRequest,
cacheKeyPrefix,
clearAllCachedData,
clearAllReloads,
clearCachedData,
clearReload,
getCachedData,
getReloads,
getRequest,
onError,
reload,
reloadAll,
}),
[
addCachedData,
addReload,
addRequest,
cacheKeyPrefix,
clearAllCachedData,
clearAllReloads,
clearCachedData,
clearReload,
getCachedData,
getRequest,
getReloads,
onError,
reload,
Expand Down
Loading