use-fetch-gif-first-frame.ts
1 import { useEffect, useState } from 'react'; 2 3 const GIF_FRAME_CACHE_KEY = 'gifFrameCache'; 4 5 const getCachedGifFrame = (url: string): string | null => { 6 const cache = JSON.parse(localStorage.getItem(GIF_FRAME_CACHE_KEY) || '{}'); 7 return cache[url] || null; 8 }; 9 10 const setCachedGifFrame = (url: string, frameUrl: string): void => { 11 const cache = JSON.parse(localStorage.getItem(GIF_FRAME_CACHE_KEY) || '{}'); 12 cache[url] = frameUrl; 13 localStorage.setItem(GIF_FRAME_CACHE_KEY, JSON.stringify(cache)); 14 }; 15 16 export const fetchImage = (url: string): Promise<ArrayBuffer> => { 17 return new Promise((resolve, reject) => { 18 const request = new XMLHttpRequest(); 19 request.open('GET', url, true); 20 request.responseType = 'arraybuffer'; 21 request.onloadend = () => { 22 if (request.response !== undefined && (request.status === 200 || request.status === 304)) { 23 resolve(request.response); 24 } else { 25 reject(new Error(`XMLHttpRequest, ${request.statusText}`)); 26 } 27 }; 28 request.send(); 29 }); 30 }; 31 32 export const readImage = (file: File): Promise<ArrayBuffer> => { 33 return new Promise((resolve) => { 34 const reader = new FileReader(); 35 reader.onload = () => { 36 resolve(reader.result as ArrayBuffer); 37 }; 38 reader.readAsArrayBuffer(file); 39 }); 40 }; 41 42 const parseGif = async (buf: ArrayBuffer): Promise<Blob> => { 43 const image = new Image(); 44 await new Promise((resolve) => { 45 image.src = URL.createObjectURL(new Blob([buf])); 46 image.onload = resolve; 47 }); 48 const canvas = document.createElement('canvas'); 49 canvas.width = image.width; 50 canvas.height = image.height; 51 const ctx = canvas.getContext('2d'); 52 if (ctx === null) throw new Error('Canvas Context null'); 53 ctx.drawImage(image, 0, 0, image.width, image.height); 54 return await new Promise((resolve, reject) => 55 canvas.toBlob((blob) => { 56 if (blob === null) { 57 reject('Canvas Blob null'); 58 } else { 59 resolve(blob); 60 } 61 }), 62 ); 63 }; 64 65 const useFetchGifFirstFrame = (url: string | undefined) => { 66 const [frameUrl, setFrameUrl] = useState<string | null>(null); 67 68 useEffect(() => { 69 if (!url) { 70 setFrameUrl(null); 71 return; 72 } 73 74 let isActive = true; 75 76 const fetchFrame = async () => { 77 try { 78 const cachedFrame = getCachedGifFrame(url); 79 if (cachedFrame) { 80 if (isActive) setFrameUrl(cachedFrame); 81 return; 82 } 83 84 const blob = typeof url === 'string' ? await parseGif(await fetchImage(url)) : await parseGif(await readImage(url as File)); 85 const objectUrl = URL.createObjectURL(blob); 86 if (isActive) { 87 setFrameUrl(objectUrl); 88 setCachedGifFrame(url, objectUrl); 89 } 90 } catch (error) { 91 console.error('Failed to load GIF frame:', error); 92 if (isActive) setFrameUrl(null); 93 } 94 }; 95 96 fetchFrame(); 97 98 // Cleanup function to avoid setting state on unmounted component 99 return () => { 100 isActive = false; 101 }; 102 }, [url]); 103 104 return frameUrl; 105 }; 106 107 export default useFetchGifFirstFrame;