import { AnyDocumentId } from "@automerge/automerge-repo/slim"
import { ChangeFn, ChangeOptions, Doc } from "@automerge/automerge/slim"
import { useCallback, useEffect, useState } from "react"
import { useDocHandle } from "./useDocHandle.js"
/**
* A hook which returns a document and a function to change it.
* Uses React Suspense for loading states, returning a tuple matching useState pattern.
*
* @example
* ```tsx
* function Counter() {
* const [doc, changeDoc] = useDocument<{ count: number }>(docUrl)
* return (
*
* )
* }
*
* // Must be wrapped in Suspense boundary
* }>
*
*
* ```
*/
interface UseDocumentSuspendingParams {
suspense: true
}
interface UseDocumentSynchronousParams {
suspense: false
}
type UseDocumentParams =
| UseDocumentSuspendingParams
| UseDocumentSynchronousParams
export type UseDocumentReturn = [
Doc,
(changeFn: ChangeFn, options?: ChangeOptions) => void
]
export function useDocument(
id: AnyDocumentId,
params: UseDocumentSuspendingParams
): UseDocumentReturn
export function useDocument(
id: AnyDocumentId | undefined,
params?: UseDocumentSynchronousParams
): UseDocumentReturn | [undefined, () => void]
export function useDocument(
id: AnyDocumentId | undefined,
params: UseDocumentParams = { suspense: false }
): UseDocumentReturn | [undefined, () => void] {
// @ts-expect-error -- typescript doesn't realize we're discriminating these types the same way in both functions
const handle = useDocHandle(id, params)
// Initialize with current doc state
const [doc, setDoc] = useState | undefined>(() => handle?.doc())
const [deleteError, setDeleteError] = useState()
// Reinitialize doc when handle changes
useEffect(() => {
setDoc(handle?.doc())
}, [handle])
useEffect(() => {
if (!handle) {
return
}
const onChange = () => setDoc(handle.doc())
const onDelete = () => {
setDeleteError(new Error(`Document ${id} was deleted`))
}
handle.on("change", onChange)
handle.on("delete", onDelete)
return () => {
handle.removeListener("change", onChange)
handle.removeListener("delete", onDelete)
}
}, [handle, id])
const changeDoc = useCallback(
(changeFn: ChangeFn, options?: ChangeOptions) => {
handle!.change(changeFn, options)
},
[handle]
)
if (deleteError) {
throw deleteError
}
if (!doc) {
return [undefined, () => {}]
}
return [doc, changeDoc]
}