Search code examples
reactjsnext.jsreact-hooksaccordionshadcnui

Is there any way to control ShadcnUI Accordion "open" and "close" functionality using value from the useState in NextJs?


import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion";


interface chapterProps{
  selectedChapter:number;
  setSelectedChapter:React.Dispatch<React.SetStateAction<number>>;
  chapters:any;
}

export default function Chapters(props: chapterProps) {
  console.log("whats in selected chapter", props?.selectedChapter);
  
  return (
    <>
      <div className="w-full flex flex-col space-y-4">
        {props?.chapters?.map((value: any, index: number) => {
          console.log("whats in chapter map", value);
          return (
            <Accordion
              type="single"
              collapsible
              className="w-full bg-white p-4"
              key={index}
              defaultValue={`item-${props?.selectedChapter}`}
            >
              <AccordionItem value={`item-${index}`} key={index}>

                <AccordionTrigger>
                  <div className="w-full text-left flex flex-col space-y-4">
                    <p className="mx-0 text-[#04445E] font-semibold text-2xl">
                      {value?.chaptername}
                    </p>
                    </>)}
                  </div>
                </AccordionTrigger>

                <AccordionContent key={index}>
                  {value?.chapterContent &&
                    value?.chapterContent?.map((content: any, i: number) => {
                      return (
                          <div className="flex space-y-6 flex-col">
                            <p className="text-2xl text-[#011E29]">
                              {content.name}
                            </p>
                            <div className="flex space-x-4 text-[#011E29]">
                              <p className="">{content.type}</p>
                              <p>{content.time}</p>
                            </div>
                          </div>
                      );
                    })}
                  
                </AccordionContent>

              </AccordionItem>
            </Accordion>
          );
        })}
      </div>
    </>
  );
}

Above is my code where I am trying to control ShadcnUI Accordion "open" and "close" using a state variable called "selectedChapter", I'm able to set the default AccordionItem open (i,e. "item-0") because default value of my state variable is also 0 but when state variable value changes (example: selectedChapter = 2) after the initial rendering, the AccordionItem with value "item-2" doesn't open

Note: here I'm passing "selectedChapter" state as props from parent component and most of the time it is controlled by parent component itself, Yes i can use "setSelectedChapter" from this component but my concern is with state change from parent component.

Thanks for helping:)


Solution

  • In RadixUI, The headless UI behind Shadcn/ui, You can make the accordion conrolled by the value and onValueChange props.

    import {
      Accordion,
      AccordionContent,
      AccordionItem,
      AccordionTrigger,
    } from "@/components/ui/accordion";
    import { useState } from "react";
    
    export default function Page() {
      const [value, setValue] = useState("1");
    
      return (
        <Accordion type="single" value={value} onValueChange={setValue}>
          {[1, 2, 3, 4, 5].map((i) => (
            <AccordionItem key={i} value={i.toString()}>
              <AccordionTrigger>Trigger {i}</AccordionTrigger>
              <AccordionContent>Content {i}</AccordionContent>
            </AccordionItem>
          ))}
        </Accordion>
      );
    }
    

    The state type depends on Accordion's type prop. When type="single", the state is of type string. When using type="multiple" the state will be string[].

    In your component, you need two things:

    Move the <Accordion> wrapper arround the loop, so it can controll all accordions. currently in your implementation every item has its own state. and change defaultValue to value and onValueChange

    export default function Chapters(props: chapterProps) {
      const [collabsed, setCollabsed] = useState(props.selectedChapter);
    
      return (
        <div className="w-full flex flex-col space-y-4">
          <Accordion
            type="single"
            collapsible
            className="w-full bg-white p-4"
            key={index}
            value={`item-${collabsed}`}
            onValueChange={(value) => setCollabsed(value)}
          >
            {props?.chapters?.map((value: any, index: number) => {
              return (
                <AccordionItem value={`item-${index}`} key={index}>
                  <AccordionTrigger>
                    <div className="w-full text-left flex flex-col space-y-4">
                      <p className="mx-0 text-[#04445E] font-semibold text-2xl">
                        {value?.chaptername}
                      </p>
                    </div>
                  </AccordionTrigger>
    
                  <AccordionContent key={index}>
                    {value?.chapterContent &&
                      value?.chapterContent?.map((content: any, i: number) => {
                        return (
                          <div className="flex space-y-6 flex-col">
                            <p className="text-2xl text-[#011E29]">
                              {content.name}
                            </p>
                            <div className="flex space-x-4 text-[#011E29]">
                              <p className="">{content.type}</p>
                              <p>{content.time}</p>
                            </div>
                          </div>
                        );
                      })}
                  </AccordionContent>
                </AccordionItem>
              );
            })}
          </Accordion>
        </div>
      );
    }