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>
)
}