I've created Layout component which is macro page layout that consists of two sub-components: BodyContent
and RightPanel
. When rightPanelOpen
prop is set to true, BodyContent
takes 70% of viewport width and RightPanel
takes 30% of width. If rightPanelOpen
is set to false then BodyContent
takes 100% width.
Problem I'm facing is regards to how each sub-component is displaying children prop. For BodyContent
, I was expecting it to display "This is for Body content" and for RightPanel
, I was expecting to see "Right Side Panel". But I see entire sentence "This is for Body content Right Side Panel" displayed on each sub-component. Not sure how I can dissect the children prop further from here. What am I doing wrong? https://codesandbox.io/s/layout-demo-ts-nsvceu?file=/src/App.tsx:845-906
App.tsx
import "./styles.css";
import React from "react";
import { RightPanel } from "./RightPanel";
import { BodyContent } from "./BodyContent";
export interface LayoutProps {
rightPanelOpen?: boolean;
children?: React.ReactNode;
}
export const Layout: React.FunctionComponent<LayoutProps> & {
RightPanel: typeof RightPanel;
BodyContent: typeof BodyContent;
} = function Layout({ rightPanelOpen = false, children }: LayoutProps) {
const content = rightPanelOpen ? (
<>
<BodyContent sectioned={true}>{children}</BodyContent>
<RightPanel>{children}</RightPanel>
</>
) : (
<BodyContent sectioned={false}>{children}</BodyContent>
);
return <div className="layout-flex">{content}</div>;
};
export default function App() {
return (
<Layout rightPanelOpen={true}>
<Layout.BodyContent sectioned={true}>
<h1> This is for </h1>
<h2> Body content </h2>
</Layout.BodyContent>
<Layout.RightPanel>
<h1>Right Side Panel</h1>
</Layout.RightPanel>
</Layout>
);
}
Layout.RightPanel = RightPanel;
Layout.BodyContent = BodyContent;
BodyContent.tsx
import "./styles.css";
export interface BodyContentProps {
children?: React.ReactNode;
sectioned?: boolean;
}
export function BodyContent({ children, sectioned = false }: BodyContentProps) {
return (
<div className={sectioned ? "bodyWidth-70" : "bodyWidth-100"}>
{children}
</div>
);
}
RightPanel.tsx
import "./styles.css";
export interface RightPanelProps {
children?: React.ReactNode;
}
export function RightPanel({ children }: RightPanelProps) {
return <div className="rightSideWidth">{children}</div>;
}
CodePudding user response:
I came up with two versions, the first take always both BodyContent
and RightPanel
as children:
import "./styles.css";
import React, { cloneElement, ReactElement } from "react";
import { RightPanel, RightPanelProps } from "./RightPanel";
import { BodyContent, BodyContentProps } from "./BodyContent";
export interface LayoutProps {
rightPanelOpen?: boolean;
children: [ReactElement<BodyContentProps>, ReactElement<RightPanelProps>];
}
export const Layout: React.FunctionComponent<LayoutProps> & {
RightPanel: typeof RightPanel;
BodyContent: typeof BodyContent;
} = function Layout({
rightPanelOpen = false,
children: [bodyContent, rightPanel]
}: LayoutProps) {
const content = rightPanelOpen ? (
<>
{cloneElement(bodyContent, { sectioned: true })}
{rightPanel}
</>
) : (
<>{bodyContent}</>
);
return <div className="layout-flex">{content}</div>;
};
export default function App() {
return (
<Layout rightPanelOpen={true}>
<Layout.BodyContent>
<h1> This is for </h1>
<h2> Body content </h2>
</Layout.BodyContent>
<Layout.RightPanel>
<h1>Right Side Panel</h1>
</Layout.RightPanel>
</Layout>
);
}
Layout.RightPanel = RightPanel;
Layout.BodyContent = BodyContent;
The second version instead take RightPanel
only if rightPanelOpen
is true
. I had to drop the props deconstructuring in the function paramenters because it seems to mess with discriminated unions (it should work fine in more recent version of typescript):
import "./styles.css";
import React, { cloneElement, ReactElement } from "react";
import { RightPanel, RightPanelProps } from "./RightPanel";
import { BodyContent, BodyContentProps } from "./BodyContent";
export type LayoutProps =
| {
rightPanelOpen?: true;
children: [ReactElement<BodyContentProps>, ReactElement<RightPanelProps>];
}
| {
rightPanelOpen?: false;
children: ReactElement<BodyContentProps>;
};
export const Layout: React.FunctionComponent<LayoutProps> & {
RightPanel: typeof RightPanel;
BodyContent: typeof BodyContent;
} = function Layout(props: LayoutProps) {
let content;
if (props.rightPanelOpen) {
const [bodyContent, rightPanel] = props.children;
content = (
<>
{cloneElement(bodyContent, { sectioned: true })}
{rightPanel}
</>
);
} else {
content = <>{props.children}</>;
}
return <div className="layout-flex">{content}</div>;
};
export default function App() {
return (
<Layout rightPanelOpen={true}>
<Layout.BodyContent>
<h1> This is for </h1>
<h2> Body content </h2>
</Layout.BodyContent>
<Layout.RightPanel>
<h1>Right Side Panel</h1>
</Layout.RightPanel>
</Layout>
);
}
Layout.RightPanel = RightPanel;
Layout.BodyContent = BodyContent;
In both versions i tried to specify exactly the children the component expects. I also moved where sectioned
is specified (based on rightPanelOpen
)
Demo: https://codesandbox.io/s/layout-demo-ts-forked-2lfyts?file=/src/App.tsx