💡

Next.js と supabase の 画像 CRUD 処理を考える

2022/03/05に公開

React.js のフレームワークである Next.js で、DB は supabase での処理を実装してみました。

次回: Next.js で supabase の DB と ストレージ画像情報の紐付け(削除時)

supabase とは

Firebase に代わるオープンソース。
こちらに日本語での詳細が載っています。

新規プロジェクト作成については割愛します。
別途ポリシーの設定(権限あるユーザーのみ扱えるようにする、など)が必要です。
今回は、DBと、画像アップロード用にストレージを使用する目的で使いました。
アカウント作成はこちらから↓

イメージコンポーネントと UUID

画像用コンポーネントが必要 & UUID (Universally Unique Identifier)を使うのでインポートしておく

import { v4 as uuidv4 } from 'uuid'
import Image from 'next/image'

ステート管理

ブラウザで見せたい画像一覧とプレビュー画像はステート管理。フォームの設定もしておく

const { register, handleSubmit, reset, formState: { errors } } = useForm()
const [photos, setPhotos] = useState([])
const [previewUrl, setPreviewUrl] = useState(null)

DBから取得した画像一覧

DBから取得した画像一覧をマウント時に一度だけ実行するためuseEffectを使用

useEffect(() => {
  fetchPhotos() // 画像一覧取得のための関数
}, [])

画像情報を DBから取得

URLオブジェクトにならないとブラウザに表示できないので、DBデータを取得した後に、url情報の不要部分('https://xxxxxxxxxxxxxx.supabase.co/storage/v1/object/public/photos/')を無くしてオブジェクト化。表示用のステート photos に持たせておく。

const fetchPhotos = async () => {
  // supabase DB photos のデータをすべて取得
  let { data: photos, error } = await supabase.from('photos').select('*').order('id', true)
  if (error) console.log('error', error)
  // URLオブジェクトにする時用のパスに整形
  for (let i = 0; i < photos.length; i++) { 
    let path = photos[i].url.replace('https://xxxxxxxxxxxxxx.supabase.co/storage/v1/object/public/photos/', '')
    let { data, error } = await supabase.storage.from('photos').download(path)
    // URLオブジェクト化した変数を配列 photos の url にして格納
    let objURL = URL.createObjectURL(data)   
    photos[i].url = objURL
    setPhotos([...photos])
  }
}  

添付ファイルの扱い

サイズが小さい時にはフォームをリセットさせたり、プレビュー用のステートも初期化処理する。
postされたファイル(eventの中身)はevent.target.files[0]なので、変数fileにいれて扱う。
プレビュー用のステートに持たせる。

setPreviewUrl(URL.createObjectURL(file)) // file は変数

画像のユニークIDとイメージキーの生成

const onSubmit = async (data, event) => {}{}の中に、送信の際の処理を書いていく。

const uuid = uuidv4()
const newImageKey = uuid.split('-')[uuid.split('-').length - 1]

ストレージに画像アップロード

const { data: inputData } = await supabase.storage
   .from('photos')
   .upload(`${user.id}/${newImageKey}`, data.image[0], {
      cacheControl: '3600',
      upsert: false
    })

ストレージの格納先 URL を取得し、DB にレコード作成

アップロードした時のconst { data: inputData }について、インプットデータのキーを変数に代入して使う。

const key = inputData?.Key
if (!key) {
  throw new Error("Error")
}

// .from() で ストレージの bucket 指定なので渡すパスから bucket名を除く
const { publicURL } = supabase.storage.from('photos').
getPublicUrl(removeBucketPath(key, 'photos'))

// DBにレコード作成
let { data: photo, error } = await supabase.from
('photos').insert([{
   user_id: user.id,
   title: title,
   is_published: is_published,
   url: publicURL
}])

画像送信後の処理

// 画像一覧ステートにプレビュー画像(URLオブジェクトになったもの)を追加
let previewDate = data
previewDate.url = previewUrl
setPhotos([...photos, previewDate])

// プレビュー画像を消す
setPreviewUrl(null)

// フォームを空にする
reset()

DB から画像情報の削除

const deletePhoto = async (id) => {
  try {
    await supabase.from('photos').delete().eq('id', id)
    // photos配列の該当画像情報も削除
    setPhotos(photos.filter((x) => x.id != id))
  } catch (error) {
    console.log('error', error)
}

view 部分

  return (
    <div>
      <p className="text-xl mb-4">新規投稿</p>
      <div>
        <form onSubmit={handleSubmit(onSubmit)} className='flex flex-col'>
          <label htmlFor="title">画像タイトル</label>
          <input id="title" className='py-1 px-2 border-2 w-80' {...register("title", { required: true })} />
          {errors.title && <span>This field is required</span>}

          <label htmlFor="is_published" className='mt-4'>公開状態</label>
          <input type="checkbox" id="is_published" className='py-1 px-2 border-2 w-4' {...register("is_published")} />

          <label htmlFor="image" className='mt-4'>画像を選択</label>
          <input
            type="file"
            id="image"
            accept="image/*"
            multiple
            {...register("image", { onChange: handleFile, required: true })}
          />
          {/* プレビュー画像があれば表示・なければ何も表示しない */}
          <div className='mt-4'>
            {previewUrl ? <Image className='w-4/12' src={previewUrl} alt="image" width={150} height={100} layout='fixed' objectfit={"cover"} /> : <></>}
          </div>
          
          <input className='border-white-300 border-2 rounded p-1 w-16 mt-4' type="submit" />
        </form>
      </div>

      {/* 投稿画像一覧と削除ボタンを表示 */}
      
      {!!errorText && <Alert text={errorText} />}
      <div className="bg-white shadow overflow-hidden rounded-md">
        <ul>
          {photos.map((photo) => (
            <li>
              <Image key={photo.id} className='w-4/12' src={photo.url} alt="image" width={150} height={100} layout='fixed' objectfit={"cover"} />
              <button onClick={() => deletePhoto(photo.id)} className='border-gray-300 border-2 rounded p-1 w-12'>削除</button>
            </li>
          ))}
        </ul>
      </div>
    </div>
  )

おわりに

今回はDBの画像情報についてはCRUD処理の実装ができましたが、ストレージの内容の操作についての処理は実装していません。オブジェクトとして扱うところで少し時間がかかってしまいましたが、supabase自体はポリシーの設定などしておけば、権限あるユーザーだけ扱える状態にできるので使いやすいなと感じました。

Discussion