Next.js Discord

Discord Forum

Components Structure (Client/Server)

Unanswered
Romain posted this in #help-forum
Open in Discord
Hello everyone,

I'm writing about component creation and how to deal with client and server components. I have a problem. My header component needs an animation that requires it to be a client component. However, inside the header component, I have children that need to be server components. The issue is that I'm declaring my header component as a client component because of the Framer Motion animation I'm applying to it. I really don't know how to fix this issue and how to structure my code so I can have my header animation and still have the header child components be server components.

Can anyone help me resolve this? Thank you!

'use client'
import React, {useState} from "react";
import {motion, useMotionValueEvent, useScroll} from "framer-motion";
import HeaderTopBar from "@/components/layout/header/header-top-bar";
import HeaderBottomBar from "@/components/layout/header/header-bottom-bar";

export default function Header() {
    const {scrollY} = useScroll();
    const [hidden, setHidden] = useState(false);

    useMotionValueEvent(scrollY, 'change', (latest) => {
        const previous = scrollY.getPrevious();
        if (previous !== undefined && latest > previous && latest > 150) {
            setHidden(true);
        } else {
            setHidden(false);
        }
    });
    return (
        <motion.header
            variants={{
                visible: {y: 0},
                hidden: {y: '-100%'}
            }}
            transition={{duration: 0.3, ease: 'easeInOut'}}
            animate={hidden ? 'hidden' : 'visible'}
            className="sticky top-0 z-50 w-screen bg-white"
        >
            <HeaderTopBar/>
            <HeaderBottomBar/>
        </motion.header>
    )
}

21 Replies

I think you can make the raw item to be rendered on server and what needs to be animated just abstract it into a client component and pass the children to the client from the server if that makes any sense
American Crow
layout / page.tsx
// parent component is a server component
...
return (
<div>
 <Header> // client component, pass children
   <HeaderTopBar /> // will be a server component
   <HeaderBottomBar /> // will be a server component
 </Header>
</div>
)
<OverlayProvider>
      <SearchProvider>
        <Announcement />
        <Header />
        <main>{children}</main>
        <Footer />
        <Overlay />
      </SearchProvider>
    </OverlayProvider>


The thing is that in my header component, all the components that I'm adding becomes client components. For example in the <HeaderTopBar /> I need to import my <Cart/> components that needs next/headers and it has to be a server components, but I keep gettings errors about it. I can't figure this out lol
American Crow
You have to look at the component in which you import others components
If your parent component is a client component and you directly import other components into it. They also become client components.
You have to work with children as shown above to avoid that
"use client" is a boundary it says "components below me / imported by me, are client components"
// Header.tsx
export default function Header() {
    return (
        <HeaderWrapper>
            <HeaderTopBar/>
            <HeaderBottomBar/>
        </HeaderWrapper>
    )
}


// headerWrapper.tsx
export function HeaderWrapper({children}: { children: React.ReactNode }) {
    const {scrollY} = useScroll();
    const [hidden, setHidden] = useState(false);

    useMotionValueEvent(scrollY, 'change', (latest) => {
        const previous = scrollY.getPrevious();
        if (previous !== undefined && latest > previous && latest > 150) {
            setHidden(true);
        } else {
            setHidden(false);
        }
    });
    return (
        <motion.header
            variants={{
                visible: {y: 0},
                hidden: {y: '-100%'}
            }}
            transition={{duration: 0.3, ease: 'easeInOut'}}
            animate={hidden ? 'hidden' : 'visible'}
            className="sticky top-0 z-50 w-screen bg-white"
        >
            {children}
        </motion.header>
    )
}
So something like this would fix my issue?
And Header Wrapper would use the 'use client'
I encounter this error: TypeError: Cannot read properties of null (reading 'useReducer')
Layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className={`${cormorant.variable}`}>
    <body>
    <OverlayProvider>
      <SearchProvider>
        <Announcement />
        <Header />
        <main>{children}</main>
        <Footer />
        <Overlay />
      </SearchProvider>
    </OverlayProvider>
    </body>
    </html>
  );
}

Header.tsx
import HeaderWrapper from "@/components/layout/header/header-wrapper";
import HeaderTopBar from "@/components/layout/header/header-top-bar";
import HeaderBottomBar from "@/components/layout/header/header-bottom-bar";

export default function Header() {
    return (
        <HeaderWrapper>
            <HeaderTopBar/>
            <HeaderBottomBar/>
        </HeaderWrapper>
    )
}
HeaderWrapper.tsx

'use client';
import React, {useState} from "react";
import {motion, useMotionValueEvent, useScroll} from "framer-motion";

export default function HeaderWrapper({children,}: { children: React.ReactNode }) {
    const {scrollY} = useScroll();
    const [hidden, setHidden] = useState(false);

    useMotionValueEvent(scrollY, 'change', (latest) => {
        const previous = scrollY.getPrevious();
        if (previous !== undefined && latest > previous && latest > 150) {
            setHidden(true);
        } else {
            setHidden(false);
        }
    });
    return (
        <motion.header
            variants={{
                visible: {y: 0},
                hidden: {y: '-100%'}
            }}
            transition={{duration: 0.3, ease: 'easeInOut'}}
            animate={hidden ? 'hidden' : 'visible'}
            className="sticky top-0 z-50 w-screen bg-white"
        >
            {children}
        </motion.header>
    )
}
HeaderTopBar.tsx
import Link from "next/link";
import {Logo} from "@/components/icons";
import IconNavigation from "@/components/layout/header/icon-navigation";
import CountrySelector from "@/components/layout/header/country-selector";
import MobileNavigation from "@/components/layout/navigation/mobile-navigation";

export default function HeaderTopBar() {
    return (
        <div className="relative z-50 flex flex-row items-center justify-center border-b border-black/10 bg-white px-4 py-8 md:px-16">
            <div className="flex flex-1 items-center justify-start">
                <CountrySelector/>
                <MobileNavigation/>
            </div>
            <div className="flex flex-1 items-center justify-center">
                <Link href={'/'}>
                    <Logo className='text-black'/>
                </Link>
            </div>
            <IconNavigation/>
        </div>
    );
}
HeaderBottomBar.tsx
import Navigation from "@/components/layout/navigation";
import Search from "@/components/search";

export default function HeaderBottomBar() {
    return (
        <div className="relative z-40 hidden w-screen md:flex">
            <Navigation/>
            <Search/>
        </div>
    );
}
American Crow
Gotta simplify to locate the error better might be in your providers.
Test if the error is within header

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className={`${cormorant.variable}`}>
    <body>

        <HeaderWrapper>
          <HeaderTopBar/>
          <HeaderBottomBar/>
         </HeaderWrapper>
        <main>{children}</main>
   
    </body>
    </html>
  );
}
It's within one of this two components, I still have the same error
American Crow
comment each out,test which one it is
Inside my header top bar I have a Search button to toggle the search bar located inside the header bottom bar. I have setup a Context Provider in order to manage the state of the button
I think that's the issue
SearchContext.tsx
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { OverlayContext } from './overlayContext';

type SearchContextType = {
  isSearchBarVisible: boolean;
  toggleSearchBar: () => void;
};

export const SearchContext = createContext<SearchContextType>({
  isSearchBarVisible: true,
  toggleSearchBar: () => {}
});

export default function SearchProvider({ children }: { children: React.ReactNode }) {
  const [isSearchBarVisible, setSearchBarVisible] = useState(false);
  const { isOverlayVisible, toggleOverlay } = useContext(OverlayContext);

  const toggleSearchBar = () => {
    const newSearchBarState = !isSearchBarVisible;
    setSearchBarVisible(newSearchBarState);

    if (newSearchBarState && !isOverlayVisible) {
      toggleOverlay();
    } else if (!newSearchBarState && isOverlayVisible) {
      toggleOverlay();
    }
  };

  useEffect(() => {
    if (!isOverlayVisible && isSearchBarVisible) {
      setSearchBarVisible(false);
    }
  }, [isOverlayVisible]);

  return (
    <SearchContext.Provider value={{ isSearchBarVisible, toggleSearchBar }}>
      {children}
    </SearchContext.Provider>
  );
}