reactjs - React iç içe Accordion Ebeveyni yüksekliği güncellemiyor


Ana sayfamın mobil sürümünü oluşturmaya çalışıyorum, iç içe geçmiş açılır menüm "Projeler" ilk açıldığında alt projeler bölümünün doğru yüksekliğini göstermeyen bir hataya sahip gibi görünüyor.

Bunu açmak için önce projeler yazısına tıklarsınız, sonra projeleri listeler ve ardından proje kartını açmak için projeye tıklarsınız.

(Güncellendi) Bunun, alt Accordion açıldığında üst Accordion'ın yüksekliğini tekrar güncellememesi nedeniyle olduğunu düşünüyorum.

Bunu yapmanın iyi bir yolunu biliyor musunuz? Yoksa gerekirse bunu mümkün kılacak şekilde bileşenlerimi mi yeniden yapılandırmalıyım? Zorluk, Accordion’ın çocukları kabul etmesi ve ben bunu içinde yeniden kullandığım için oldukça kafa karıştırıcı olması. Potansiyel olarak ebeveyni tetiklemek için bir geri çağırma fonksiyonu kullanabileceğimi biliyorum ama bunu nasıl ele alacağım konusunda tam olarak emin değilim.

AnaSayfa.tsx

import { Accordion } from "@/components/atoms/Accordion"
import { AccordionGroup } from "@/components/atoms/AccordionGroup"
import { AccordionSlideOut } from "@/components/atoms/AccordionSlideOut"
import { Blog } from "@/components/compositions/Blog"
import { Contact } from "@/components/compositions/Contact"
import { Portfolio } from "@/components/compositions/Portfolio"
import { PuyanWei } from "@/components/compositions/PuyanWei"
import { Resumé } from "@/components/compositions/Resumé"
import { Socials } from "@/components/compositions/Socials"
import { Component } from "@/shared/types"

interface HomepageProps extends Component {}

export function Homepage({ className = "", testId = "homepage" }: HomepageProps) {
  return (
    <main className={`grid grid-cols-12 pt-24 ${className}`} data-testid={testId}>
      <section className="col-span-10 col-start-2">
        <AccordionGroup>
          <Accordion title="Puyan Wei">
            <PuyanWei />
          </Accordion>
          <Accordion className="lg:hidden" title="Portfolio">
            <Portfolio />
          </Accordion>
          <AccordionSlideOut className="hidden lg:flex" title="Portfolio">
            <Portfolio />
          </AccordionSlideOut>
          <Accordion title="Resumé">
            <Resumé />
          </Accordion>
          <Accordion title="Contact">
            <Contact />
          </Accordion>
          <Accordion title="Blog">
            <Blog />
          </Accordion>
          <Accordion title="Socials">
            <Socials />
          </Accordion>
        </AccordionGroup>
      </section>
    </main>
  )
}

Portfolio.tsx

import { Accordion } from "@/components/atoms/Accordion"
import { AccordionGroup } from "@/components/atoms/AccordionGroup"
import { ProjectCard } from "@/components/molecules/ProjectCard"
import { projects } from "@/shared/consts"
import { Component } from "@/shared/types"

interface PortfolioProps extends Component {}

export function Portfolio({ className = "", testId = "portfolio" }: PortfolioProps) {
  return (
    <AccordionGroup className={`overflow-hidden ${className}`} testId={testId}>
      {projects.map((project, index) => (
        <Accordion title={project.title} key={`${index}-${project}`} headingSize="h2">
          <ProjectCard project={project} />
        </Accordion>
      ))}
    </AccordionGroup>
  )
}

AccordionGroup.tsx - AccordionGroup'un amacı, aynı anda yalnızca bir çocuk Accordion'ın açık olmasına izin vermektir. Bir Accordion AccordionGroup içinde değilse, bağımsız olarak açılıp kapanabilir.

"use client"

import React, { Children, ReactElement, cloneElement, isValidElement, useState } from "react"
import { AccordionProps } from "@/components/atoms/Accordion"
import { Component } from "@/shared/types"

interface AccordionGroupProps extends Component {
  children: ReactElement<AccordionProps>[]
}

export function AccordionGroup({
  children,
  className = "",
  testId = "accordion-group",
}: AccordionGroupProps) {
  const [activeAccordion, setActiveAccordion] = useState<number | null>(null)

  function handleAccordionToggle(index: number) {
    setActiveAccordion((prevIndex) => (prevIndex === index ? null : index))
  }

  return (
    <div className={className} data-testid={testId}>
      {Children.map(children, (child, index) =>
        isValidElement(child)
          ? cloneElement(child, {
              onClick: () => handleAccordionToggle(index),
              isActive: activeAccordion === index,
              children: child.props.children,
              title: child.props.title,
            })
          : child
      )}
    </div>
  )
}

Akordeon.tsx

"use client"
import { Component } from "@/shared/types"
import React, { MutableRefObject, ReactNode, RefObject, useEffect, useRef, useState } from "react"
import { Heading } from "@/components/atoms/Heading"

export interface AccordionProps extends Component {
  title: string
  children: ReactNode
  isActive?: boolean
  onClick?: () => void
  headingSize?: "h1" | "h2"
}

export function Accordion({
  className = "",
  title,
  children,
  isActive,
  onClick,
  headingSize = "h1",
  testId = "Accordion",
}: AccordionProps) {
  const [isOpen, setIsOpen] = useState(false)
  const [height, setHeight] = useState("0px")
  const contentHeight = useRef(null) as MutableRefObject<HTMLElement | null>

  useEffect(() => {
    if (isActive === undefined) return
    isActive ? setHeight(`${contentHeight.current?.scrollHeight}px`) : setHeight("0px")
  }, [isActive])

  function handleToggle() {
    if (!contentHeight?.current) return
    setIsOpen((prevState) => !prevState)
    setHeight(isOpen ? "0px" : `${contentHeight.current.scrollHeight}px`)
    if (onClick) onClick()
  }
  return (
    <div className={`w-full text-lg font-medium text-left focus:outline-none ${className}`}>
      <button onClick={handleToggle} data-testid={testId}>
        <Heading
          className="flex items-center justify-between"
          color={isActive ? "text-blue-200" : "text-white"}
          level={headingSize}
        >
          {title}
        </Heading>
      </button>
      <div
        className={`overflow-hidden transition-max-height duration-250 ease-in-out`}
        ref={contentHeight as RefObject<HTMLDivElement>}
        style={{ maxHeight: height }}
      >
        <div className="pt-2 pb-4">{children}</div>
      </div>
    </div>
  )
}

ProjeKartı.tsx

import Image from "next/image"
import { Card } from "@/components/atoms/Card"
import { Children, Component, Project } from "@/shared/types"
import { Subheading } from "@/components/atoms/Subheading"
import { Tag } from "@/components/atoms/Tag"
import { Text } from "@/components/atoms/Text"

interface ProjectCardProps extends Component {
  project: Project
}

export function ProjectCard({
  className = "",
  testId = "project-card",
  project,
}: ProjectCardProps) {
  const {
    title,
    description,
    coverImage: { src, alt, height, width },
    tags,
  } = project
  return (
    <Card className={`flex min-h-[300px] ${className}`} data-testid={testId}>
      <div className="w-1/2">
        <CoverImage className="relative w-full h-full mb-4 -mx-6-mt-6">
          <Image
            className="absolute inset-0 object-cover object-center w-full h-full rounded-l-md"
            src={src}
            alt={alt}
            width={parseInt(width)}
            height={parseInt(height)}
            loading="eager"
          />
        </CoverImage>
      </div>
      <div className="w-1/2 p-4 px-8 text-left">
        <Subheading className="text-3xl font-bold" color="text-black">
          {title}
        </Subheading>
        <Tags className="flex flex-wrap pb-2">
          {tags.map((tag, index) => (
            <Tag className="mt-2 mr-2" key={`${index}-${tag}`} text={tag} />
          ))}
        </Tags>
        <Text color="text-black" className="text-sm">
          {description}
        </Text>
      </div>
    </Card>
  )
}

function CoverImage({ children, className }: Children) {
  return <div className={className}>{children}</div>
}
function Tags({ children, className }: Children) {
  return <div className={className}>{children}</div>
}

Her türlü yardım takdir edilir, teşekkürler!

Cevap #1

Biliyor musunuz, burada bu uygulama ile biraz zorlayıcı oluyor, çünkü çocuğundan büyükbaba Accordion'ın yüksekliğini güncellemek istediğinizde, oradan gerçekten hangi büyükbaba Accordion'ı güncellemek istediğinizi bilemezsiniz; ancak propsları büyükbaba Accordion'a aktarır ve ayrıca aradaki bileşene (örneğin Portfolio, yani çocuk Accordion'ın ebeveyni) props gönderirseniz, bu şekilde onları çocuk Accordion'a iletebilir.
Bunu yaparak büyükbaba ve çocuk Accordion'ların bir şekilde iletişim kurmasını sağlayabiliriz.
Belki bu bulabileceğiniz en iyi çözüm değildir, ama ne yazık ki daha iyisini düşünemiyorum.


Özetle: fikir, her bir üst Accordion'a atıfta bulunan yükseklikleri tutmak için en üst seviyede bir durum oluşturmaktır, yani uzunluğu 'manuel' olarak ayarlanan bir dizi, bu biraz çirkin yapıyor, ama bileşenlerinizi dinamik olarak görüntülemek için bir veri dizisi kullanmanız gerekiyorsa, bu bir sorun değildir, bunu daha sonra keşfedeceğiz, ayrıca geçici çözümün sınırlamalarını da göreceğiz.


Geçici çözüm :

Şimdi, soruda yer alan şeyler için işe yarayan en basit ve en doğrudan çözümle devam edeceğiz.

Belirtildiği gibi, önce HomePage bileşeninde durumu oluşturuyoruz:

const [heights, setHeights] = useState(Array(7).fill("0px")); // you have 7 parent Accordions

Diziyi en üst seviyede oluşturduktan sonra, şimdi her Accordion bileşenine durum ayarlayıcı fonksiyonu setHeights, indeks indexx ve eğer bir üst Accordion ise ilgili yükseklik heightParent değerini iletiriz

<AccordionGroup>
  <Accordion title="Puyan Wei" heightParent={heights[0]} setHeights={setHeights} indexx="0">
      <PuyanWei setHeights={setHeights} indexx="0" />
  </Accordion>
  <Accordion title="Portfolio" heightParent={heights[1]} setHeights={setHeights} indexx="1">
      <Portfolio setHeights={setHeights} indexx="1" />
  </Accordion>
  //...
  <Accordion title="Socials" heightParent={heights[6]} setHeights={setHeights} indexx="6">
      <Socials setHeights={setHeights} indexx="6" />
  </Accordion> 
</AccordionGroup>

Not: indexx özelliği üst bileşene ve indexx özelliği ara bileşene (Portfolio) iletilir bu değerler aynı olmalıdır ve ilgili indeksi göstermelidir, bu aslında çözümün anahtarıdır.
İleride çakışmayı önlemek için adı iki 'x' ile 'indexx' olarak verilmiştir.

Then, ara bileşenden aldığınız bu propsları çocuk Accordion'a geçirirsiniz:

export function Portfolio({
  className = "",
  testId = "portfolio",
  indexx,
  setHeight,
}: PortfolioProps) {
  // update props interface
  return (
    <AccordionGroup className={`overflow-hidden ${className}`} testId={testId}>
      {projects.map((project, index) => (
        <Accordion
          title={project.title}
          key={`${index}-${project}`}
          headingSize="h2"
          indexx={indexx}
          setHeight={setHeight}
        >
          <ProjectCard project={project} />
        </Accordion>
      ))}
    </AccordionGroup>
  );
}

Şimdi çocuk Accordion bileşeninizden, iletilen indexx özelliklerinden faydalanarak, HomePage durumundaki ilgili Accordion ebeveyninin yüksekliğini güncelleyebilirsiniz; böylece çocuk yüksekliğini güncellediğimizde ebeveyn yüksekliğini de güncellemiş oluruz.

function handleToggle() {
  if (!contentHeight?.current) return;
  setIsOpen((prevState) => !prevState);
  let newHeight = isOpen ? "0px" : `${contentHeight.current.scrollHeight}px`;
  if (!heightParent) { // there is no need to update the state when it is a parent Accordion
    setHeight(newHeight);
  }
  setHeights((prev) =>
    prev.map((h, index) => (index == indexx ? newHeight : h))
  );
}

Son olarak, bir Accordion için yüksekliği belirttiğinizde, props olarak heightParent alıp almadığını kontrol edebilirsiniz, böylece onun bir üst öğe olduğunu biliriz, bu sayede Accordion bileşeninin heightParent'i kendi durumu olan height yerine maxHeight olarak kullanmasına izin veririz eğer bir üst öğeyse. İşte bir üst Accordion açıldığında height durumunun güncellenmesinin göz ardı edilmesinin nedeni budur, bu nedenle artık maxHeight özelliğinin ayarlanma şeklini değiştirmemiz gerekiyor, şimdi Accordion'un niteliğine bağlı olmalıdır:

style={{ maxHeight: `${heightParent? heightParent : height }` }}

Ebeveyn Akkordiyon'u yapmak istiyorsanız, sadece durumunu height olarak maxHeight kullanın ve kodunuzu olduğu gibi bırakın, böylece daha mantıklı olur

style={{ maxHeight: height }}

Bunu hâlâ yapabilirsiniz, Accordion bileşenine bir useEffect ekleyerek ve bunun yalnızca alınan heightParent props güncellendiğinde ve ayrıca tanımlı olduğunda çalışmasını sağlayarak, kodun yalnızca güncellenmesi gereken bir üst accordion olduğunda height durumunu güncellemesi için çalıştığından emin oluruz:

useEffect(()=>{
 if(heightParent){
   setHeight(heightParent)
 }
},[heightParent])

Dendiği gibi, bu daha mantıklı ve ayrıca en güzel olanı da bu, ama yine de ilkini tercih ediyorum çünkü daha doğrudan ve ayrıca bir ek render tasarrufu sağlıyor.


Verilerle dinamik olarak çalışmak:

Eğer verilerimizi bir dizide saklıyorsak ve buna göre bileşenlerimizi görüntülemek istiyorsak, bunun yerine şunu yaparız:

const data = [...]
const [heights, setHeights] = useState(data.map(_ => "0px")); 
//...
<section className="col-span-10 col-start-2">
 <AccordionGroup>
  {data.map(element, index => {
     <Accordion key={index} title={element.title} heightParent={heights[index]} setHeights={setHeights} indexx={index} >
       {element.for === "portfolio" ? <Portfolio setHeights={setHeights} indexx={index} /> : <PuyanWei setHeights={setHeights} indexx={index} /> }  // just an example
     </Accordion>
  })
  }
 </AccordionGroup>
</section>

Burada fark edebilirsiniz ki, indexx yerine kullanabilmemiz için üst accordiona bir key belirtmemiz gerekiyor ama biliyorsunuz ki key özelliği özeldir ve zaten onunla uğraşmak istemiyoruz, umarım fikri anlarsınız


Sınırlama :

Bu çözümün sadece bir seviye için çalıştığı açık, bu yüzden eğer çocuk Accordion kendine bir çocuk Accordion ederse, o zaman tekrar kafa yorman gerekir, ama muhtemelen ne yaptığını anladıysam, bununla karşılaşmayacaksın, çünkü senin uygulamanla çocuk Accordion öğeleri göstermek için tasarlanmış, ama belki bir gün başka bir çocuk Accordion döndürmesi gerekir, işte bu yüzden önerimi optimal bir çözümden ziyade geçici bir çözüm olarak değerlendiriyorum.


Dediğim gibi, bu en iyi çözüm olmayabilir, ama dürüst olmak gerekirse, özellikle bu uygulama ile, böyle çok seviyeli bir çalışma çözümü olduğuna inanmıyorum, lütfen yanıldığımı kanıtlayın, gönderiyi takip ediyorum.