typescript 109 lines · 4 tabs

Debounced Draft Autosave with React Query and a Saving-Status Indicator

Shared by codesnips Sep 2026
4 tabs
import { useEffect, useState } from "react";

export function useDebouncedValue<T>(value: T, delay = 800): T {
  const [debounced, setDebounced] = useState<T>(value);

  useEffect(() => {
    const handle = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(handle);
  }, [value, delay]);

  return debounced;
}
4 files · typescript Explain with highlit

Autosaving a draft is deceptively tricky: firing a network request on every keystroke floods the server and races responses, while saving too rarely risks losing work. This snippet coordinates three pieces — a debounced value hook, a mutation-driven autosave hook, and a small editor component with a live status badge — to get the balance right.

In useDebouncedValue hook, the raw editor content is passed through a setTimeout-based debounce. The effect clears the previous timer on every change, so the debounced value only settles once the user pauses typing for delay milliseconds. This is the classic debounce trade-off: latency in exchange for far fewer writes. Returning a plain value (not a callback) keeps the consuming component declarative — downstream effects simply react to the settled value.

useAutosaveDraft hook wires that debounced value into a React Query useMutation. It tracks a lastSaved ref so it can skip redundant saves when the content has not actually changed since the last successful write — an important guard, because effects can re-run for reasons unrelated to content. The mutation carries a monotonically increasing seq and stores the latest in latestSeq, so a slow earlier response cannot overwrite a newer one in onSuccess — this defeats the out-of-order response race that naive autosave code suffers from. The hook derives a single SaveStatus union (idle, unsaved, saving, saved, error) rather than exposing raw booleans, which keeps rendering logic trivial and unambiguous.

DraftEditor component composes the two hooks. It holds the live content in local state for instant typing feedback, feeds it to useDebouncedValue, and hands the settled value to useAutosaveDraft. The StatusBadge maps each status to human text, and a manual save button lets users force a flush via saveNow, disabled while a request is in flight.

The pattern shines for editors, note apps, and settings pages where losing input is unacceptable. Watch the pitfalls it addresses: debounce plus a dirty check to avoid needless traffic, sequence guarding to survive concurrency, and a derived status enum so the UI never lies about whether work is persisted.


Related snips

javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
swift
import SwiftUI

struct ContentView: View {
    @State private var username = ""
    @State private var isLoggedIn = false
    @StateObject private var viewModel = LoginViewModel()

SwiftUI declarative UI with state management

swift swiftui ios
by Sofia Martinez 2 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab
javascript
import { Application } from "@hotwired/stimulus"
import FormSubmitController from "./controllers/form_submit_controller"

const application = Application.start()
application.debug = false

Disable submit button while Turbo form is submitting

rails hotwire stimulus
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Debounced Draft Autosave with React Query and a Saving-Status Indicator — share card
Link copied