Solving Language Switching in Next.js: A Guide to Client-Server Component Architecture
Introduction
Building multilingual applications in Next.js can present unique challenges, especially when balancing server-side data fetching with client-side language switching. In this article, I'll walk through a real-world case study where we encountered and resolved a series of issues while implementing language switching for a blog page that fetches data from a Notion database.
This technical guide will demonstrate how to properly separate client and server components in Next.js to achieve both efficient server-side data fetching and dynamic client-side language switching.
The Challenge
Our project required a blog page that could:
The main technical challenge arose from the conflicting requirements of these features. We needed server-side data fetching for security and performance, but also required client-side interactivity for language switching.
Initial Setup and Errors
Error 1: use client Directive Placement
Our first error appeared during the build process:
Error: The "use client" directive must be placed at the top of the file.
This was occurring in the file src/app/blog/page.js, where we had incorrectly positioned the 'use client' directive after other code rather than at the top of the file:
// Incorrect code
export const revalidate = 0;
'use client'; // This should be at the top
The fix was straightforward - moving the 'use client' directive to the very top of the file, ensuring it appears before any other code.
Error 2: Invalid Revalidate Function
After fixing the first error, we encountered a runtime error:
Error: Invalid revalidate value "function() { throw new Error(...) }" on "/blog", must be a non-negative number or false
This occurred because we were using both the 'use client' directive and the server-side export const revalidate = 0 in the same file. In Next.js, a client component cannot use server-side features like revalidate.
Error 3: Failed to Fetch Data
After removing the revalidate directive, we encountered another issue:
Error: Failed to fetch
src\lib\notion.js (143:53) @ getAllBlogPosts
This occurred because we were trying to fetch data from the Notion API on the client side, but this requires secure API keys that shouldn't be exposed to the client.
Error 4: Function Passing Between Server and Client Components
Our final error appeared when we tried to pass a function from a server component to a client component:
Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server"
The Solution: Splitting Components by Responsibility
To solve these issues, we implemented a hybrid architecture that separates data fetching and UI rendering responsibilities:
1. Server Component for Data Fetching
We converted the main page component to a server component responsible for data fetching:
// src/app/blog/page.js
import Link from 'next/link';
import Image from 'next/image';
import { MainLayout } from '../../components/layouts';
import { getAllBlogPosts } from '../../lib/notion';
import { blogConfig } from '../../lib/blog-config';
import BlogUI from '../../components/blog/BlogUI';
// Styles definition
const styles = {
featuredArticleContainer: {
marginBottom: '60px',
paddingBottom: '30px',
borderBottom: '1px solid #f1f1f1',
},
articleGridContainer: {
marginTop: '40px',
},
blogContent: {
display: 'flex',
flexDirection: 'column',
gap: '40px'
}
};
// Disable caching, always get the latest content
export const revalidate = 0;
// Main page component
export default async function BlogPage() {
// Server-side data fetching
const blogPosts = await getAllBlogPosts(blogConfig.databaseId);
const error = null; // Could be set in a try-catch block
return (
<MainLayout>
<BlogUI blogPosts={blogPosts} error={error} styles={styles} />
</MainLayout>
);
}
This server component:
2. Client Component for UI and Language Switching
We created a separate client component for UI rendering and language switching:
// src/components/blog/BlogUI.jsx
"use client";
import Link from 'next/link';
import Image from 'next/image';
import { useTranslation } from '../../hooks/useTranslation';
import { useLanguage } from '../../contexts/LanguageContext';
// Blog header component
function BlogHeader() {
const { t } = useTranslation();
return (
<header className="mb-14 pb-6 border-b border-gray-100 dark:border-gray-800">
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
{t('blog.title')}
</h1>
</header>
);
}
// Latest articles title component
function LatestArticlesTitle() {
const { t } = useTranslation();
return (
<h2 className="text-2xl font-medium mb-12 text-gray-900 dark:text-white mt-6">
{t('blog.subtitle')}
</h2>
);
}
// Empty blog display component
function BlogEmpty() {
const { t } = useTranslation();
return (
<div className="bg-white dark:bg-gray-800/80 rounded-lg p-10 max-w-lg mx-auto text-center shadow-sm border border-gray-100 dark:border-gray-700/50 my-24">
{/* SVG and content */}
<h3 className="text-xl font-medium mb-4 text-gray-900 dark:text-white">{t('blog.empty')}</h3>
<p className="text-gray-500 dark:text-gray-400">{t('blog.emptyDescription')}</p>
</div>
);
}
// Published date component
function PublishedDate({ createdAt, isSmall = false }) {
const { t } = useTranslation();
const { language } = useLanguage();
const formatDate = () => {
if (!createdAt) return '';
try {
if (language === 'en') {
return createdAt.toLocaleDateString('en-US', {
year: 'numeric',
month: isSmall ? 'short' : 'long',
day: 'numeric'
});
} else {
return createdAt.toLocaleDateString('zh-CN', {
year: 'numeric',
month: isSmall ? 'short' : 'long',
day: 'numeric'
});
}
} catch (e) {
console.error('Date formatting error:', e);
return createdAt.toString();
}
};
return (
<div className="flex items-center">
<time className={`${isSmall ? 'text-xs' : 'text-sm'} text-gray-500 dark:text-gray-400`}>
{isSmall ? formatDate() : `${t('blog.publishedOn')} ${formatDate()}`}
</time>
</div>
);
}
// Main component
export default function BlogUI({ blogPosts, error, styles }) {
const { t } = useTranslation();
// Loading state, error handling, and main content rendering
// ... (rest of the component)
}
This client component:
Key Implementation Details
1. Maintaining a Clear Separation of Concerns
The server component (page.js) is responsible for:
The client component (BlogUI.jsx) is responsible for:
2. Implementing the Date Formatting with Language Support
One crucial aspect was the date formatting component that adapts to the current language:
function PublishedDate({ createdAt, isSmall = false }) {
const { t } = useTranslation();
const { language } = useLanguage();
const formatDate = () => {
if (!createdAt) return '';
try {
if (language === 'en') {
return createdAt.toLocaleDateString('en-US', {
year: 'numeric',
month: isSmall ? 'short' : 'long',
day: 'numeric'
});
} else {
return createdAt.toLocaleDateString('zh-CN', {
year: 'numeric',
month: isSmall ? 'short' : 'long',
day: 'numeric'
});
}
} catch (e) {
console.error('Date formatting error:', e);
return createdAt.toString();
}
};
return (
<div className="flex items-center">
<time className={`${isSmall ? 'text-xs' : 'text-sm'} text-gray-500 dark:text-gray-400`}>
{isSmall ? formatDate() : `${t('blog.publishedOn')} ${formatDate()}`}
</time>
</div>
);
}
This component:
3. Avoiding Common Pitfalls
We encountered and solved several key issues:
Conclusion
By properly separating our blog page into server and client components, we successfully implemented a solution that:
This architecture pattern provides a blueprint for implementing multilingual applications in Next.js where both server-side data fetching and client-side interactivity are required. The key insight is to understand the distinct responsibilities of server and client components and to design a component architecture that respects these boundaries.
By following this pattern, you can build applications that benefit from both the security and performance advantages of server components and the interactive capabilities of client components, all while offering a seamless multilingual user experience.