Next.jsで「 Objects are not valid as a React child」エラーでハマった話
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 への参照を保持しています。
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 して、クライアントサイドに渡してあげています。
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(category
とproducts
) を取得し、コンポーネント<ProductCard>
にproduct
を渡して表示してるってだけです。
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}
と書いていたために上記のエラーが出たということでした。
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