Reputation: 125
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.
Upvotes: -1
Views: 1598
Reputation: 3213
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".
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):
Upvotes: 1