首頁
學習紀錄
遊戲心得影視Life書單案件檔案
Side Projects委託作品與二創互動實驗場
Kurau
百百 BLOG
首頁
學習紀錄
遊戲心得影視Life書單案件檔案
Side Projects委託作品與二創互動實驗場
Kurau

Kurau Blog

「隨心而寫,真真假假,都是我」

一個記錄生活、輸出興趣的個人空間。
遊戲、影視、閱讀、學習……每一段體驗都值得留下文字。

頁面導覽

  • 學習紀錄
  • 遊戲心得
  • 影視Life
  • 書單
  • 委託作品與二創
  • Kurau
  • 合作邀請

找到我

歡迎來 Discord 找我聊天!

“曾經發生的事不可能忘記,只是暫時想不起來而已。”-《神隱少女》

© 2026 Kurau All rights reserved

後端與資料庫

一開始就獲取Firebase storage初始值

By Kurau·2023-02-04·Updated 2026-05-09·3 分鐘閱讀

一開始就獲取 Firebase Storage 初始值

TL;DR
Component mount 後 Firebase async 才回資料,首次 render 沒值 是常見問題。標準解:用 loading state 等待。現代做法:Suspense + RSC 直接 await、或 TanStack Query / SWR 自動處理 loading 狀態。

問題情境

function FileList() {
  const [files, setFiles] = useState([]);

  useEffect(() => {
    listAll(ref(storage, 'images')).then(async (res) => {
      const urls = await Promise.all(res.items.map((it) => getDownloadURL(it)));
      setFiles(urls);
    });
  }, []);

  return <div>{files.map((url) => <img src={url} />)}</div>;
  // ⚠️ 第一次 render 時 files === [],畫面空白(或閃爍)
}
tsx

典型 async data fetching pitfall:component mount → render → useEffect 才跑 → fetch → set state → re-render。中間 100-500ms 是空畫面。


解法 1:loading state(經典寫法)

import { getStorage, ref, listAll, getDownloadURL } from 'firebase/storage';
import { useEffect, useState } from 'react';

interface FileItem {
  url: string;
  name: string;
}

function useFirebaseFiles(path: string) {
  const [files, setFiles] = useState<FileItem[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);

    const storage = getStorage();
    const listRef = ref(storage, path);

    listAll(listRef)
      .then(async (res) => {
        const items = await Promise.all(
          res.items.map(async (item) => ({
            name: item.name,
            url: await getDownloadURL(item),
          }))
        );
        if (!cancelled) setFiles(items);
      })
      .catch((err) => {
        if (!cancelled) setError(err);
        console.error('Error listing files:', err);
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });

    return () => { cancelled = true; };  // ⭐ 避免 unmounted setState
  }, [path]);

  return { files, loading, error };
}
typescript
function FileList() {
  const { files, loading, error } = useFirebaseFiles('images');

  if (loading) return <Skeleton />;
  if (error) return <ErrorMessage error={error} />;
  if (files.length === 0) return <p>沒有檔案</p>;

  return (
    <div>
      {files.map((f) => <img key={f.name} src={f.url} alt={f.name} />)}
    </div>
  );
}
tsx

重點:

  • cancelled flag 處理 component unmount 期間 fetch 還沒結束的 race condition
  • error state 給 fallback UI
  • 空陣列 vs loading 區分清楚(loading 是還沒查、files === [] 是查過了沒檔案)

解法 2:TanStack Query(實務首選)

import { useQuery } from '@tanstack/react-query';
import { getStorage, ref, listAll, getDownloadURL } from 'firebase/storage';

function useFirebaseFiles(path: string) {
  return useQuery({
    queryKey: ['firebase-files', path],
    queryFn: async () => {
      const res = await listAll(ref(getStorage(), path));
      return Promise.all(res.items.map(async (it) => ({
        name: it.name,
        url: await getDownloadURL(it),
      })));
    },
    staleTime: 5 * 60_000,    // 5 分鐘內不重新 fetch
  });
}
typescript
function FileList() {
  const { data: files = [], isLoading, error } = useFirebaseFiles('images');

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorMessage error={error as Error} />;

  return <div>{files.map((f) => <img key={f.name} src={f.url} />)}</div>;
}
tsx

自動處理 loading / error / cache / refetch / 多元件共享 query。1/3 程式碼,功能多 5 倍。


解法 3:Server Component(Next.js App Router)

2026 年最乾淨寫法:把資料 fetch 直接寫在 Server Component:

// app/files/page.tsx — 這是 Server Component
import { adminStorage } from '@/lib/firebase-admin';

async function getFiles() {
  const [files] = await adminStorage.bucket().getFiles({ prefix: 'images/' });
  return Promise.all(files.map(async (f) => ({
    name: f.name,
    url: (await f.getSignedUrl({ action: 'read', expires: Date.now() + 3600_000 }))[0],
  })));
}

export default async function FilesPage() {
  const files = await getFiles();   // ⭐ 直接 await,不用 useEffect

  return (
    <div>
      {files.map((f) => <img key={f.name} src={f.url} alt={f.name} />)}
    </div>
  );
}
tsx

零 loading state,server 端就把 HTML 渲染好了。配 Suspense streaming:

<Suspense fallback={<Skeleton />}>
  <FilesPage />
</Suspense>
tsx

重點:Server Component 用 Firebase Admin SDK,不是 client SDK。要 service account 認證。


預載入策略

如果 知道使用者會去某頁面,可以提前 fetch:

1. router prefetch(Next.js)

import Link from 'next/link';

<Link href="/files" prefetch>檔案列表</Link>
// hover 時自動 prefetch 該頁面 + RSC 資料
tsx

2. TanStack Query prefetchQuery

const queryClient = useQueryClient();

<button
  onMouseEnter={() => {
    queryClient.prefetchQuery({
      queryKey: ['firebase-files', 'images'],
      queryFn: () => fetchFiles('images'),
    });
  }}
>
  Show Files
</button>
typescript

使用者 hover 就先 fetch,點下去資料已經在 cache,瞬開。


常見地雷

1. URL 對外暴露
getDownloadURL 拿到的是 包含 access token 的 public URL。外洩等於別人能下載你的檔。

解法:

  • 用 Storage Rules 控制誰能讀
  • 對敏感檔案用 Signed URL with short expiry(Admin SDK)
  • 不要把 URL 存進 public DB
2. listAll 可能很慢
listAll 沒分頁,如果該路徑下有 1000+ 檔案會撈很久。

解法:用 list({ maxResults, pageToken }) 分頁:

const result = await list(listRef, { maxResults: 100 });
// result.items, result.nextPageToken
typescript
3. 不必要的 re-listAll
每次 component mount 都 fetch,即使資料沒變。

解法:TanStack Query 的 staleTime 處理。或自己加 cache。


最簡 wrapper(若不想裝 TanStack Query)

// hooks/useAsync.ts
import { useState, useEffect } from 'react';

export function useAsync<T>(fn: () => Promise<T>, deps: any[]) {
  const [state, setState] = useState<{
    data: T | null;
    loading: boolean;
    error: Error | null;
  }>({ data: null, loading: true, error: null });

  useEffect(() => {
    let cancelled = false;
    setState((s) => ({ ...s, loading: true }));

    fn().then(
      (data) => !cancelled && setState({ data, loading: false, error: null }),
      (error) => !cancelled && setState({ data: null, loading: false, error }),
    );

    return () => { cancelled = true; };
  }, deps);

  return state;
}
typescript
function FileList() {
  const { data: files, loading, error } = useAsync(async () => {
    const res = await listAll(ref(getStorage(), 'images'));
    return Promise.all(res.items.map(async (it) => ({
      name: it.name,
      url: await getDownloadURL(it),
    })));
  }, []);

  if (loading) return <Skeleton />;
  if (error) return <Error error={error} />;
  return <div>{files?.map((f) => <img key={f.name} src={f.url} />)}</div>;
}
tsx

足夠涵蓋 90% 情境,不裝額外套件。

目錄

    ◆ 相關文章

    • Firebase 驗證 儲存到useContext

      2026-05-09
    • Firebase 前端API 是安全的

      2026-05-09
    • CORS firebase

      2026-05-09
    • Firebase 資料庫如何建立 與 上傳

      2026-05-09
    ← 上一篇個人網站怎麼選資料庫 (關聯 非關聯)下一篇 →前端學習資源

    ◆ 關於作者

    Kurau

    個人寫作 / 創作的 SoT,記錄遊戲、影視、學習與生活。

    更多 Kurau 的文章