Migrating from React to Next.js: Lessons Learned
Migrating from React to Next.js: Lessons Learned
When I was tasked with migrating samsara.social from React to Next.js, I knew it would be a significant undertaking. This post shares the challenges we faced, solutions we implemented, and the performance improvements we achieved.
Why Migrate to Next.js?
The decision to migrate wasn't taken lightly. Here were our main motivations:
- Server-Side Rendering (SSR): Improved SEO and initial page load performance
- File-based Routing: Simplified routing structure and better code organization
- API Routes: Built-in backend capabilities without separate server setup
- Image Optimization: Automatic image optimization with next/image
- Built-in Performance: Automatic code splitting and optimized bundle sizes
The Migration Process
1. Setting Up the Project Structure
First, we created a new Next.js project and gradually moved components over. We organized our project structure to match Next.js conventions with proper separation of concerns.
The key was to start with a clean Next.js 13+ app using the App Router. We created a parallel structure where we could test components before fully migrating them.
2. Converting React Router to Next.js Routing
One of the biggest changes was moving from React Router to Next.js file-based routing. This simplified our routing logic significantly and made the codebase more maintainable.
We had to convert dynamic routes from React Router's useParams to Next.js's params prop. Nested routes became folder structures, which was intuitive once we understood the pattern.
3. Handling State Management
We kept Redux for global state but optimized it for Next.js. The integration was seamless and allowed us to maintain our existing state management patterns.
However, we learned to leverage Server Components for data fetching where possible, reducing the need for client-side state management in many cases.
4. API Integration
We converted our API calls to use Next.js API routes and Server Actions, which improved performance and simplified our backend integration.
Server Actions were particularly powerful for form submissions and mutations, eliminating the need for separate API endpoints in many cases.
5. Component Migration Strategy
We migrated components in phases:
- Phase 1: Static components (headers, footers, layouts)
- Phase 2: Data-fetching components (lists, cards, profiles)
- Phase 3: Interactive components (forms, modals, complex interactions)
- Phase 4: Authentication and protected routes
This phased approach allowed us to test thoroughly at each stage and catch issues early.
Challenges We Faced
Client vs Server Components
Understanding when to use Client Components vs Server Components was initially confusing. We learned that Server Components are great for data fetching, while Client Components are necessary for interactivity.
The "use client" directive became our friend, but we learned to use it sparingly. Starting with Server Components and only adding "use client" when needed resulted in better performance.
Hydration Errors
We encountered hydration mismatches when using browser-only APIs. The solution was to use useEffect for client-only code and ensure server and client rendering matched.
Common culprits included: - Date formatting with different timezones - localStorage access during initial render - Random values or IDs generated during render - Browser-specific APIs like window or document
Image Optimization
Converting all img tags to Next.js Image components required careful attention to sizing and layout shifts.
We created a wrapper component that handled common image patterns and made migration easier. The performance gains from automatic optimization were worth the effort.
Environment Variables
Next.js has specific rules for environment variables. We had to prefix client-side variables with NEXT_PUBLIC_ and restructure our environment configuration.
Build Time Optimization
Our initial builds were slow. We optimized by: - Using dynamic imports for heavy components - Implementing proper code splitting - Optimizing our dependencies - Using SWC instead of Babel
Performance Improvements
The migration resulted in significant performance gains:
- First Contentful Paint: 62% faster (2.1s → 0.8s)
- Time to Interactive: 66% faster (3.5s → 1.2s)
- Lighthouse Score: +23 points (72 → 95)
- Bundle Size: 60% smaller (450KB → 180KB)
- Server Response Time: 45% faster (180ms → 99ms)
These improvements directly translated to better user experience and higher engagement metrics.
Best Practices We Discovered
1. Start with Server Components
Default to Server Components and only use Client Components when you need interactivity. This maximizes performance and simplifies data fetching.
2. Use Streaming and Suspense
Implement loading states with Suspense boundaries to improve perceived performance. Users see content faster, even if the full page isn't ready.
3. Optimize Images Aggressively
Use next/image everywhere. The automatic optimization, lazy loading, and modern format support are game-changers.
4. Leverage Parallel Routes
For complex layouts, parallel routes allow you to show multiple pages in the same layout, perfect for dashboards and split views.
5. Implement Proper Error Boundaries
Use error.tsx files to handle errors gracefully at different levels of your application.
Key Takeaways
1. Plan the migration in phases: Don't try to migrate everything at once. Start with static pages and gradually move to more complex features.
2. Understand Server vs Client Components: This is crucial for Next.js 13+. The mental model shift takes time but results in better architecture.
3. Leverage Next.js features: Use Image optimization, API routes, and built-in performance features. They're there for a reason.
4. Test thoroughly: Hydration errors can be subtle. Test on different devices and browsers.
5. Monitor performance: Use Lighthouse and Web Vitals to track improvements. The data will justify the migration effort.
6. Document patterns: Create internal documentation for common patterns. This helps the team adopt Next.js conventions consistently.
Tools That Helped
- Next.js DevTools: Essential for debugging Server Components and understanding rendering
- Lighthouse: For performance monitoring
- React DevTools: Still useful for Client Components
- Vercel Analytics: For real-world performance data
Conclusion
Migrating from React to Next.js was challenging but absolutely worth it. The performance improvements, better SEO, and improved developer experience have made our application significantly better.
The framework's opinions on routing, data fetching, and rendering have simplified our codebase and reduced the number of decisions we need to make. This has accelerated our development velocity.
If you're considering a similar migration, start small, learn the Next.js patterns, and gradually move your application over. The benefits are substantial, and your users will thank you for the improved performance.
The investment in learning Next.js pays dividends in the long run. Our team is now more productive, our application is faster, and we're better positioned for future growth.