javascriptreactjstypescriptjsxchildren

React.Children returns (function)string as `type` for Custom Components as Child with typescript


This may sound strange, maybe I completely get it wrong in the first place. But as I read some articles and react docs related to get the children and identify specific child via React.Component.map() however when I try this with my custom components, the child returns a stringify function as type. (I know that react does the stringify thing to prevent script injection). But I basically need to identify specific children that pass into the component and place them in the correct positions in another custom component. (material_ui style).

<Card>
 <CardTitle>
 </CardTitle>
 <CardContent>
 </CardContent>
</Card>

The problem is I can't map passed children since the type has a string. my environment uses

"react": "^17.0.2",
"@types/react": "^17.0.0",
"react-dom": "^17.0.2",
"@types/react-dom": "^17.0.0",
"typescript": "^4.1.2"

and this is what I have so far

type ExpandableCardProps = {
 children: ReactElement<any> | ReactElement<any>[],
}
const ExpandableCard = ({children}: ExpandableCardProps) => {

 React.Children.map(children, (child) => {
  concole.log(child); // just can't map the child elements as described in some articales
 })
 

 // note that I need to identify the correct child to be render in correct position
 render (
  <div>
   <div className="title-container">
    // I want to render <ExpandableTitle> here
   </div>
   <div className="content-container">
    // I want to render <ExpandableContent> here
   </div>
   <div className="content-other">
    // may be some other elements here
   </div>
  </div>
 );
}

export default ExpandableCardProps;

type CommonType = {
 children: ReactNode;
}

export const ExpandableTitle ({children}:CommonType) => {
 <div>
 {children}
 </div>
}

export const ExpandableContent ({children}:CommonType) => {
 <div>
 {children}
 </div>
}
// usage
<ExpandableCard>
 <ExpandableTitle>
  /*{ some jsx here }*/
 </ExpandableTitle>
 <ExpandableContent>
  /*{ some jsx here }*/
 </ExpandableContent>
</ExpandableCard>

Here's what it looks like in the console console view

Here's an article I was referring to and which explained most closely what I need, but Can't use the pattern it explained since the type stringify thing, wonder it's with the React version or maybe as I mentioned earlier it's completely misunderstood by myself. I need some insight into this. How can I achieve something like this?


Solution

  • After a few workarounds with the Neal Burns answer, I concluded with a typescript compatible solution.

    I Will post it here since for someone it may be come in handy someday.

    import React, { Children, ReactElement, ReactNode, useEffect, useRef, useState } from 'react';
    import { CSSTransition } from 'react-transition-group';
    import './expandableCard.scss';
    import { v4 as uuidv4 } from 'uuid'
    
    const types = {
      EXPANDABLE_CARD_HEADER: 'expandableCardTitle',
      EXPANDABLE_CARD_CONTENT: 'expandableCardContent',
      EXPANDABLE_CARD_FOOTER: 'expandableCardFooter',
      EXPANDABLE_ITEMS: 'expandableItems',
    }
    
    type ExpandableCardProps = {
      id?: string;
      select?: boolean;
      onSelect?: (id: string) => void;
      children: ReactElement<ExpandableCardContentProps> | ReactElement<ExpandableCardContentProps>[];
    }
    
    const ExpandableCard = ({ id = uuidv4(), select = false, children, onSelect = () => { } }: ExpandableCardProps) => {
      const transitionRef = useRef(null);
      const [selected, setSelected] = useState(select);
      const [expand, setExpand] = useState(false);
      const headerElement = useRef<any>(null);
      const contentElement = useRef<any>(null);
      const expandableFooter = useRef<any>(null);
      const expandableItems = useRef<any>(null);
    
      const handleSelected = () => {
        setSelected(!selected);
      }
    
      useEffect(() => {
        if (selected) {
          onSelect(id);
        }
      }, [id, onSelect, selected])
    
      const handleExpand = () => {
        setExpand(!expand);
      }
    
      Children.forEach(children, (child) => {
        switch (child.props.__TYPE) {
          case types.EXPANDABLE_CARD_HEADER:
            headerElement.current = child;
            break;
          case types.EXPANDABLE_CARD_CONTENT:
            contentElement.current = child;
            break;
          case types.EXPANDABLE_ITEMS:
            expandableItems.current = child;
            break;
          case types.EXPANDABLE_CARD_FOOTER:
            expandableFooter.current = child;
            break;
          default:
            return <div></div>;
        }
      });
    
      return (
        <div className={`expandable-card ${selected ? 'expandable-card-selected' : ''}`}>
          <div className={`expandable-card--content ${expand ? 'expandable-card--content-active' : ''}`}>
            <div className="expandable-card--expand-button">
              <button type="button" onClick={handleExpand}>expand</button>
            </div>
            {headerElement.current &&
              <div className="expandable-card--header">
                {headerElement.current}
              </div>
            }
            {contentElement.current}
            <div className="d-flex align-items-center mt-3">
              <button
                type="button"
                className={`btn expandable-card--button ${selected ? 'expandable-card--button-active' : ''}`}
                onClick={handleSelected}>
                {selected && !}
              </button>
              {expandableFooter.current}
            </div>
          </div>
    
          <CSSTransition
            nodeRef={transitionRef}
            in={expand}
            timeout={500}
            classNames={`expandable-card--drawer`}
            mountOnEnter
            unmountOnExit>
            <div ref={transitionRef} className="expandable-card--drawer">
              {expandableItems.current}
            </div>
          </CSSTransition>
        </div >
      );
    }
    
    type ExpandableCardContentProps = {
      children: ReactNode,
      __TYPE: string;
    }
    
    export const ExpandableCardHeader = ({ children }: ExpandableCardContentProps) => {
      return (
        <>
          {children}
        </>
      );
    }
    
    ExpandableCardHeader.defaultProps = {
      __TYPE: types.EXPANDABLE_CARD_HEADER,
    }
    
    export const ExpandableCardContent = ({ children }: ExpandableCardContentProps) => (
      <>
        {children}
      </>
    );
    
    ExpandableCardContent.defaultProps = {
      __TYPE: types.EXPANDABLE_CARD_CONTENT,
    }
    
    export const ExpandableCardFooter = ({ children }: ExpandableCardContentProps) => (
      <>
        {children}
      </>
    );
    
    ExpandableCardFooter.defaultProps = {
      __TYPE: types.EXPANDABLE_CARD_FOOTER,
    }
    
    export const ExpandableItems = ({ children }: ExpandableCardContentProps) => (
      <>
        {children}
      </>
    );
    
    ExpandableItems.defaultProps = {
      __TYPE: types.EXPANDABLE_ITEMS,
    }
    
    export default ExpandableCard;
    
    

    Please note that this is the complete expandable component with animations in it I'll put up the SCSS code also with this to be complete

    .expandable-card {
      display: flex;
      flex-direction: column;
      box-shadow: 0 0px 25px 0px rgba(0, 0, 0, 0.2);
      width: 100%;
      background-color: #fff;
      border-radius: 14px;
      position: relative;
    
      &--expand-button {
        position: absolute;
        top: 10px;
        right: 15px;
      }
    
      &-selected {
        border-bottom: 15px solid yellow;
        border-radius: 14px;
      }
    
      &--content {
        padding: 18px 15px;
        border-radius: 14px 14px 0 0;
        transition: all 500ms ease-out;
    
        &-active {
          z-index: 1;
          box-shadow: 0 7px 7px 0 rgba(0, 0, 0, 0.2);
        }
      }
    
      &--drawer {
        display: flex;
        flex-direction: column;
        width: 100%;
        max-height: 0;
        background-color: #fff;
        padding: 18px 20px;
        border-radius: 0 0 14px 14px;
        overflow-x: hidden;
        overflow-y: auto;
        transition: all 500ms ease-out;
    
        /* .classes for help dropdown animations */
        &-enter-active {
          max-height: 320px;
          padding: 18px 20px;
        }
        &-enter-done {
          max-height: 320px;
          padding: 18px 20px;
        }
        &-exit-active {
          max-height: 0;
          padding: 0 20px;
        }
        &-exit-done {
          max-height: 0;
          padding: 0 20px;
        }
      }
    
      &--header {
        display: flex;
        align-items: center;
      }
    
      &--button {
        min-width: 43px;
        height: 43px;
        background: transparent;
        border: 2px solid aqua;
        box-sizing: border-box;
        border-radius: 10px;
    
        &:focus {
          box-shadow: none;
        }
    
        &-active {
          background-color: blue;
          border: none;
        }
      }
    }