DEV Community

Ray Ch
Ray Ch

Posted on

Fixing React Error #306 in Next.js App Router

What is React Error #306?

React Error #306 occurs when you try to pass a non-serializable object (like a function, class instance, or complex object) across the server/client component boundary in Next.js App Router.

The full error message is:

Objects are not valid as a React child (found: [object Object]). If you meant to render a collection of children, use an array instead.

In production, you'll see the minified version:

Error: Minified React error #306; visit https://react.dev/errors/306?args[]=%5Bobject%20Object%5D&args[]= for the full message
Enter fullscreen mode Exit fullscreen mode

Why Does It Work Locally But Fail in Production?

Environment Behavior
Development (npm run dev) React is lenient and allows some serialization issues to pass silently
Production (npm run build && npm start) React strictly enforces serialization rules and throws errors

This happens because:

  1. Development mode prioritizes developer experience with helpful warnings
  2. Production mode optimizes for performance with strict validation
  3. Minification in production removes detailed error messages

Common Causes

1. Missing 'use client' Directive

Components using hooks, browser APIs, or UI libraries like MUI must be client components.

Problem:

// ❌ Server component by default - will fail with MUI
import Box from '@mui/material/Box';

export default function Page() {
  return <Box>Hello</Box>;
}
Enter fullscreen mode Exit fullscreen mode

Solution:

// ✅ Explicitly mark as client component
'use client';

import Box from '@mui/material/Box';

export default function Page() {
  return <Box>Hello</Box>;
}
Enter fullscreen mode Exit fullscreen mode

2. Router Object in Dependency Arrays

The router object from useRouter() contains non-serializable functions and symbols.

Problem:

'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export function ProtectedRoute({ children }) {
  const router = useRouter();

  useEffect(() => {
    router.push('/login');
  }, [router]); // ❌ Router in dependency array causes serialization issues

  return <>{children}</>;
}
Enter fullscreen mode Exit fullscreen mode

Solution:

'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export function ProtectedRoute({ children }) {
  const router = useRouter();

  useEffect(() => {
    router.push('/login');
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // ✅ Router is stable, safe to omit from deps

  return <>{children}</>;
}
Enter fullscreen mode Exit fullscreen mode

3. Non-Memoized Context Values

Creating a new object on every render causes React to attempt serialization.

Problem:

'use client';

export const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const login = () => { /* ... */ };
  const logout = () => { /* ... */ };

  return (
    <AuthContext.Provider
      value={{ isAuthenticated, login, logout }} // ❌ New object every render
    >
      {children}
    </AuthContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Solution:

'use client';
import React, { useState, useCallback, useMemo } from 'react';

export const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const login = useCallback(() => { /* ... */ }, []);
  const logout = useCallback(() => { /* ... */ }, []);

  const contextValue = useMemo(
    () => ({ isAuthenticated, login, logout }),
    [isAuthenticated, login, logout]
  ); // ✅ Memoized object

  return (
    <AuthContext.Provider value={contextValue}>
      {children}
    </AuthContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

4. Functions Without useCallback in Context

Functions recreated on every render cause context value changes.

Problem:

const searchInvestors = async (query) => { // ❌ Recreated every render
  return investors.filter(i => i.name.includes(query));
};
Enter fullscreen mode Exit fullscreen mode

Solution:

const searchInvestors = useCallback(async (query) => { // ✅ Stable reference
  return investors.filter(i => i.name.includes(query));
}, [investors]);
Enter fullscreen mode Exit fullscreen mode

Real-World Cases & Fixes

Case 1: Dashboard Layout Missing Client Directive

File: src/app/dashboard/layout.tsx

Before:

import { ProtectedRoute } from '@/components/ProtectedRoute';
import { SideNav } from '@/components/dashboard/layout/side-nav';

export default function Layout({ children }) {
  return (
    <ProtectedRoute>
      <SideNav />
      {children}
    </ProtectedRoute>
  );
}
Enter fullscreen mode Exit fullscreen mode

After:

'use client';

import { ProtectedRoute } from '@/components/ProtectedRoute';
import { SideNav } from '@/components/dashboard/layout/side-nav';

export default function Layout({ children }) {
  return (
    <ProtectedRoute>
      <SideNav />
      {children}
    </ProtectedRoute>
  );
}
Enter fullscreen mode Exit fullscreen mode

Case 2: AuthProvider with Router and Unmemoized Context

File: src/components/AuthProvider.tsx

Before:

'use client';
import { useRouter } from 'next/navigation';

export const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const router = useRouter();

  useEffect(() => {
    checkAuth();
  }, []); // Missing router but using it

  const login = (tokens) => {
    setIsAuthenticated(true);
    router.push('/dashboard/');
  };

  const logout = () => {
    setIsAuthenticated(false);
    router.push('/auth/login');
  };

  return (
    <AuthContext.Provider value={{ isAuthenticated, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

After:

'use client';
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { useRouter } from 'next/navigation';

export const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const router = useRouter();

  useEffect(() => {
    checkAuth();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const login = useCallback((tokens) => {
    setIsAuthenticated(true);
    router.replace('/dashboard/');
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const logout = useCallback(() => {
    setIsAuthenticated(false);
    window.location.href = '/portal/auth/login'; // Full redirect avoids basePath issues
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const contextValue = useMemo(
    () => ({ isAuthenticated, login, logout }),
    [isAuthenticated, login, logout]
  );

  return (
    <AuthContext.Provider value={contextValue}>
      {children}
    </AuthContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Case 3: ProtectedRoute with Router Dependency

File: src/components/ProtectedRoute.tsx

Before:

'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export const ProtectedRoute = ({ children }) => {
  const { isAuthenticated, isLoading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!isLoading && !isAuthenticated) {
      router.push('/auth/login');
    }
  }, [isLoading, isAuthenticated, router]); // ❌ Router in deps

  return <>{children}</>;
};
Enter fullscreen mode Exit fullscreen mode

After:

'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export const ProtectedRoute = ({ children }) => {
  const { isAuthenticated, isLoading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!isLoading && !isAuthenticated) {
      router.push('/auth/login');
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isLoading, isAuthenticated]); // ✅ Router omitted

  return <>{children}</>;
};
Enter fullscreen mode Exit fullscreen mode

Case 4: SupportContext with Multiple Functions

File: src/components/support/context/SupportContext.tsx

Before:

'use client';

export const SupportProvider = ({ children }) => {
  const [investors, setInvestors] = useState([]);

  const searchInvestors = async (query) => { /* ... */ };
  const addInvestorTab = (investor) => { /* ... */ };
  const removeInvestorTab = (id) => { /* ... */ };

  return (
    <SupportContext.Provider value={{
      investors,
      searchInvestors,
      addInvestorTab,
      removeInvestorTab,
    }}>
      {children}
    </SupportContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

After:

'use client';
import React, { useState, useCallback, useMemo } from 'react';

export const SupportProvider = ({ children }) => {
  const [investors, setInvestors] = useState([]);

  const searchInvestors = useCallback(async (query) => { /* ... */ }, [investors]);
  const addInvestorTab = useCallback((investor) => { /* ... */ }, []);
  const removeInvestorTab = useCallback((id) => { /* ... */ }, []);

  const contextValue = useMemo(() => ({
    investors,
    searchInvestors,
    addInvestorTab,
    removeInvestorTab,
  }), [investors, searchInvestors, addInvestorTab, removeInvestorTab]);

  return (
    <SupportContext.Provider value={contextValue}>
      {children}
    </SupportContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Best Practices Checklist

When to Use 'use client'

Add 'use client' directive when your component:

  • Uses React hooks (useState, useEffect, useContext, etc.)
  • Uses browser APIs (window, document, localStorage)
  • Uses UI libraries like MUI, Chakra UI, or styled-components
  • Imports client-only components (like ProtectedRoute)
  • Uses event handlers (onClick, onChange, etc.)

Context Best Practices

  • Wrap all context functions with useCallback
  • Memoize context value object with useMemo
  • Keep context providers as client components
  • Avoid passing router or non-serializable objects through context

Router Usage

  • Never include router in useEffect dependency arrays
  • Use eslint-disable-next-line comment when omitting required deps
  • Consider window.location.href for full-page redirects to avoid basePath issues
  • Use router.replace() instead of router.push() for auth redirects

Debugging Tips

1. Enhanced Error Logging

Create an error.tsx file with detailed logging:

'use client';
import { useEffect } from 'react';

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    console.error('Global app error:', error);
    console.error('Error name:', error.name);
    console.error('Error message:', error.message);
    console.error('Error stack:', error.stack);
    console.error('Error digest:', error.digest);
    console.error('Full error:', JSON.stringify(error, Object.getOwnPropertyNames(error)));
  }, [error]);

  return (
    <html>
      <body>
        <h1>Something went wrong</h1>
        <p>{error.message}</p>
        {error.stack && <pre>{error.stack}</pre>}
        <button onClick={reset}>Try again</button>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Temporarily Disable Minification

Add to next.config.mjs to see full error messages:

const nextConfig = {
  // ... other config
  webpack: (config, { dev, isServer }) => {
    if (!dev && !isServer) {
      config.optimization.minimize = false;
    }
    return config;
  },
};
Enter fullscreen mode Exit fullscreen mode

Warning: Remove this before deploying to production as it significantly increases bundle size.

3. Enable Source Maps

const nextConfig = {
  productionBrowserSourceMaps: true,
  // ... other config
};
Enter fullscreen mode Exit fullscreen mode

4. Check Component Hierarchy

Use React DevTools to trace where non-serializable props might be passed:

  1. Look for components crossing server/client boundaries
  2. Check what props are being passed to client components
  3. Verify all layouts and pages have correct directives

Quick Reference

Symptom Likely Cause Fix
Error only in production Serialization issue Add 'use client' or memoize
Error mentions [object Object] Non-serializable prop Use useMemo/useCallback
Error in layout/page with MUI Missing client directive Add 'use client'
Error after adding router to deps Router serialization Remove router from deps
Error in context provider Unmemoized context value Memoize with useMemo

Top comments (0)