💨

Next.jsで「 Objects are not valid as a React child」エラーでハマった話

2021/06/15に公開

Error: Objects are not valid as a React child (found: object with keys {id, name, slug}). If you meant to render a collection of children, use an array instead.

和訳すると「オブジェクトはReactのChildとして相応しくないよ。keyが{id, name, slug}であるオブジェクトが見つかったんだ。もしchildrenのコレクションとしてレンダーしたいんだったら、Arrayを代わりに使ってね」という感じか。

自分の場合この{id, name, slug}とはproductが持つ参照プロパティbrand: DocumentRefferenceのValueのことなんですが、結論から言うと、フロント側のコードで name ではなく、オブジェクトそのもの(brand 自体)を出力しようとしていたために起こったエラーでした。

つまり僕の場合はコンポーネント ProductCard.jsx の中で{product.brand} じゃなくて {product.brand.name} と書くべきだったということです。

エラー文としては「brandってオブジェクトだからそのまま表示とかできねーよ。表示したいならその中の Value である id, name, slug のどれかじゃねーの?」ってことを伝えたかったみたい。

前置き

作成したサイト

今回はこのようなガジェット比較サイトを作りました。

PCモニターのカテゴリー(/categories/pc-monitor)に行けば所属するDellのモニターなどの Product が一覧で表示されるような比較サイトです。

データモデル

データモデルとしては以下のように Product が Category や Brand への参照を保持しています。

models/Product.ts
export default interface Product {
  id: string
  name: string
  slug: string
  categories: [Category]
  brand: Brand
}

default interface Brand { // ホントはファイルを分けるべき
  id: string
  name: string
  slug: string
}

default interface Category { // ホントはファイルを分けるべき
  id: string
  name: string
  slug: string
}

サーバーサイドの実装

サーバーサイドというか Next.js の api.jsx の実装ですが、今回は FireStore を CMS化できる Flamelink を使っていて、下記はその JavaScript SDK のコードですが、要は FireStore からフィルター条件を指定して get() しているコードです。

Flamelink の書き方で get() してやると _products というオブジェクトを取得できるので、その Value だけを Object.values() を使って return して、クライアントサイドに渡してあげています。

utils/api.jsx
export async function getProductByCategory(id) {
  const categoryRef = await app.content
    .ref(["categories", id]) // [schemaKey, entryId]
    .get()

  const _products = await app.content
    .get({
      schemaKey: "products",
      filters: [["categories", "array-contains", categoryRef.docs[0].ref]],
      orderBy: 'price',
      fields: [
        "id",
        "name",
        "slug",
        'brand',
      ],
      populate: [       
        {
          field: 'brand',
          fields: ['id', 'name', 'slug'],
        }
      ]
    })    
    
  console.log('_products: ', _products)
  return _products ? Object.values(_products) : []
}

console.logした結果ターミナルにはこのように表示され、Objectが返ってきていて、brandのvalueもObjectである。

_products: {
  xxx: {
    id: 'xxx',
    name: 'Dell 4Kモニター 27インチ U2720QM (Amazon限定Type-Cモデル)',
    slug: 'dell-u2720qm',
    brand: { id: 'yyy', name: 'Dell', slug: 'dell' }
  },
  {
  ...
  },
}

クライアントサイドの実装

説明しやすいように簡略化していますが、要は Next.js の基本の SSR のやり方通りgetStaticPropsを使って props(categoryproducts) を取得し、コンポーネント<ProductCard>productを渡して表示してるってだけです。

pages/categories/[slug].tsx
import { GetStaticPaths, GetStaticProps } from 'next'
import { useRouter } from 'next/router'
import Category from '../../models/Category'
import Product from '../../models/Product'
import ProductCard from '../../components/ProductCard'
import { getCategory, getCategoriesForPaths, getProductByCategory } from '../../utils/api'

type Query = {
  slug: string
}

export default function CategoryPage({ category, products }: {
  category: Category,
  products: [Product],
}) {
  const router = useRouter()
  
  if (router.isFallback) {
    return <div>Loading category...</div>;
  }

  return (
	<ul className="flex overflow-scroll">
	{products.map((product) => {
	  return (
	    <ProductCard
	      product={product}
	      key={product.id}
	    ></ProductCard>
	  )
	})}
	</ul>
  )
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const category = await getCategory(params.slug as string);
  const products = await getProductByCategory(category.id);
  return {
    props: {
      category,
      products,
    }
  };
}

export const getStaticPaths: GetStaticPaths = async () => {
  const categorys = await getCategoriesForPaths()
  return {
    paths: categorys.map((_category: Category) => {
      return {
        params: { slug: _category.slug },
      };
    }),
    fallback: false,
  };
}

問題のコード

そんなに説明するほどではないのですが、このコンポーネント内で{product.brand.name}と書くべきところを{product.brand}と書いていたために上記のエラーが出たということでした。

ProductCard.jsx
import Link from "next/link"
import Product from '../models/Product'

export default function ProductCard({ product }: {
  product: Product
}) {
  return (
    <Link href={`/products/${product.id}`}>
      <a className="m-2">
        <div className="px-4 w-48 overflow-hidden bg-white shadow p-3 rounded">   
          <div className="mt-6">
            <p className="text-sm text-gray-600 font-hairline">
              {product.brand.name} // ここをproduct.brandにしていた
            </p>
            <p className="truncate text-xs text-bold tracking-wide text-gray-600 mb-2">
              {product.name}
            </p>
          </div>
        </div>
      </a>
    </Link>
  )
}

てっきり、バックエンドのapi.jsxで取得したObjectの中のbrandをOjbect型からArray型に変換しないと行けないのかと思い、「そのためにBrandに複数選択を許可するのもなぁ‥(Flamelinkの仕様で複数選択にすると参照のArrayで返ってくる」とか「本来単数のObjectからArrayでラップした変数をつくり、deleteで消して、作った変数をぶっこんで‥」なんて悪魔的なコードを考えていたのですが、やはり物事はもっとシンプルでした。

JavaScriptやObjectって操作がすごい複雑で難しく感じるんですが、一般的なキャリアのWebエンジニアだと何てことないんだろうか‥。

Discussion