我正在使用应用程序路由器构建一个 Next.js 应用程序,其中“layout.tsx”文件管理一个按钮状态,我想用它来渲染“page.tsx”中的不同内容。据我所知,“layout.tsx”充当“page.tsx”中内容的包装器,我需要将状态从布局传递到页面,以便它可以根据状态值确定要渲染哪个组件。但是,我不确定如何实现这一点。
由于我从layout.tsx 文件导出元数据,因此无法将其设为客户端渲染组件。因此,我明确创建了一个
RootLayoutClient.tsx
组件,它从父级 layout.tsx 接收子级。
这是目前的项目结构。
// layout.tsx
import type { Metadata } from "next";
import "./globals.css";
import RootLayoutClient from "./RootLayoutClient";
import { Open_Sans } from 'next/font/google';
const openSans = Open_Sans({
subsets: ['latin'],
display: 'swap',
});
export const metadata: Metadata = {
title: "xxxx",
description: "yyyy",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={openSans.className}>
<RootLayoutClient>{children}</RootLayoutClient>
</body>
</html>
);
}
// RootLayoutClient.tsx
'use client';
import Navbar from "./components/Navbar";
import { useState } from "react";
export default function RootLayoutClient({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const [selectedButton, setSelectedButton] = useState('today');
console.log(children);
return (
<div className="main">
<Navbar selectedButton={selectedButton} setSelectedButton={setSelectedButton} />
</div>
);
}
import Inbox from "./components/content/Inbox";
import Today from "./components/content/Today";
import Upcoming from "./components/content/Upcoming";
import FiltersAndLabels from "./components/content/FiltersAndLabels";
export default function Home({selectedButton}: {selectedButton: string}) {
console.log(selectedButton);
const renderContent = () => {
switch (selectedButton) {
case 'inbox':
return <Inbox/>
case 'today':
return <Today/>
case 'upcoming':
return <Upcoming/>
case 'filtersAndLabels':
return <FiltersAndLabels/>
default:
return null;
}
}
return (
<main>
{renderContent()}
</main>
);
}
有人可以帮我想办法解决这个问题吗?
布局,更一般地说是“包装器”,组件通常不会显式地将 props 传递给它们所包装的子组件。他们不知道要包装什么,也不知道具体要传递什么。
Children
助手和cloneElement
来注入额外的道具:
import { cloneElement, Children, useState } from "react";
export default function RootLayoutClient({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const [selectedButton, setSelectedButton] = useState('today');
return (
<div className="main">
<Navbar
selectedButton={selectedButton}
setSelectedButton={setSelectedButton}
/>
{Children.map(children, child => {
return cloneElement(child, { selectedButton, setSelectedButton });
})}
</div>
);
}
包装的子组件现在可以访问注入的
selectedButton
和 setSelectedButton
属性值。这通常在非 Typescript 项目中效果很好,但对于 TS,需要在包装的所有组件中了解这些附加属性,以便在这些组件的 props
接口中正确键入它们。
另一种可能更可取的解决方案是通过 React 上下文提供程序在 ReactTree 中提供“props”值。
示例:
const SelectButtonContext = createContext<{
selectedButton: string;
setSelectedButton: React.Dispatch<React.SetStateAction<string>>;
}>({
selectedButton: "";
setSelectedButton: (value: string) => {};
});
const useSelectButtonContext = React.useContext(SelectButtonContext);
export default function RootLayoutClient({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const [selectedButton, setSelectedButton] = useState('today');
return (
<div className="main">
<Navbar
selectedButton={selectedButton}
setSelectedButton={setSelectedButton}
/>
<SelectButtonContext.Provider
value={{ selectedButton, setSelectedButton }}
>
{children}
</SelectButtonContext.Provider>
</div>
);
}
export default function Home() {
const { selectedButton } = useSelectButtonContext();
const renderContent = () => {
switch (selectedButton) {
case 'inbox':
return <Inbox />;
case 'today':
return <Today />;
case 'upcoming':
return <Upcoming />;
case 'filtersAndLabels':
return <FiltersAndLabels />;
default:
return null;
}
}
return (
<main>
{renderContent()}
</main>
);
}