SCALE — Build Lab
開発パターン · TYPESCRIPT PATTERN

Supabaseデータフェッチパターン

CATEGORY開発パターン TYPETypeScript Pattern EFFORT120〜360分 DIFFICULTY
PRIMARY CODE
ts
// === lib/supabase.ts ===
import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

// 型安全 fetch ラッパー(エラーハンドリング込み)
export async function dbFetch<T>(
  query: () => Promise<{ data: T | null; error: any }>
): Promise<T> {
  const { data, error } = await query();
  if (error) throw new Error(error.message);
  if (!data) throw new Error('No data');
  return data;
}

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

export function useSupabaseQuery<T>(
  key: string,
  fetcher: () => Promise<T>,
  realtime?: { table: string; filter?: string },
) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const refetch = async () => {
    try { setLoading(true); setData(await fetcher()); setError(null); }
    catch (e) { setError(e as Error); }
    finally { setLoading(false); }
  };

  useEffect(() => {
    refetch();
    if (!realtime) return;
    const ch = supabase.channel(`${key}-rt`)
      .on('postgres_changes', { event: '*', schema: 'public', table: realtime.table }, () => refetch())
      .subscribe();
    return () => { supabase.removeChannel(ch); };
  }, [key]);

  return { data, loading, error, refetch };
}

// 使い方:
// const { data: tasks } = useSupabaseQuery('tasks',
//   () => dbFetch(() => supabase.from('tasks').select('*').order('created_at')),
//   { table: 'tasks' }  // リアルタイム購読
// );
前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • 全データドリブンアプリ

Supabaseデータフェッチパターン

:LiTarget: 用途

Supabase からデータを取得・キャッシュ・更新する標準パターン。

:LiSparkle: 特徴

  • SSRサポート
  • リアルタイム購読
  • 楽観的更新
  • エラーハンドリング

:LiCode: コード(コピペ用)

// === lib/supabase.ts ===
import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

// 型安全 fetch ラッパー(エラーハンドリング込み)
export async function dbFetch<T>(
  query: () => Promise<{ data: T | null; error: any }>
): Promise<T> {
  const { data, error } = await query();
  if (error) throw new Error(error.message);
  if (!data) throw new Error('No data');
  return data;
}

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

export function useSupabaseQuery<T>(
  key: string,
  fetcher: () => Promise<T>,
  realtime?: { table: string; filter?: string },
) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const refetch = async () => {
    try { setLoading(true); setData(await fetcher()); setError(null); }
    catch (e) { setError(e as Error); }
    finally { setLoading(false); }
  };

  useEffect(() => {
    refetch();
    if (!realtime) return;
    const ch = supabase.channel(`${key}-rt`)
      .on('postgres_changes', { event: '*', schema: 'public', table: realtime.table }, () => refetch())
      .subscribe();
    return () => { supabase.removeChannel(ch); };
  }, [key]);

  return { data, loading, error, refetch };
}

// 使い方:
// const { data: tasks } = useSupabaseQuery('tasks',
//   () => dbFetch(() => supabase.from('tasks').select('*').order('created_at')),
//   { table: 'tasks' }  // リアルタイム購読
// );

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加