mr_sky
September 5, 2024, 1:37pm
1
Getting an Error as Error: React.Children.only expected to receive a single React element child.
While Integrating Internet Identity on Next.
Here’s the layout.tsx file
import { Analytics } from '@vercel/analytics/react'
import { cn } from '@/lib/utils'
import { TailwindIndicator } from '@/components/tailwind-indicator'
import { Providers } from '@/components/providers'
import { Header } from '@/components/header'
import { Toaster } from '@/components/ui/sonner'
import { KasadaClient } from '@/lib/kasada/kasada-client'
import { IdentityProvider } from '../context/AppContext'
export const metadata = {
metadataBase: new URL('https://gemini.vercel.ai'),
title: {
default: 'abc',
template: ``
},
description:
'xyz',
icons: {
icon: '/favicon.ico',
shortcut: '/favicon-16x16.png',
apple: '/apple-touch-icon.png'
}
}
export const viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'white' },
{ media: '(prefers-color-scheme: dark)', color: 'black' }
]
}
interface RootLayoutProps {
children: React.ReactNode
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={cn(
'font-sans antialiased',
GeistSans.variable,
GeistMono.variable
)}
>
<IdentityProvider>
<KasadaClient />
<Toaster position="top-center" />
<Providers
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<div className="flex flex-col min-h-screen bg-black">
<Header />
<main className="flex flex-col flex-1">{children}</main>
</div>
<TailwindIndicator />
</Providers>
<Analytics />
</IdentityProvider>
</body>
</html>
)
}
Providers.tsx file
'use client'
import * as React from 'react'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { ThemeProviderProps } from 'next-themes/dist/types'
import { SidebarProvider } from '@/lib/hooks/use-sidebar'
import { TooltipProvider } from '@/components/ui/tooltip'
export function Providers({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider {...props}>
<SidebarProvider>
<TooltipProvider>{children}</TooltipProvider>
</SidebarProvider>
</NextThemesProvider>
)
}
Been stuck here for long. Can anybody help?
Thanking you in advance
Can you please double-check where the error is coming from?
The ThemesProvider from the next-themes library can accept multiple children elements. There are many NextJS examples showing so.
I assume that the error is coming from another provider (IdentityProvider, SidebarProvider, TooltipProvider). However, without looking into the custom code of the other providers, it is difficult to pinpoint which one allows only one child.
mr_sky
September 9, 2024, 10:58am
3
Yeah, you’re right. It seems like the issue is coming from the IdentityProvider, as it likely accepts only one child
SiderbarProvider code →
'use client'
import * as React from 'react'
const LOCAL_STORAGE_KEY = 'sidebar'
interface SidebarContext {
isSidebarOpen: boolean
toggleSidebar: () => void
isLoading: boolean
}
const SidebarContext = React.createContext<SidebarContext | undefined>(
undefined
)
export function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error('useSidebarContext must be used within a SidebarProvider')
}
return context
}
interface SidebarProviderProps {
children: React.ReactNode
}
export function SidebarProvider({ children }: SidebarProviderProps) {
const [isSidebarOpen, setSidebarOpen] = React.useState(true)
const [isLoading, setLoading] = React.useState(true)
React.useEffect(() => {
const value = localStorage.getItem(LOCAL_STORAGE_KEY)
if (value) {
setSidebarOpen(JSON.parse(value))
}
setLoading(false)
}, [])
const toggleSidebar = () => {
setSidebarOpen(value => {
const newState = !value
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newState))
return newState
})
}
if (isLoading) {
return null
}
return (
<SidebarContext.Provider
value={{ isSidebarOpen, toggleSidebar, isLoading }}
>
{children}
</SidebarContext.Provider>
)
}
tooptipProvider →
'use client'
import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { cn } from '@/lib/utils'
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-lg bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
and IdentityProvider →
'use client'
import React, {
createContext,
ReactNode,
useContext,
useState,
SetStateAction,
useEffect,
useMemo,
} from "react";
import { createAuthClient } from '../lib/auth';
const defaultValue = {
identity: "",
setIdentity: (): void => {},
};
export const IdentityContext = createContext<{
identity: string;
setIdentity: React.Dispatch<SetStateAction<string>>;
}>(defaultValue);
interface Props {
children: ReactNode;
}
export const IdentityProvider = ({ children }: Props) => {
const [identity, setIdentity] = useState("");
const value = useMemo(() => ({ identity, setIdentity }), [identity]);
useEffect(() => {
createAuthClient()
.then((authClient) => {
const existingIdentity = authClient
.getIdentity()
.getPrincipal()
.toString();
if(existingIdentity && existingIdentity !== "2vxsx-fae"){
setIdentity(existingIdentity);
}
})
.catch((err) => console.log(err));
}, [identity]);
return (
<IdentityContext.Provider value={value}>
{useMemo(
() => (
<>{children}</>
),
[children]
)}
</IdentityContext.Provider>
);
};
// eslint-disable-next-line react-refresh/only-export-components
export const useIdentity = () => useContext(IdentityContext);
{/*export const useIdentity = () => {
return useContext(IdentityContext);
}*/}
console.log(useIdentity)
console.log(typeof useIdentity)
From an initial glance, it looks like it does accept multiple children?
mr_sky
September 9, 2024, 5:08pm
5
But when I remove the IdentityProvider, everything works fine. It seems to be causing the issue.
Wrap the children in a react fragment .
Either
<>
<KasadaClient />
<Toaster position="top-center" />
// ...
</>
or
import {Fragment} from 'react';
<Fragment>
<KasadaClient />
<Toaster position="top-center" />
// ...
</Fragment>
1 Like
mr_sky
September 10, 2024, 11:16am
7
Nope still the same error
mr_sky
September 10, 2024, 11:53am
8
I think I’ve pinpointed the issue—it looks like the error is coming from here. Any chance you could take a quick look?
header.tsx
'use client'
/* eslint-disable @next/next/no-img-element */
import * as React from 'react'
import Link from 'next/link'
import { cn } from '@/lib/utils'
import { auth } from '@/auth'
import { Button, buttonVariants } from '@/components/ui/button'
import {
IconGitHub,
IconNextChat,
IconSeparator,
IconVercel
} from '@/components/ui/icons'
import { UserMenu } from '@/components/user-menu'
import { SidebarMobile } from './sidebar-mobile'
import { SidebarToggle } from './sidebar-toggle'
import { ChatHistory } from './chat-history'
import { Session } from '@/lib/types'
import { LoginButton } from './login-button'
import { IILogin, IILogout } from '../lib/auth';
import { useIdentity } from '../context/AppContext';
async function UserOrLogin() {
const session = (await auth()) as Session
return (
<>
{session?.user ? (
<>
<SidebarMobile>
<ChatHistory userId={session.user.id} />
</SidebarMobile>
<SidebarToggle />
</>
) : (
<Link href="/new" rel="nofollow">
<img className="size-6" src="/images/gemini.png" alt="gemini logo" />
</Link>
)}
<div className="flex items-center">
{session?.user ? (
<UserMenu user={session.user} />
) : (
<Button variant="link" asChild className="-ml-2">
<Link href="/">internet-identity</Link>
</Button>
)}
</div>
</>
)
}
export function Header() {
const { identity, setIdentity } = useIdentity();
const handleLogout = () => {
IILogout().then(() => {
setIdentity("");
});
};
const handleConnect = () => {
IILogin().then((id) => setIdentity(id));
};
return (
<header className="sticky top-0 z-50 flex items-center justify-between w-full h-16 px-4 shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
<div className="flex items-center">
<React.Suspense fallback={<div className="flex-1 overflow-auto" />}>
<UserOrLogin />
</React.Suspense>
</div>
<div className="flex items-center justify-end gap-2">
{identity ? (
<Button asChild size="sm" className="rounded-lg gap-1">
<span onClick={handleLogout} className="hidden sm:block">{identity.slice(0, 6) + "..." + identity.slice(-4)}</span>
<span onClick={handleLogout} className="sm:hidden">{identity.slice(0, 6) + "..." + identity.slice(-4)}</span>
</Button>
) : (
<Button asChild size="sm" className="rounded-lg gap-1">
<span onClick={handleConnect} className="hidden sm:block">Connect Wallet</span>
<span onClick={handleConnect} className="sm:hidden">Connect Wallet</span>
</Button>
)}
</div>
</header>
)
}