/ src / hooks / use-fetch-gif-first-frame.ts
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;