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
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:
- Development mode prioritizes developer experience with helpful warnings
- Production mode optimizes for performance with strict validation
- 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>;
}
Solution:
// ✅ Explicitly mark as client component
'use client';
import Box from '@mui/material/Box';
export default function Page() {
return <Box>Hello</Box>;
}
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}</>;
}
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}</>;
}
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>
);
};
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>
);
};
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));
};
Solution:
const searchInvestors = useCallback(async (query) => { // ✅ Stable reference
return investors.filter(i => i.name.includes(query));
}, [investors]);
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>
);
}
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>
);
}
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>
);
};
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>
);
};
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}</>;
};
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}</>;
};
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>
);
};
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>
);
};
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
routerinuseEffectdependency arrays - Use
eslint-disable-next-linecomment when omitting required deps - Consider
window.location.hreffor full-page redirects to avoid basePath issues - Use
router.replace()instead ofrouter.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>
);
}
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;
},
};
Warning: Remove this before deploying to production as it significantly increases bundle size.
3. Enable Source Maps
const nextConfig = {
productionBrowserSourceMaps: true,
// ... other config
};
4. Check Component Hierarchy
Use React DevTools to trace where non-serializable props might be passed:
- Look for components crossing server/client boundaries
- Check what props are being passed to client components
- 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)