Home > OS >  How to get each component to display correct Children?
How to get each component to display correct Children?

Time:03-16

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

  • Related