skip to Main Content

In my next.js 14 project, I need to pass a state from a child component to the parent component.

Currently, I declared the state in the parent component and passed it to children. ( thus making everything under the parent component client side)

But I want to keep things server component when possible. Here’s a minimal reproducible example of what I’m trying to do:

The state I have is isFocused. The navbar has a button to toggle it. When it’s enabled. The whole website is just,

<Navbar/>
<FocusedPage />

If not enabled,

<Navbar/>
<Hero />
<FocusedPage />
...

Now whether to render the <Hero /> component or not, I need to know if isFocused is true or false.

So, I’m keeping it under the client parent component. Thus, <Hero /> also becomes a client side component.

2

Answers


  1. What you are doing is called "Lifting state up": https://react.dev/learn/sharing-state-between-components

    There is no way to pass state from child to parent without it. Data flow in react is unidirectional.

    Others solution for your case are:

    Login or Signup to reply.
  2. First, you should know that it’s not a bad practice to use client components, they exist for a reason, and that reasons are mentioned in NextJS Docs:

    • Interactivity: Client Components can use state, effects, and event listeners, meaning they can provide immediate feedback to the user and update the UI.
    • Browser APIs: Client Components have access to browser APIs, like geolocation or localStorage.

    Also, as per Lee Robinson from Vercel:

    Client Components aren’t a de-opt, nor a cop out. You can think of Server Components as additive to the existing model. I understand the sentiment of wanting to use Server Components for lots of things (not bad!) but it’s also totally okay to use Client Components for certain things

    However, Sometimes (like in your situation), you need to keep everything server component but the interactivity part to be a client component.

    Lifting state up from a child client component (because of the onClick event listener) to the parent server component is not an option and it will throw Runtime Error:

    Unhandled Runtime Error

    Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".

    Solution

    The most common way to "lift the state up" in NextJS is by using URLSearchParams you will use it in many situations especially when you want to pass data from client to server, but that’s not the only use case though.

    To take advantage of URLSearchParams, you have to tweak your code a little bit.

    Currently, you have Four components and one page.tsx all of the components will be server components except the Navbar.tsx, you will put your state in Navbar.tsx instead of Home.tsx because that component is responsible for changing state, also will have event listener to change the state and add the query param to the URL to use it later in server components.

    Navbar.tsx

    'use client'; // make sure it's a client component
    
    import { useRouter } from 'next/navigation';
    import { useState, useEffect } from 'react';
    const Navbar = () => {
      // bring the state here
      const [focusMode, setFocusMode] = useState(false);
      const router = useRouter();
    
      // useEffect will append the query param whenever the state changes
      useEffect(() => {
        router.push(`?focusMode=${focusMode}`);
      }, [focusMode]);
    
      return (
        <nav className="flex items-center justify-between border-b-2 border-gray-400 px-5 py-5">
          <a className="text-2xl font-bold tracking-tighter" href="/">
            Next.js
          </a>
          <div>
            <a href="/" className="mr-5 underline">
              Home
            </a>
            <button
              className="rounded-md border bg-gray-700 p-2 font-semibold text-gray-100 hover:bg-gray-500"
              onClick={() => setFocusMode((prevIsFocused) => !prevIsFocused)}
            >
              Toggle Focus Button
            </button>
          </div>
        </nav>
      );
    };
    
    export default Navbar;
    

    By default, page.tsx recives searchParams which is an object containing all query params, you can pass it to home.tsx:

    page.tsx

    import Home from '@/components/Home';
    
    interface IProps {
      params: {};
      searchParams: {
        focusMode: string;
      };
    }
    export default function Root({ searchParams }: IProps) {
      return (
        <>
          <Home focusMode={JSON.parse(searchParams?.focusMode ?? false)} />
        </>
      );
    }
    

    JSON.parse needed to convert true/false from string to boolean since everything you get from the url is string.

    searchParams?.focusMode ?? false is needed because first render you will not have any searchParams so it will return undefined.

    Finally, this how you Home.tsx will look like:

    Home.tsx

    import Navbar from '@/components/Navbar';
    import Hero from './Hero';
    import Focused from '@/components/Focused';
    
    interface IProps {
      focusMode: boolean;
    }
    function Home({ focusMode }: IProps) {
      return (
        <main className="text-center">
          <Navbar />
          {!focusMode && <Hero />}
          <Focused focusMode={focusMode} />
        </main>
      );
    }
    
    export default Home;
    

    all other files remained as they were.

    Bonus

    If you want to make sure that all components are server components, try console.log(window) in any of them and you will see error (except for Navbar.tsx because it’s a client component):

    enter image description here

    Check out the working example on StackBlitz

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search