Next.jsでundefinedエラーを防ぐ実践的な対策方法

公開日:
目次

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エラーを防ぐためには、以下のアプローチが効果的です:

予防策

  1. 型安全性の向上: TypeScriptの厳密な型定義
  2. Null/Undefinedチェック: 適切なガード句の実装
  3. Optional Chaining: 安全なプロパティアクセス
  4. Error Boundary: 予期しないエラーのキャッチ

デバッグ支援

  1. 開発時ログ: propsの状態を可視化
  2. 型ガード関数: ランタイムでの型検証
  3. フォールバック値: デフォルト値の適切な設定

これらの対策を組み合わせることで、堅牢で保守性の高いNext.jsアプリケーションを構築できるでしょう。