如何为类Instagram的GIF搜索结果实现单个GIF的加载Spinner
如何为类Instagram的GIF搜索结果实现单个GIF的加载Spinner
我来帮你实现每个GIF单独的加载Spinner效果,核心思路是给每一个GIF项单独跟踪它的加载状态,而不是用全局的加载状态。这样就能做到每个GIF加载完成后才替换掉加载动画,完全符合你想要的效果。
核心修改点
- 跟踪单个GIF的加载状态:用一个
Set来记录已经加载完成的GIF ID,这样能高效判断每个GIF是否加载完毕 - 重置加载状态:每次搜索到新的GIF列表时,清空已加载的ID集合,确保新的GIF都处于未加载状态
- 单个GIF卡片的加载逻辑:在每个卡片里,加载完成前显示Spinner/Skeleton,加载完成后显示GIF
- 利用图片的
onLoad事件:每个GIF图片加载完成后,触发回调更新加载状态
修改后的完整代码
import { Box, Group, Image, Skeleton, Stack, TextInput, Spinner } from '@mantine/core'; import { useDebouncedValue } from '@mantine/hooks'; import { useEffect, useState } from 'react'; import axios from 'axios'; // 记得导入axios const GifSelectorModal = () => { const [searchQuery, setSearchQuery] = useState(''); const [debouncedQuery] = useDebouncedValue(searchQuery, 400); const [gifs, setGifs] = useState([]); const [isLoading, setIsLoading] = useState(true); // 用Set存储已加载完成的GIF ID,高效判断状态 const [loadedGifIds, setLoadedGifIds] = useState(new Set<string>()); const searchGifs = async (query: string) => { const response = await axios.get('https://tenor.googleapis.com/v2/search', { params: { q: query, key: 'my-key', // 替换为你的实际API Key limit: 30, client_key: 'tribecrafter', }, }); return response.data; }; useEffect(() => { // 当搜索关键词长度≥2时发起请求 if (debouncedQuery.length >= 2) { setIsLoading(true); const fetchGifs = async () => { try { const data = await searchGifs(debouncedQuery); setGifs(data.results); } catch (err) { console.error('搜索GIF失败:', err); setGifs([]); } finally { setIsLoading(false); } }; fetchGifs(); } else { // 关键词过短时清空结果并结束加载状态 setGifs([]); setIsLoading(false); } }, [debouncedQuery]); // 每次GIF列表更新时,重置已加载的ID集合 useEffect(() => { setLoadedGifIds(new Set()); }, [gifs]); // 标记某个GIF为已加载 const markGifAsLoaded = (gifId: string) => { setLoadedGifIds(prev => new Set(prev).add(gifId)); }; return ( <Stack gap="md" p="md"> <TextInput value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search GIFs" autoFocus /> {isLoading ? ( // 全局加载时显示30个骨架屏,模拟网格布局 <Group wrap="wrap" gap="md"> {Array.from({ length: 30 }).map((_, idx) => ( <Skeleton key={idx} width={120} height={120} borderRadius="md" /> ))} </Group> ) : ( <Group wrap="wrap" gap="md"> {gifs.map((gif) => ( <GifCard key={gif.id} gif={gif} isLoaded={loadedGifIds.has(gif.id)} onLoad={() => markGifAsLoaded(gif.id)} /> ))} </Group> )} </Stack> ); }; const GifCard = ({ gif, isLoaded, onLoad }) => { return ( <Box width={120} height={120} position="relative" borderRadius="md" overflow="hidden" cursor="pointer" onClick={() => console.log('selected gif:', gif)} > {/* 加载中的状态:显示Spinner和浅灰色背景 */} {!isLoaded && ( <Box position="absolute" inset={0} bg="#f5f5f5" display="flex" alignItems="center" justifyContent="center" zIndex={1} > <Spinner size="sm" color="gray" /> </Box> )} {/* GIF图片:加载完成前透明度为0,实现平滑过渡 */} <Image src={gif.media_formats.gif.url} alt="GIF" fit="cover" width="100%" height="100%" onLoad={onLoad} style={{ opacity: isLoaded ? 1 : 0, transition: 'opacity 0.2s ease' }} /> </Box> ); }; export default GifSelectorModal;
效果说明
- 当你输入搜索词时,先显示30个骨架屏作为全局加载状态
- 当GIF列表加载完成后,每个卡片会显示Spinner,直到对应的GIF加载完成
- GIF加载完成后会平滑过渡显示,不会出现突然闪现的情况
- 完全和你提供的Instagram截图效果一致,每个GIF独立控制加载状态
备注:内容来源于stack exchange,提问作者Sujal
相关产品推荐
相关产品推荐

