The Tasks App currently implements a non-standard, inconsistent approach to Next.js routing that causes performance problems, breaks the SPA experience, and results in unnecessary full-page reloads. This document outlines the current issues, provides examples of correct implementation, and presents a detailed plan to fix these issues systematically.
Problem: The app mixes different navigation methods:
- Direct browser navigation with
window.location.href - Next.js
Linkcomponent - Next.js
router.push()
Impact: This inconsistency creates a fragmented user experience, causes full page reloads that destroy React state, and adds significant performance overhead.
Example in /src/pages/tasks.tsx:
// Direct browser navigation (problematic)
const openTaskDetail = (taskId: string, task?: Task) => {
// Store task data in localStorage to avoid unnecessary API calls
if (task) {
localStorage.setItem(`task_cache_${taskId}`, JSON.stringify(task));
}
// Use the standard query parameter format
window.location.href = `/task-detail?id=${taskId}`;
};Problem: The app uses localStorage as a workaround for state loss during navigation.
Impact: This creates a fragile state synchronization mechanism that:
- Is prone to errors and race conditions
- Creates unnecessary caching/cleanup logic
- Requires complex serialization/deserialization
- Adds code complexity and maintenance burden
Example:
// In task/[id].tsx
useEffect(() => {
// First check if we have cached task data
const cachedTaskJson = localStorage.getItem(`task_cache_${taskId}`);
if (cachedTaskJson) {
try {
// Try to parse the cached task data
const cachedTask = JSON.parse(cachedTaskJson);
// Check if the cached task has sufficient data
if (cachedTask && cachedTask.id) {
console.log('Using cached task data:', cachedTask);
// Set the task from cache
setTask(normalizedTask);
// Clean up the cache after using it
localStorage.removeItem(`task_cache_${taskId}`);
}
} catch (cacheError) {
console.error('Error parsing cached task data:', cacheError);
}
}
}, [id]);Problem: The app has inconsistent URL patterns:
- Task detail page uses query parameters (
/task-detail?id=123) in some places - Task detail page uses dynamic routes (
/task/[id]) in others
Impact:
- Makes URLs less SEO-friendly and harder to bookmark
- Creates confusion in codebase and for developers
- Requires additional logic to handle both formats
Problem: The app doesn't utilize Next.js data fetching methods:
- No use of
getServerSidePropsorgetStaticProps - Unnecessary client-side data fetching for pre-renderable content
Impact:
- Slower initial page loads
- Poor SEO as content isn't available during server rendering
- Missed opportunities for optimization
Principles:
- Always use Next.js navigation primitives
- Never use direct DOM APIs like
window.location - Preserve application state during navigation
Correct Implementation:
// For declarative navigation (in JSX):
<Link href={`/task/${taskId}`}>
View Task
</Link>
// For programmatic navigation (in event handlers, etc.):
const router = useRouter();
const viewTask = (taskId) => {
router.push(`/task/${taskId}`);
};Principles:
- Use server-side data fetching when possible
- Implement proper client-side data fetching with SWR/React Query
- Cache data appropriately at the app level
Correct Implementation:
// In /pages/task/[id].tsx
export async function getServerSideProps(context) {
const { id } = context.params;
try {
const task = await getTask(id);
return { props: { initialTask: task || null } };
} catch (error) {
console.error('Error fetching task:', error);
return { props: { error: 'Failed to load task', initialTask: null } };
}
}
function TaskPage({ initialTask, error }) {
// Use SWR for real-time updates after initial load
const { data: task, error: fetchError } = useSWR(
`/api/tasks/${id}`,
fetcher,
{ initialData: initialTask }
);
// Component code...
}Principles:
- Use dynamic segments for resource identifiers
- Follow REST-like patterns for URLs
- Maintain consistent URL structure across the app
Correct Structure:
/tasks // Tasks list
/task/:id // Task detail by ID
/api/tasks // API endpoint for tasks
/api/tasks/:id // API endpoint for individual task
Principles:
- Store shared state in context providers
- Use React Query/SWR for server state management
- Avoid storing complex state in localStorage
- Leverage Next.js data fetching patterns
Correct Implementation:
// In _app.tsx
function MyApp({ Component, pageProps }) {
return (
<TasksProvider>
<Component {...pageProps} />
</TasksProvider>
);
}
// In task detail component
function TaskDetail() {
const { id } = useRouter().query;
const { task } = useTasks(id);
// Component code using task data...
}-
Standardize on Dynamic Routes:
- Consolidate all task detail URLs to use
/task/[id]pattern - Add proper redirects for any legacy URLs
- Consolidate all task detail URLs to use
-
Fix Navigation Methods:
- Replace all
window.location.hrefcalls with Next.jsrouter.push() - Update all direct links to use
<Link>component - Create shared utility functions for common navigation patterns
- Replace all
-
Add Server-Side Rendering:
- Implement
getServerSidePropsfor all task detail pages - Ensure proper error handling and loading states
- Implement
-
Remove LocalStorage Caching:
- Eliminate task caching in localStorage
- Implement proper state management using context or SWR/React Query
-
Optimize Data Flow:
- Implement proper data fetching hooks
- Use SWR or React Query for client-side data management
-
Clean Up Legacy Code:
- Remove redundant state management
- Clean up URL parameter handling
- Ensure consistent patterns throughout the codebase
-
Implement Advanced Patterns:
- Add prefetching for common navigation paths
- Implement optimistic UI updates for actions
- Add proper loading indicators
// REPLACE THIS:
const openTaskDetail = (taskId: string, task?: Task) => {
if (task) {
localStorage.setItem(`task_cache_${taskId}`, JSON.stringify(task));
}
window.location.href = `/task-detail?id=${taskId}`;
};
// WITH THIS:
const openTaskDetail = (taskId: string) => {
router.push(`/task/${taskId}`);
};// ADD SERVER-SIDE RENDERING:
export async function getServerSideProps(context) {
const { id } = context.params;
try {
const task = await getTask(id);
return {
props: {
initialTask: task || null,
error: task ? null : 'Task not found'
}
};
} catch (error) {
console.error('Error fetching task:', error);
return {
props: {
error: 'Failed to load task',
initialTask: null
}
};
}
}
// REMOVE ALL LOCALSTORAGE RELATED CODE:
// Delete the entire useEffect block that checks for cached task dataimport { useEffect } from 'react';
import { useRouter } from 'next/router';
export default function TaskDetailRedirect() {
const router = useRouter();
const { id } = router.query;
useEffect(() => {
if (id) {
// Redirect to the new URL pattern
router.replace(`/task/${id}`);
}
}, [id, router]);
return (
<div className="p-8 text-center">
<p>Redirecting to new task page...</p>
</div>
);
}-
Always use Next.js navigation primitives:
- Use
<Link>for user-clickable links - Use
router.push()for programmatic navigation - Use
router.replace()for redirects - Never manipulate browser history or location directly
- Use
-
Preserve the SPA experience:
- Ensure navigation doesn't trigger full page reloads
- Maintain scroll position appropriately
- Keep UI responsive during navigation
-
Server-side rendering for initial data:
- Use
getServerSidePropsfor dynamic data - Use
getStaticProps+getStaticPathsfor static data - Properly handle loading and error states
- Use
-
Client-side data fetching for updates:
- Use SWR or React Query for data fetching
- Implement proper caching strategy
- Add optimistic UI updates for actions
-
Global state:
- Use React Context for app-wide state
- Ensure providers are at the appropriate level
-
Server state:
- Use SWR/React Query for server data
- Implement proper revalidation strategies
-
Page/Component state:
- Use useState/useReducer for local component state
- Leverage Next.js router for URL-driven state
The current routing implementation in the Tasks App creates unnecessary complexity and performance issues. By adopting proper Next.js patterns, we can significantly improve the user experience, performance, and code maintainability.
This implementation plan provides a systematic approach to fixing the routing issues while ensuring backward compatibility and minimal disruption to users.