I'm developing a Next.js application and facing an issue related to routing and context management. My goal is to create a page that dynamically loads event data based on the event_id obtained from the URL. However, I'm encountering the "NextRouter was not mounted" error when trying to access useRouter in a dynamically imported component.
Problem Description
Context and Objective:
I have a Next.js page (page.js) where I need to load event data based on an event_id obtained from the URL query parameters. The application structure includes a context (EventIdContext.js) for managing eventId state across components.
Implementation Approach:
I've implemented a RouterComponent (in EventRouterComponent.js) that uses useRouter to fetch the event_id from the URL. This RouterComponent is then dynamically imported in page.js using next/dynamic with { ssr: false } to ensure client-side only execution.
Encountered Error:
Despite the dynamic import with { ssr: false }, I'm receiving an error: "NextRouter was not mounted" in the EventRouterComponent.js when trying to use useRouter. This error occurs specifically at the line where useRouter is called. Attempted Solutions:
Ensured correct import of dynamic from next/dynamic and its usage. Double-checked file paths and component names for accuracy. Simplified the EventRouterComponent to render static content without useRouter to isolate the issue.
Code Snippets:
event-data-fetcher.js and event-h2h-odds-fetcher.js: Custom hooks for fetching event and odds data.
EventRouterComponent.js: Component that uses useRouter to access event_id from URL.
page.js: Page component that dynamically imports EventRouterComponent.
EventIdContext.js: Context for managing eventId across components.
h2h-odds-table.tsx: Component that presumably uses data fetched based on event_id.
Questions for the Community:
What could be causing the "NextRouter was not mounted" error despite using dynamic import with { ssr: false } in a Next.js application?
Are there specific considerations or best practices when using useRouter in dynamically imported components in Next.js?
Could this issue be related to how Next.js handles routing and context in dynamically imported components, and if so, how can it be resolved?
Any insights or suggestions from the community would be greatly appreciated. I am including the code from each file for a more detailed context.
components/events-data/nba-events-data/event-data-fetcher.js
import useSWR from 'swr';
import axios from 'axios';
const API_BASE_URL = 'https://api-url.app/api/nba/event_data/';
const fetcher = async (url) => {
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
console.error('Error fetching data:', error);
throw error;
}
};
export function useEventData(event_id) {
if (!event_id) {
return { eventData: null, isLoading: false, isError: null };
}
const url = `${API_BASE_URL}${event_id}`;
const { data, error } = useSWR(url, fetcher);
return {
eventData: data,
isLoading: !error && !data,
isError: error,
};
}
components/events-data/nba-events-data/event-h2h-odds-fetcher.js
import useSWR from 'swr';
import axios from 'axios';
const API_URL = 'https://api-url.app/api/nba/h2h/odds/';
const fetcher = async (url) => {
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
console.error('Error fetching data:', error);
throw error;
}
};
const useOddsData = (eventId) => {
if (!eventId) {
return { eventData: null, isLoading: false, isError: null };
}
const url = `${API_URL}${eventId}`;
const { data, error } = useSWR(url, fetcher);
return {
eventData: data,
isLoading: !error && !data,
isError: error,
};
};
export default useOddsData;
components/events-data/nba-events-data/EventRouterComponent.js
import React, { useEffect, useContext } from 'react';
import { useRouter } from 'next/router';
import EventIdContext from '../../../app/(default)/basketball/nba/events/[events]/EventIdContext';
const EventRouterComponent = () => {
const router = useRouter();
const { setEventId } = useContext(EventIdContext);
useEffect(() => {
if (router.isReady) {
const event_id = router.query.event_id;
setEventId(event_id); // Updating the eventId in the context
// You can also call useEventData or other logic that depends on event_id here
}
}, [router.isReady, router.query.event_id]);
// Your component can return JSX or null depending on your requirement
return null;
};
export default EventRouterComponent;
app/(default)/basketball/nba/events/[events]/page.js
// pages.js
"use client";
import React, { useState } from 'react';
import dynamic from 'next/dynamic';
import EventIdContext from './EventIdContext';
import BreadcrumbsBar from '../../../../../../components/breadcrumbs-bar';
import BetStats from '../../bet-stats';
import EventTabs from '../../events-components/event-tabs';
import ResponsibleGambling from '../../../../../../components/responsible-wagering';
import WelcomeBanner from '../../events-components/events-header-box';
import Betslip from '../../../../../../components/ui/betslip/betslip';
const metadata = {
title: 'NBA Betting - Odds, Tips, Statistics, Tips',
description: 'Punt App allows you to compare odds for NBA from multiple bookmakers and exchanges. You can also place bets and track your bets.',
};
const NavigationBarItems = [
{ label: 'Home', href: '/' },
{ label: 'Basketball', href: '/basketball' },
{ label: 'NBA', href: '/basketball/nba' },
];
const RouterComponent = dynamic(() => import('../../../../../../components/events-data/nba-events-data/EventRouterComponent'), { ssr: false });
const Page = () => {
const [eventId, setEventId] = useState(null); // State for eventId
// Additional state or logic can be added here as needed
return (
<EventIdContext.Provider value={{ eventId, setEventId }}>
<>
<div>
<header className="bg-white shadow-sm">
<div className="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
<BreadcrumbsBar items={NavigationBarItems} />
</div>
</header>
</div>
<div className="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-[76rem] mx-auto">
<ResponsibleGambling />
<WelcomeBanner />
<div className="grid grid-cols-12 gap-6">
<div className="col-span-12 sm:col-span-8">
{/* You can pass additional props as needed */}
<EventTabs />
</div>
<div className="col-span-12 sm:col-span-4">
<Betslip />
{/* You can pass additional props as needed */}
<BetStats />
</div>
</div>
</div>
<RouterComponent />
</>
</EventIdContext.Provider>
);
};
export default Page;
app/(default)/basketball/nba/events/[events]/EventIdContext.js
import React from 'react';
const EventIdContext = React.createContext({
eventId: null,
setEventId: (eventId) => {},
});
export default EventIdContext;
app/(default)/basketball/nba/events-components/odds/h2h-odds-table.tsx
// h2h-odds-table.tsx
import React, {useContext, useState} from 'react';
import useOddsData from '../../../../../../components/events-data/nba-events-data/event-h2h-odds-fetcher';
import OddsDataToggle from './odds-data-toggle';
import BookmakersInfo from '@/app/(default)/bookmakers/logos/bookmakers-info';
import BetslipContext from '@/components/ui/betslip/betslip-context';
interface H2hOddsTableProps {
event_id: string;
}
const H2hOddsTable: React.FC<H2hOddsTableProps> = ({ event_id: eventId }) => {
const { eventData, isLoading, isError } = useOddsData(eventId);
const [activeTab, setActiveTab] = useState('Odds');
const { addOddsToBetslip } = useContext(BetslipContext);
const handleTabClick = (tabName: string) => {
setActiveTab(tabName);
};
const handleAddToBetslip = (oddsType: string, bookmaker: any) => {
if (!eventData) {
console.error('Event data is not available.');
return;
}
const oddsValue = oddsType === 'home' ? bookmaker.home_win_odds : bookmaker.away_win_odds;
const selection = oddsType === 'home' ? eventData.home_team : eventData.away_team;
addOddsToBetslip({
event_id: eventData.event_id,
bookmaker: bookmaker.bookmaker,
selection: selection,
type: bookmaker.bet_type,
odds: parseFloat(oddsValue),
event_date: eventData.event_date,
event_start: eventData.event_start,
league: eventData.league,
sport: eventData.sport,
event_name: eventData.event_name,
line: false,
bookie_icon: BookmakersInfo.australia[bookmaker.bookmaker]?.logo || 'fallback-logo-url',
stake: 0,
});
};
const getDisplayValue = (bookmaker: any, valueType: string) => {
// ... (unchanged)
};
if (isError) return <div>Error: Failed to fetch data</div>;
if (isLoading) return <div>Loading...</div>;
if (!eventData) return <div>No data available</div>;
return (
<div className="col-span-full xl:col-span-8 sm:col-span-8 bg-white dark:bg-slate-800 shadow-lg rounded-sm border border-slate-200 dark:border-slate-700 overflow-y-auto overflow-x-hidden">
<OddsDataToggle onTabClick={handleTabClick} />
<div className="overflow-x-auto">
<table className="w-full text-sm text-left text-gray-900">
<thead className="text-xs text-white uppercase bg-[#47566d]">
<tr>
<th scope="col" className="px-1 py-3 w-12 pl-2">Best</th>
<th scope="col" className="px-1 py-3 w-4 lg:w-4">SGM</th>
<th scope="col" className="px-1 py-1 text-center">Home</th>
<th scope="col" className="px-1 py-1 text-center">Away</th>
<th scope="col" className="px-4 py-3 w-2 text-center">Margin</th>
</tr>
</thead>
<tbody>
{eventData.best && eventData.best.map((bookmaker, index) => (
<tr className={`border-b dark:border-gray-700 ${index % 2 === 0 ? 'bg-gray-50' : ''}`} key={index}>
<th scope="row" className="px-1 py-3 font-bold text-gray-900 whitespace-nowrap">
<img className="w-7 h-7" src={BookmakersInfo.australia[bookmaker.bookmaker]?.logo || 'fallback-logo-url'} alt={bookmaker.bookmaker} />
</th>
<td className="px-1 py-1">
<div className={`inline-block w-5 h-5 mr-2 rounded-full ${bookmaker.sgm === 'true' ? 'bg-green-700' : 'bg-red-700'}`}></div>
</td>
<td className="px-1 py-1 text-center">
<button onClick={() => handleAddToBetslip('home', bookmaker)}
type="button"
className="w-full py-2.5 px-4 mr-2 mb-0 text-xs font-bold text-zinc-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
{getDisplayValue(bookmaker, 'home')}
</button>
</td>
<td className="px-1 py-1 text-center">
<button onClick={() => handleAddToBetslip('away', bookmaker)}
type="button"
className="w-full py-2.5 px-4 mr-2 mb-0 text-xs font-bold text-zinc-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
{getDisplayValue(bookmaker, 'away')}
</button>
</td>
<td className="px-1 py-3 font-semibold justify-end text-center">
{bookmaker.margin}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead className="text-xs text-white uppercase bg-[#47566d]">
<tr>
<th scope="col" className="px-1 py-3 w-12 pl-2">EXCH</th>
<th scope="col" className="px-1 py-3 w-4 lg:w-4">SGM</th>
<th scope="col" className="px-1 py-1 text-center">Home</th>
<th scope="col" className="px-1 py-1 text-center">Away</th>
<th scope="col" className="px-4 py-3 w-2 text-center">Margin</th>
</tr>
</thead>
<tbody>
{eventData.exchange && eventData.exchange.map((bookmaker, index) => (
<tr className={`border-b dark:border-gray-700 ${index % 2 === 0 ? 'bg-gray-50' : ''}`} key={index}>
<th scope="row" className="px-1 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">
<img className="w-7 h-7" src={BookmakersInfo.australia[bookmaker.bookmaker]?.logo || 'fallback-logo-url'} alt={`${bookmaker.bookmaker} logo`} />
</th>
<td className="px-1 py-1">
<div className={`inline-block w-5 h-5 mr-2 rounded-full ${bookmaker.sgm === 'true' ? 'bg-green-700' : 'bg-red-700'}`}></div>
</td>
<td className="px-1 py-1 text-center">
<button onClick={() => handleAddToBetslip('home', bookmaker)}
type="button"
className="w-full py-2.5 px-4 mr-2 mb-0 text-xs font-bold text-zinc-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
{getDisplayValue(bookmaker, 'home')}
</button>
</td>
<td className="px-1 py-1 text-center">
<button onClick={() => handleAddToBetslip('away', bookmaker)}
type="button"
className="w-full py-2.5 px-4 mr-2 mb-0 text-xs font-bold text-zinc-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
{getDisplayValue(bookmaker, 'away')}
</button>
</td>
<td className="px-1 py-3 font-semibold justify-end text-center">{bookmaker.margin}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead className="text-xs text-white uppercase bg-[#47566d]">
<tr>
<th scope="col" className="px-1 py-3 w-12 pl-2">All</th>
<th scope="col" className="px-1 py-3 w-4 lg:w-4">SGM</th>
<th scope="col" className="px-1 py-1 text-center">Home</th>
<th scope="col" className="px-1 py-1 text-center">Away</th>
<th scope="col" className="px-4 py-3 w-2 text-center">Margin</th>
</tr>
</thead>
<tbody>
{eventData.all && eventData.all.map((bookmaker, index) => (
<tr className={`border-b dark:border-gray-700 ${index % 2 === 0 ? 'bg-gray-50' : ''}`} key={index}>
<th scope="row" className="px-1 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">
<img className="w-7 h-7" src={BookmakersInfo.australia[bookmaker.bookmaker]?.logo || 'fallback-logo-url'} alt={`${bookmaker.bookmaker} logo`} />
</th>
<td className="px-1 py-1">
<div className={`inline-block w-5 h-5 mr-2 rounded-full ${bookmaker.sgm === 'true' ? 'bg-green-700' : 'bg-red-700'}`}></div>
</td>
<td className="px-1 py-1 text-center">
<button onClick={() => handleAddToBetslip('home', bookmaker)}
type="button"
className="w-full py-2.5 px-4 mr-2 mb-0 text-xs font-bold text-zinc-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
{getDisplayValue(bookmaker, 'home')}
</button>
</td>
<td className="px-1 py-1 text-center">
<button onClick={() => handleAddToBetslip('away', bookmaker)}
type="button"
className="w-full py-2.5 px-4 mr-2 mb-0 text-xs font-bold text-zinc-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
{getDisplayValue(bookmaker, 'away')}
</button>
</td>
<td className="px-1 py-3 font-semibold justify-end text-center">{bookmaker.margin}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
export default H2hOddsTable;