@tanstack/react-db
Version:
React integration for @tanstack/db
139 lines (131 loc) • 4.26 kB
text/typescript
import { useCallback, useMemo, useRef } from 'react'
import { createPacedMutations } from '@tanstack/db'
import type { PacedMutationsConfig, Transaction } from '@tanstack/db'
/**
* React hook for managing paced mutations with timing strategies.
*
* Provides optimistic mutations with pluggable strategies like debouncing,
* queuing, or throttling. The optimistic updates are applied immediately via
* `onMutate`, and the actual persistence is controlled by the strategy.
*
* @param config - Configuration including onMutate, mutationFn and strategy
* @returns A mutate function that accepts variables and returns Transaction objects
*
* @example
* ```tsx
* // Debounced auto-save
* function AutoSaveForm({ formId }: { formId: string }) {
* const mutate = usePacedMutations<string>({
* onMutate: (value) => {
* // Apply optimistic update immediately
* formCollection.update(formId, draft => {
* draft.content = value
* })
* },
* mutationFn: async ({ transaction }) => {
* await api.save(transaction.mutations)
* },
* strategy: debounceStrategy({ wait: 500 })
* })
*
* const handleChange = async (value: string) => {
* const tx = mutate(value)
*
* // Optional: await persistence or handle errors
* try {
* await tx.isPersisted.promise
* console.log('Saved!')
* } catch (error) {
* console.error('Save failed:', error)
* }
* }
*
* return <textarea onChange={e => handleChange(e.target.value)} />
* }
* ```
*
* @example
* ```tsx
* // Throttled slider updates
* function VolumeSlider() {
* const mutate = usePacedMutations<number>({
* onMutate: (volume) => {
* settingsCollection.update('volume', draft => {
* draft.value = volume
* })
* },
* mutationFn: async ({ transaction }) => {
* await api.updateVolume(transaction.mutations)
* },
* strategy: throttleStrategy({ wait: 200 })
* })
*
* return <input type="range" onChange={e => mutate(+e.target.value)} />
* }
* ```
*
* @example
* ```tsx
* // Debounce with leading/trailing for color picker (persist first + final only)
* function ColorPicker() {
* const mutate = usePacedMutations<string>({
* onMutate: (color) => {
* themeCollection.update('primary', draft => {
* draft.color = color
* })
* },
* mutationFn: async ({ transaction }) => {
* await api.updateTheme(transaction.mutations)
* },
* strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })
* })
*
* return (
* <input
* type="color"
* onChange={e => mutate(e.target.value)}
* />
* )
* }
* ```
*/
export function usePacedMutations<
TVariables = unknown,
T extends object = Record<string, unknown>,
>(
config: PacedMutationsConfig<TVariables, T>,
): (variables: TVariables) => Transaction<T> {
// Keep refs to the latest callbacks so we can call them without recreating the instance
const onMutateRef = useRef(config.onMutate)
onMutateRef.current = config.onMutate
const mutationFnRef = useRef(config.mutationFn)
mutationFnRef.current = config.mutationFn
// Create stable wrappers that always call the latest version
const stableOnMutate = useCallback<typeof config.onMutate>((variables) => {
return onMutateRef.current(variables)
}, [])
const stableMutationFn = useCallback<typeof config.mutationFn>((params) => {
return mutationFnRef.current(params)
}, [])
// Create paced mutations instance with proper dependency tracking
// Serialize strategy for stable comparison since strategy objects are recreated on each render
const mutate = useMemo(() => {
return createPacedMutations<TVariables, T>({
...config,
onMutate: stableOnMutate,
mutationFn: stableMutationFn,
})
}, [
stableOnMutate,
stableMutationFn,
config.metadata,
// Serialize strategy to avoid recreating when object reference changes but values are same
JSON.stringify({
type: config.strategy._type,
options: config.strategy.options,
}),
])
// Return stable mutate callback
const stableMutate = useCallback(mutate, [mutate])
return stableMutate
}