1

I have an Accordion component which his children can change his height dynamically (by API response), I try this code but not working because the height changes only if I close and re-open the accordion. The useEffect not triggering when children DOM change. Can anyone help me? Thanks

export const VerticalAccordion = (props) => {
    const accordionContainerRef = useRef<HTMLDivElement>(null);

    const [contentHeight, setContentHeight] = useState<number | undefined>(0);
    const [animationClass, setAnimationClass] = useState<'animated'>();
    const [overflow, setOverflow] = useState<'visible' | 'hidden'>('visible');

    const [isOpen, setIsOpen] = useState<boolean>(true);

    const {title, children} = props;

    useEffect(() =>{
        if(accordionContainerRef.current){
            const height = isOpen ? accordionContainerRef.current.scrollHeight: 0;
            setContentHeight(height);
            if(isOpen){
                // delay need for animation
                setTimeout(() => setOverflow('visible'),700);
                return;
            }
            return setOverflow('hidden')
        }
    }, [isOpen, accordionContainerRef, children]);

    const onAccordionClick = () => {
        setAnimationClass('animated');
        setIsOpen(prevState => !prevState)
    };

    return (
        <div className={'accordion'}>
            <div className={`header`}>
              
                <div className={`header-title`}>{title}</div>
                <MyIcon onClick={() => onAccordionClick()}
                        customClass={`header-arrow`}
                        path={menuDown}
                        size={20}/>
            </div>
            <div ref={accordionContainerRef}
                 style={{ height: `${contentHeight}px`, overflow}}
                 className={`data` + (animationClass ? ` data--${animationClass}` : '')}>
                {children}
            </div>
        </div>
    )
}
Humanbeings
  • 89
  • 2
  • 8

2 Answers2

0

Even though the working around what I have found is not a robust one, but it is working completely fine for me. If someone stumbles upon this same issue might find this useful.

const [activeState, setActiveState] = useState("");
const [activeHeight, setActiveHeight] = useState("0px");
const contentRef = useRef(null)

const toogleActive = () => {
    setActiveState(activeState === "" ? "active" : "");
    setActiveHeight(activeState === "active" ? "0px" :`${contentRef.current.scrollHeight + 100}px`)
    }


return (
    <div className={styles.accordion_section}>
      <button className={styles.accordion} onClick={toogleActive}>
        <p className={styles.accordion_title}>{title}</p>
      </button>

      <div ref={contentRef}
        style={{ maxHeight: `${activeHeight}` }}
        className={styles.accordion_content}>
        <div>
          {content}
          </div>
      </div>
    </div>
  )

I have hardcoded some extra space so that while the dynamic response is accepted the Child DOM is shown. In the CSS module file, I have kept the overflow as auto, earlier it was hidden.

  .accordion_content {
    background-color: white;
    overflow: auto;
    max-height: max-content;
  }

As a result, the Child DOM is appearing dynamically and the user can scroll inside the Accordion if the Child DOM needs larger space.

If you want to use state hooks it is better to use context, so that the hook will be shared between the child and parent. This link might give you an idea of how does context works.

Sumit Paul
  • 21
  • 7
0

Here is how I am solving a similar problem. I needed an accordion that

  1. Could be opened/closed depending on external variable
  2. Could be opened/closed by clicking on the accordion title
  3. Would resize when the contents of the accordion changes height

Here's the accordion in its parent component. Also, the useState hook for opening/closing the accordion from outside the accordion (as necessary).

const [isOpen, setOpen] = useState(false)

// some code...

<Accordion title={"Settings"} setOpen={setOpen} open={isOpen}>
 {children}
</Accordion>

Here's the accordion component. I pass the setOpen hook from the parent to the accordion. I use useEffect to update the accordion's properties when either open or the children change.

import React, { useEffect, useState, useRef } from "react"
import styled from "styled-components"

const AccordionTitle = styled.span`
  cursor: pointer;
`

const AccordionContent = styled.div`
  height: ${({ height }) => height}px;
  opacity: ${({ height }) => (height > 0 ? 1 : 0)};
  overflow: hidden;
  transition: 0.5s;
`

export const Accordion = ({ title, setOpen, open, children }) => {
  const content = useRef(null)
  const [height, setHeight] = useState(0)
  const [direction, setDirection] = useState("right")

  useEffect(() => {
    if (open) {
      setHeight(content.current.scrollHeight)
      setDirection("down")
    } else {
      setHeight(0)
      setDirection("right")
    }
  }, [open, children])

  return (
    <>
      <h3>
        <AccordionTitle onClick={(e) => setOpen((prev) => !prev)}>
          {title}
          <i className={`arrow-accordion ${direction}`}></i>
        </AccordionTitle>
      </h3>
      <AccordionContent height={height} ref={content}>
        {children}
      </AccordionContent>
    </>
  )
}
Ben Upham
  • 63
  • 1
  • 6