Published on

How to Manage Shadcn Accordion Open-close with useState in Next.js

Authors
  • Name
    Ripal & Zalak
    Twitter

How to Control Shadcn UI Accordion with React State in Next.js

Shadcn UI Accordion is a powerful component for creating collapsible sections. In this guide, we’ll explore how to control its open and close functionality using React’s useState, making it responsive to changes in parent-controlled state.

Prerequisites

  1. Basic knowledge of React and Next.js.
  2. Familiarity with Shadcn UI library.
  3. Installed Shadcn UI components.

The Challenge

When using Shadcn UI Accordion, you might want to control which section is open based on a state variable from a parent component. This guide demonstrates how to achieve this.


Solution: Controlled Accordion

Here’s how you can implement a controlled Shadcn UI Accordion:

Code Implementation

import React, { useState, useEffect } from 'react'
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from '@/components/ui/accordion'

interface ChapterProps {
  selectedChapter: number
  setSelectedChapter: React.Dispatch<React.SetStateAction<number>>
  chapters: Array<{
    chaptername: string
    chapterContent: Array<{ name: string; type: string; time: string }>
  }>
}

const Chapters = ({ selectedChapter, setSelectedChapter, chapters }: ChapterProps) => {
  const [openChapter, setOpenChapter] = useState<string>(`item-${selectedChapter}`)

  // Sync local state with selectedChapter prop whenever it changes
  useEffect(() => {
    setOpenChapter(`item-${selectedChapter}`)
  }, [selectedChapter])

  return (
    <div className="flex w-full flex-col space-y-4">
      <Accordion
        type="single"
        collapsible
        value={openChapter}
        onValueChange={(value) => {
          if (value) {
            const chapterIndex = parseInt(value.replace('item-', ''), 10)
            setSelectedChapter(chapterIndex)
          }
          setOpenChapter(value || '')
        }}
        className="w-full bg-white p-4"
      >
        {chapters.map((chapter, index) => (
          <AccordionItem value={`item-${index}`} key={index}>
            <AccordionTrigger>
              <div className="flex w-full flex-col space-y-4 text-left">
                <p className="mx-0 text-2xl font-semibold text-[#04445E]">{chapter.chaptername}</p>
              </div>
            </AccordionTrigger>
            <AccordionContent>
              {chapter.chapterContent.map((content, i) => (
                <div key={i} className="flex flex-col space-y-6">
                  <p className="text-2xl text-[#011E29]">{content.name}</p>
                  <div className="flex space-x-4 text-[#011E29]">
                    <p>{content.type}</p>
                    <p>{content.time}</p>
                  </div>
                </div>
              ))}
            </AccordionContent>
          </AccordionItem>
        ))}
      </Accordion>
    </div>
  )
}

export default Chapters

Key Features

  1. Controlled State:

    • The Accordion component uses value to determine which item is open.
    • Updates the openChapter state and parent selectedChapter when an item is clicked.
  2. Dynamic Synchronization:

    • Automatically updates openChapter when selectedChapter changes in the parent.
  3. Reusable Structure:

    • The Accordion dynamically maps through chapters, making it adaptable for various datasets.

Common Issues and Fixes

  1. Accordion Not Updating:

    • Ensure the value prop matches the format of useState.
  2. React Key Warnings:

    • Use unique key values in lists, such as index or unique IDs.
  3. State Sync Issues:

    • Use useEffect to synchronize local and parent states.

Benefits

  • Improved Usability: The Accordion responds to external state changes.
  • Scalability: Supports dynamic content and API-driven data.
  • Ease of Use: Simplifies state management with a single source of truth.

FAQs

How do I set a default open item?

Set the value prop to the corresponding AccordionItem value:

<Accordion value="item-0">...</Accordion>

Can I use this with API data?

Yes, fetch data in a useEffect hook and pass it as chapters:

useEffect(() => {
  fetch('/api/chapters')
    .then((res) => res.json())
    .then((data) => setChapters(data))
}, [])

What is the role of onValueChange?

It updates the open Accordion item and synchronizes with the parent component.