Solving Language Switching in Next.js: A Guide to Client-Server Component Architecture

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:

  1. Fetch and display posts from a Notion database
  2. Support language switching between English and Chinese
  3. Format content like dates according to the selected language

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:

  • Securely fetches data using API keys that aren't exposed to the client
  • Uses the revalidate directive to control caching
  • Has no UI language dependencies
  • Passes only serializable data to the client 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:

  • Uses the "use client" directive to indicate client-side rendering
  • Imports and uses the translation and language context hooks
  • Defines all UI components that need language switching
  • Implements language-specific date formatting
  • Receives and displays data fetched by the server component


Key Implementation Details

1. Maintaining a Clear Separation of Concerns

The server component (page.js) is responsible for:

  • Secure data fetching from external APIs
  • Implementing server-side features like revalidate
  • Passing serializable data to client components

The client component (BlogUI.jsx) is responsible for:

  • UI rendering and interactivity
  • Language switching and internationalization
  • User interface components that depend on language context

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:

  • Gets the current language from the language context
  • Formats dates differently based on whether English or Chinese is selected
  • Dynamically translates the "Published on" text
  • Handles different formatting for small and standard displays

3. Avoiding Common Pitfalls

We encountered and solved several key issues:

  1. Never pass functions between server and client components
  2. Keep server-specific features in server components
  3. Ensure API calls with sensitive tokens run server-side


Conclusion

By properly separating our blog page into server and client components, we successfully implemented a solution that:

  1. Securely fetches data from Notion on the server
  2. Supports dynamic language switching on the client
  3. Correctly formats dates based on the selected language
  4. Avoids common Next.js pitfalls related to component boundaries

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.

To view or add a comment, sign in

Explore content categories