{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "useImageDecode", "title": "useImageDecode", "description": "useImageDecode external hook", "files": [ { "path": "packages/external-hooks/src/useImageDecode/index.ts", "content": "import {\n useRef,\n useState,\n useEffect,\n useCallback,\n type RefObject,\n} from \"react\";\n\nexport type ImageDecodeStatus =\n | \"idle\"\n | \"loading\"\n | \"decoded\"\n | \"error\"\n | \"painted\";\n\nexport interface UseImageDecodeOptions {\n /** Gọi khi ảnh đã decode xong trong bộ nhớ */\n onDecoded?: (el: HTMLImageElement) => void;\n /** Gọi khi ảnh đã render hết pixel lên màn hình */\n onPainted?: (el: HTMLImageElement) => void;\n}\n\nexport interface UseImageDecodeReturn {\n status: ImageDecodeStatus;\n isLoading: boolean;\n isDecoded: boolean;\n isPainted: boolean;\n isError: boolean;\n error: string | null;\n naturalWidth: number;\n naturalHeight: number;\n}\n\n/**\n * useImageDecode\n *\n * Nhận một React ref gắn vào element.\n * Tự động gọi HTMLImageElement.decode() mỗi khi src thay đổi,\n * sau đó dùng double rAF để xác định ảnh đã paint hết lên màn hình.\n *\n * Flow:\n * src thay đổi (MutationObserver)\n * → el.decode() → onDecoded()\n * → rAF #1 → rAF #2 → onPainted() ✅\n *\n * @example\n * const imgRef = useRef(null)\n * const { isPainted } = useImageDecode(imgRef, {\n * onPainted: (el) => console.log('render hết rồi!', el.naturalWidth)\n * })\n *\n * \n */\nfunction useImageDecode(\n imgRef: RefObject,\n options: UseImageDecodeOptions = {}\n): UseImageDecodeReturn {\n // Dùng useRef để giữ callback ổn định, tránh trigger lại effect\n const onDecodedRef = useRef(options.onDecoded);\n const onPaintedRef = useRef(options.onPainted);\n onDecodedRef.current = options.onDecoded;\n onPaintedRef.current = options.onPainted;\n\n const [state, setState] = useState<\n Omit<\n UseImageDecodeReturn,\n \"isLoading\" | \"isDecoded\" | \"isPainted\" | \"isError\"\n > & {\n status: ImageDecodeStatus;\n isLoading: boolean;\n isDecoded: boolean;\n isPainted: boolean;\n isError: boolean;\n error: string | null;\n naturalWidth: number;\n naturalHeight: number;\n }\n >({\n status: \"idle\",\n isLoading: false,\n isDecoded: false,\n isPainted: false,\n isError: false,\n error: null,\n naturalWidth: 0,\n naturalHeight: 0,\n });\n\n const decode = useCallback(async (el: HTMLImageElement) => {\n // Dùng object ref để cancel async flow đang chạy\n const token = { cancelled: false };\n\n setState({\n status: \"loading\",\n isLoading: true,\n isDecoded: false,\n isPainted: false,\n isError: false,\n error: null,\n naturalWidth: 0,\n naturalHeight: 0,\n });\n\n let raf1 = 0;\n let raf2 = 0;\n\n const cancel = () => {\n token.cancelled = true;\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n };\n\n try {\n await el.decode();\n\n if (token.cancelled) return cancel;\n\n setState((prev) => ({\n ...prev,\n status: \"decoded\",\n isLoading: false,\n isDecoded: true,\n naturalWidth: el.naturalWidth,\n naturalHeight: el.naturalHeight,\n }));\n\n onDecodedRef.current?.(el);\n\n // Double rAF: đảm bảo pixel đã flush lên màn hình\n raf1 = requestAnimationFrame(() => {\n raf2 = requestAnimationFrame(() => {\n if (token.cancelled) return;\n\n setState((prev) => ({\n ...prev,\n status: \"painted\",\n isPainted: true,\n }));\n\n onPaintedRef.current?.(el);\n });\n });\n } catch (err) {\n if (token.cancelled) return cancel;\n\n setState((prev) => ({\n ...prev,\n status: \"error\",\n isLoading: false,\n isError: true,\n error: err instanceof Error ? err.message : \"Failed to decode image\",\n }));\n }\n\n return cancel;\n }, []); // không deps vì dùng ref cho callbacks\n\n useEffect(() => {\n const el = imgRef.current;\n if (!el) return;\n\n let cancelDecode: (() => void) | undefined;\n\n // Decode ngay khi element mount\n decode(el).then((cancel) => {\n cancelDecode = cancel;\n });\n\n // Theo dõi thay đổi src\n const observer = new MutationObserver(() => {\n cancelDecode?.();\n decode(el).then((cancel) => {\n cancelDecode = cancel;\n });\n });\n observer.observe(el, { attributes: true, attributeFilter: [\"src\"] });\n\n return () => {\n cancelDecode?.();\n observer.disconnect();\n };\n }, [imgRef, decode]);\n\n return state;\n}\n\nexport default useImageDecode;\n", "type": "registry:file", "target": "src/hooks/external-hooks/useImageDecode/index.ts" } ], "type": "registry:hook" }