目次
Next.jsアプリケーション開発において、「Cannot read properties of undefined」エラーは最も頻繁に遭遇する問題の一つです。この記事では、実際のタグページネーション実装で発生したundefinedエラーを例に、効果的な対策方法を詳しく解説します。
よくあるundefinedエラーのパターン
1. プロパティアクセスエラー
最も一般的なエラーパターン:
// エラー例
TypeError: Cannot read properties of undefined (reading 'endsWith')
TypeError: Cannot read properties of undefined (reading 'length')
2. 発生する主な原因
- 初期化タイミングの問題: propsが渡される前にアクセス
- 非同期データの処理: API呼び出し完了前のアクセス
- 条件分岐の不備: null/undefinedチェックの不足
- SSR/SSGでの初期値問題: サーバーサイドでのデータ不整合
実践例: タグページネーション実装での解決
発生したエラー
// pages/tags/[tag]/[page].tsx
export default function TagPageWithPagination({ tag, posts, currentPage, totalPages }: Props) {
const getImageName = (name: string) => {
if (name.endsWith('js')) { // ← ここでエラー: name is undefined
return name.slice(0, -2) + '.js'
} else {
return name
}
}
return (
<Layout>
{posts.length > 0 && <RecentPosts posts={posts} />} {/* ← ここでもエラー: posts is undefined */}
</Layout>
)
}
解決方法1: Null/Undefinedチェック
export default function TagPageWithPagination({ tag, posts, currentPage, totalPages }: Props) {
const getImageName = (name: string) => {
// undefinedチェックを追加
if (!name) return ''
if (name.endsWith('js')) {
return name.slice(0, -2) + '.js'
} else {
return name
}
}
return (
<Layout>
{/* 配列の存在チェックを追加 */}
{posts && posts.length > 0 && <RecentPosts posts={posts} />}
</Layout>
)
}
TypeScriptを活用した型安全性の向上
1. Propsの型定義強化
// 基本的な型定義
type Props = {
tag: string
posts: Post[]
currentPage: number
totalPages: number
}
// より厳密な型定義
type Props = {
tag: string | undefined
posts: Post[] | undefined
currentPage: number
totalPages: number
}
// デフォルト値付きの型定義
type Props = {
tag?: string
posts?: Post[]
currentPage: number
totalPages: number
}
2. Optional Chainingの活用
// 従来の書き方
if (data && data.posts && data.posts.length > 0) {
// 処理
}
// Optional Chainingを使用
if (data?.posts?.length > 0) {
// 処理
}
// さらに安全な書き方
const postsCount = data?.posts?.length ?? 0
if (postsCount > 0) {
// 処理
}
3. Nullish Coalescingの活用
// 従来の書き方
const title = props.title || 'デフォルトタイトル' // 空文字やfalseでもデフォルト値
// Nullish Coalescingを使用
const title = props.title ?? 'デフォルトタイトル' // null/undefinedのみデフォルト値
// 実践例
const getImageName = (name: string | undefined) => {
const safeName = name ?? ''
return safeName.endsWith('js') ? safeName.slice(0, -2) + '.js' : safeName
}
getStaticPropsでの安全なデータ取得
1. エラーハンドリング付きのデータ取得
export const getStaticProps: GetStaticProps = async ({ params }) => {
try {
const tag = params?.tag as string
const page = parseInt(params?.page as string)
// バリデーション
if (!tag || isNaN(page) || page < 1) {
return {
notFound: true
}
}
const tagPosts = getTagPosts(tag, [
'title',
'date',
'slug',
'tag',
'coverImage',
'excerpt',
])
// データが存在しない場合の処理
if (!tagPosts || tagPosts.length === 0) {
return {
props: {
tag,
posts: [],
currentPage: page,
totalPages: 0
}
}
}
const startIndex = (page - 1) * POSTS_PER_PAGE
const endIndex = startIndex + POSTS_PER_PAGE
const posts = tagPosts.slice(startIndex, endIndex)
const totalPages = Math.ceil(tagPosts.length / POSTS_PER_PAGE)
return {
props: {
tag,
posts,
currentPage: page,
totalPages
}
}
} catch (error) {
console.error('getStaticProps error:', error)
return {
notFound: true
}
}
}
2. getStaticPathsでの安全なパス生成
export const getStaticPaths: GetStaticPaths = async () => {
try {
const tags = getAllTags()
const paths = []
for (const tag of tags) {
// タグが有効かチェック
if (!tag || typeof tag !== 'string') continue
const tagPosts = getTagPosts(tag.toLowerCase(), ['slug'])
// 投稿が存在するタグのみ処理
if (!tagPosts || tagPosts.length === 0) continue
const totalPages = Math.ceil(tagPosts.length / POSTS_PER_PAGE)
// 2ページ目以降を生成(1ページ目は /tags/[tag] で処理)
for (let page = 2; page <= totalPages; page++) {
paths.push({
params: {
tag: tag.toLowerCase(),
page: page.toString()
}
})
}
}
return {
paths,
fallback: false
}
} catch (error) {
console.error('getStaticPaths error:', error)
return {
paths: [],
fallback: false
}
}
}
カスタムHooksでの安全なデータ管理
1. useEffect内での安全な処理
const useSafeEffect = (effect: () => void, deps: any[]) => {
useEffect(() => {
try {
effect()
} catch (error) {
console.error('Effect error:', error)
}
}, deps)
}
// 使用例
const TagPage = ({ tag, posts }: Props) => {
const [safePosts, setSafePosts] = useState<Post[]>([])
useSafeEffect(() => {
if (posts && Array.isArray(posts)) {
setSafePosts(posts)
}
}, [posts])
return (
<div>
{safePosts.map(post => (
<div key={post.slug}>{post.title}</div>
))}
</div>
)
}
2. カスタムバリデーションフック
const useValidatedProps = <T>(props: T, validator: (props: T) => boolean) => {
const [isValid, setIsValid] = useState(false)
const [safeProps, setSafeProps] = useState<T | null>(null)
useEffect(() => {
try {
if (validator(props)) {
setIsValid(true)
setSafeProps(props)
} else {
setIsValid(false)
setSafeProps(null)
}
} catch (error) {
console.error('Validation error:', error)
setIsValid(false)
setSafeProps(null)
}
}, [props, validator])
return { isValid, safeProps }
}
// 使用例
const TagPage = (props: Props) => {
const { isValid, safeProps } = useValidatedProps(props, (p) => {
return typeof p.tag === 'string' &&
Array.isArray(p.posts) &&
typeof p.currentPage === 'number'
})
if (!isValid || !safeProps) {
return <div>データを読み込んでいます...</div>
}
return (
<div>
{/* 安全にpropsを使用 */}
<h1>{safeProps.tag}</h1>
{safeProps.posts.map(post => (
<div key={post.slug}>{post.title}</div>
))}
</div>
)
}
デバッグとエラー追跡
1. 開発時のデバッグ支援
const debugProps = (props: any, componentName: string) => {
if (process.env.NODE_ENV === 'development') {
console.group(`🐛 ${componentName} Props Debug`)
console.log('Props:', props)
// undefinedプロパティの検出
Object.entries(props).forEach(([key, value]) => {
if (value === undefined) {
console.warn(`⚠️ Property '${key}' is undefined`)
}
if (value === null) {
console.info(`ℹ️ Property '${key}' is null`)
}
})
console.groupEnd()
}
}
// 使用例
const TagPage = (props: Props) => {
debugProps(props, 'TagPage')
// 実際のコンポーネント処理
return <div>...</div>
}
2. Error Boundaryの実装
class TagPageErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean; error?: Error }
> {
constructor(props: any) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('TagPage Error:', error, errorInfo)
// エラーレポーティングサービスに送信
if (process.env.NODE_ENV === 'production') {
// Sentry.captureException(error)
}
}
render() {
if (this.state.hasError) {
return (
<div className="text-center py-10">
<h2>ページの読み込みに失敗しました</h2>
<p>しばらく時間をおいて再度アクセスしてください。</p>
<button
onClick={() => window.location.reload()}
className="btn btn-primary mt-4"
>
再読み込み
</button>
</div>
)
}
return this.props.children
}
}
// 使用例
export default function TagPageWrapper(props: Props) {
return (
<TagPageErrorBoundary>
<TagPage {...props} />
</TagPageErrorBoundary>
)
}
プロダクション対応のベストプラクティス
1. 型ガード関数の実装
// 型ガード関数
const isValidPost = (post: any): post is Post => {
return post &&
typeof post.slug === 'string' &&
typeof post.title === 'string' &&
typeof post.date === 'string'
}
const isValidPostArray = (posts: any): posts is Post[] => {
return Array.isArray(posts) && posts.every(isValidPost)
}
// 使用例
const TagPage = ({ tag, posts, currentPage, totalPages }: Props) => {
// ランタイムでの型チェック
if (!tag || typeof tag !== 'string') {
return <ErrorPage message="無効なタグです" />
}
if (!isValidPostArray(posts)) {
return <ErrorPage message="投稿データが無効です" />
}
return (
<div>
{/* 安全にデータを使用 */}
<h1>{tag}</h1>
{posts.map(post => (
<article key={post.slug}>
<h2>{post.title}</h2>
<p>{post.date}</p>
</article>
))}
</div>
)
}
2. フォールバック値の定義
const DEFAULT_VALUES = {
tag: '',
posts: [] as Post[],
currentPage: 1,
totalPages: 1
} as const
const TagPage = (props: Partial<Props>) => {
// デフォルト値との結合
const safeProps = { ...DEFAULT_VALUES, ...props }
return (
<div>
<h1>{safeProps.tag || '不明なタグ'}</h1>
{safeProps.posts.length > 0 ? (
safeProps.posts.map(post => (
<div key={post.slug}>{post.title}</div>
))
) : (
<p>投稿が見つかりませんでした。</p>
)}
</div>
)
}
まとめ
Next.jsでのundefinedエラーを防ぐためには、以下のアプローチが効果的です:
予防策
- 型安全性の向上: TypeScriptの厳密な型定義
- Null/Undefinedチェック: 適切なガード句の実装
- Optional Chaining: 安全なプロパティアクセス
- Error Boundary: 予期しないエラーのキャッチ
デバッグ支援
- 開発時ログ: propsの状態を可視化
- 型ガード関数: ランタイムでの型検証
- フォールバック値: デフォルト値の適切な設定
これらの対策を組み合わせることで、堅牢で保守性の高いNext.jsアプリケーションを構築できるでしょう。