Ant Design Collapse – zamykanie panelu przyciskiem w 2026
Dlaczego onChange w Collapse nie wystarcza
Wrzucasz przycisk do nagłówka Panel, klikasz w niego, panel się rozwija albo zwija, a logika biznesowa leży. Brzmi znajomo? Problem nie leży w samym przycisku, tylko w sposobie, w jaki Ant Design w wersji 5 obsługuje stan kontrolowany. Komponent Collapse traktuje activeKey jako jedyne źródło prawdy o tym, które panele są otwarte. Zmienna ta może przyjąć postać łańcucha znaków dla trybu pojedynczego albo tablicy łańcuchów dla trybu wielokrotnego wyboru.

- Dlaczego onChange w Collapse nie wystarcza
- Wzorzec lift state up z activeKey w rodzicu
- Alternatywa: zamykanie przez ref i panelKey API
- Najczęstsze pułapki przy kontrolowanym stanie paneli
- Programowe sterowanie wieloma panelami i edge case'y
Kiedy korzystasz z wariantu kontrolowanego, właściwość onChange reaguje wyłącznie na akcje wykonane na nagłówku panelu, nie na kliknięcia elementów umieszczonych w środku. Próba podpięcia własnego przycisku pod nagłówek z wykorzystaniem wywołania zwrotnego zwykle kończy się niespodziewanym zachowaniem. Panel zamyka się po akcji przycisku, lecz rodzic nie otrzymuje żadnej informacji o tej zmianie, bo sam ją zainicjowałeś z jego poziomu.
Najczęstsze błędy widać na pierwszy rzut oka w publicznych repozytoriach. Programiści wywołują funkcję zamiast ją przekazywać jako referencję, przez co onChange dostaje natychmiastowy rezultat działania funkcji zamiast jej samej. Inni ustawiają stan lokalny komponentu na podstawie props.isOpened bez efektu ubocznego, więc aktualizacja rodzica nigdy nie dociera z powrotem. Jeszcze inni mieszają tryb kontrolowany z niekontrolowanym, przekazując jednocześnie activeKey i defaultActiveKey.
- Wywołanie
onChange={mojaFunkcja()} zamiast onChange={mojaFunkcja}.- Próba synchronizacji stanu przez
useEffect z props.isOpened.- Użycie
defaultActiveKey przy jednoczesnym zarządzaniu activeKey.Zanim napiszesz kolejną linijkę kodu, zatrzymaj się na chwilę i zadaj sobie pytanie, kto w tej architekturze jest właścicielem stanu panelu. Jeśli odpowiedź brzmi „komponent Collapse", to właśnie tam szukasz przyczyny niepowodzenia. Rozwiązanie wymaga świadomego przeniesienia tej odpowiedzialności wyżej.
Wzorzec lift state up z activeKey w rodzicu
Fundamentem kontrolowanego komponentu w React jest tak zwany wzorzec lift state up. Stan, który wcześniej żył wewnątrz Collapse, trafia do komponentu nadrzędnego. Rodzic staje się jedynym miejscem decyzyjnym o tym, które panele są otwarte. Komponent potomny otrzymuje jedynie aktualną wartość activeKey oraz funkcję zwrotną onToggle, która pozwala mu poinformować rodzica o intencji zmiany.
Mechanizm jest prosty i niezawodny. Kliknięcie w nagłówek wywołuje funkcję przekazaną przez rodzica. Rodzic aktualizuje swój własny stan za pomocą useState, a React ponownie renderuje drzewo komponentów z nową wartością activeKey. Dzięki temu logika pozostaje spójna niezależnie od tego, ile przycisków dodasz do wnętrza panelu. Każdy z nich może wywołać tę samą funkcję onToggle z odpowiednim identyfikatorem.
Praktyczna implementacja wygląda następująco dla komponentu funkcyjnego:
function CustomCollapse({ panels }) {
const [activeKey, setActiveKey] = useState([]);
const handleToggle = (key) => {
setActiveKey((prev) =>
prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]
);
};
return (
<Collapse activeKey={activeKey} onChange={(keys) => setActiveKey(keys)}>
{panels.map(({ key, header, content, button }) => (
<Panel
key={key}
header={header}
extra={button(handleToggle, key, activeKey.includes(key))}
>
{content}
<Panel>
))}
</Collapse>
);
}
Wariant klasowy zachowuje identyczną logikę, lecz przechowuje stan w this.state i wykorzystuje this.setState do aktualizacji. Pamiętaj o bindowaniu metod w konstruktorze lub użyciu strzałkowych funkcji klasowych, bo bez tego this wskaże na undefined w trakcie wywołania zwrotnego.
Dlaczego warto wybrać tę ścieżkę zamiast innych? Lift state up daje jedno źródło prawdy, które łatwo testować za pomocą React Testing Library. Możesz zasymulować kliknięcie przycisku i sprawdzić, czy activeKey w rodzicu zmieniło się prawidłowo. Komponent staje się przewidywalny, a debugowanie ogranicza się do prześledzenia jednego łańcucha wywołań.
Wariant z TypeScript i testami
Silne typowanie w React rozwiązuje całą klasę problemów związanych z przekazywaniem nieprawidłowych identyfikatorów paneli. Definicja interfejsu PanelData wymusza podanie key jako łańcucha znaków, a funkcja handleToggle akceptuje wyłącznie wartości zgodne z tym typem. Kompilator sam złapie próbę przekazania liczby lub obiektu.
Test w React Testing Library sprowadza się do trzech kroków. Najpierw renderujesz komponent z początkowym stanem. Następnie znajdujesz przycisk za pomocą screen.getByRole i wywołujesz na nim userEvent.click. Na końcu sprawdzasz asercją, czy odpowiedni element otrzymał klasę odpowiedzialną za stan rozwinięcia. Cały test mieści się w dwudziestu linijkach kodu i nie wymaga żadnych mocków.
Alternatywa: zamykanie przez ref i panelKey API
Czasami przenoszenie stanu wyżej nie ma sensu architektonicznego. Komponent Collapse w antd v5 nie udostępnia imperatywnego API tak jak Modal czy Drawer, ale możesz obejść to ograniczenie za pomocą referencji React. Wzorzec polega na stworzeniu obiektu useRef, który przechowuje kopię activeKey aktualizowaną przy każdym renderze.
Mechanizm jest sprytny. Wewnątrz useEffect bez zależności zapisujesz do referencji aktualną wartość activeKey. Funkcja zamykająca panel pobiera tę wartość z ref.current, oblicza nową tablicę i wywołuje setActiveKey. Dzięki temu przycisk wewnątrz panelu działa tak, jakby kontrolował stan nadrzędny, choć w praktyce jedynie mutuje referencję.
function CollapseWithRef() {
const [activeKey, setActiveKey] = useState([]);
const activeKeyRef = useRef(activeKey);
useEffect(() => {
activeKeyRef.current = activeKey;
});
const closePanel = (key) => {
setActiveKey(activeKeyRef.current.filter((k) => k !== key));
};
return (
<Collapse activeKey={activeKey} onChange={setActiveKey}>
<Panel header="Sekcja" key="1" extra={
<Button onClick={() => closePanel('1')}>Zamknij</Button>
}>
Treść panelu
</Panel>
</Collapse>
);
}
Kiedy wybrać to podejście zamiast lift state up? Sprawdza się w sytuacjach, gdy Collapse jest głęboko zagnieżdżony w drzewie komponentów i przekazywanie onToggle przez kilka poziomów propsów tworzy tzw. prop drilling. W małych formularzach, gdzie panel nie komunikuje się z resztą aplikacji, referencja wystarczy. W rozbudowanych dashboardach SaaS lepiej postawić na context albo globalny store.
Porównanie obu podejść
| Kryterium | Lift state up | Ref + panelKey |
|---|---|---|
| Czytelność kodu | Wysoka | Średnia |
| Testowalność | Łatwa | Wymaga mocków referencji |
| Prop drilling | Możliwy | Brak |
| Skalowalność | Dobra | Ograniczona |
| Debugowanie | Jeden przepływ | Dwa źródła stanu |
Najczęstsze pułapki przy kontrolowanym stanie paneli
Praca z activeKey jako tablicą to pierwsza poważna pułapka. Wielu deweloperów zapomina, że w trybie accordion (pojedynczym) wartość jest łańcuchem, a w trybie domyślnym tablicą. Próba ustawienia activeKey na łańcuch przy wielokrotnym trybie skutkuje cichym błędem, bo React nie zgłosi wyjątku, lecz po prostu nie rozwinie żadnego panelu.
Mieszanie trybu kontrolowanego z niekontrolowanym to druga klasyka. Jeśli przekażesz jednocześnie activeKey i defaultActiveKey, antd v5 zignoruje drugą wartość, ale komunikat ostrzegawczy pojawi się w konsoli. W produkcyjnych buildach webpack wycina ostrzeżenia, więc błąd pozostaje niewidoczny do momentu, aż użytkownik zgłosi dziwne zachowanie panelu.
- Czy
activeKey jest tego samego typu (string vs array) co oczekuje tryb Collapse?- Czy każda aktualizacja stanu w rodzicu wywołuje ponowny render?
- Czy nie używasz
useEffect do mirrorowania propsów w stan lokalny?- Czy funkcja przekazana do
onChange nie jest wywoływana natychmiast?Trzecia pułapka dotyczy dynamicznie generowanych kluczy paneli. Jeśli tworzysz panele z tablicy obiektów i każdemu nadajesz klucz na podstawie indeksu (key={index}), to po usunięciu jednego elementu z tablicy React pomyli identyfikatory. Panel, który powinien się zamknąć, pozostanie otwarty, bo jego klucz przesunął się o jedną pozycję. Rozwiązanie jest banalne: używaj stabilnych identyfikatorów biznesowych, a nie indeksów.
Czwarta kwestia to wydajność przy dużej liczbie paneli. Właściwość destroyInactivePanel domyślnie ma wartość false, więc treść każdego zamkniętego panelu pozostaje w drzewie DOM. Przy kilkudziesięciu panelach z ciężkimi komponentami (wykresy, edytory) zauważysz spadek płynności. Ustawienie destroyInactivePanel na true usuwa zawartość z DOM, ale kosztem ponownego montowania przy każdym otwarciu. Wybór zależy od priorytetu: pamięć RAM czy czas renderowania.
Programowe sterowanie wieloma panelami i edge case'y
Zamknięcie wszystkich paneli jednym przyciskiem spoza Collapse wymaga znajomości aktualnej wartości activeKey. Rodzic ustawia stan na pustą tablicę (setActiveKey([])) i przekazuje tę akcję jako funkcję zwrotną do komponentu sterującego. Animacja zamknięcia jest obsługiwana automatycznie przez antd, więc nie musisz ręcznie zarządzać transition.
Zamknięcie panelu po zatwierdzeniu formularza to typowy scenariusz w panelach ustawień. Formularz znajduje się wewnątrz Panel, po onFinish wywołuje props.onClosePanel z własnym kluczem, rodzic aktualizuje activeKey, panel znika. Cała logika mieści się w trzech linijkach kodu i nie wymaga żadnych dodatkowych bibliotek do zarządzania formularzami.
Animacja zamknięcia może zostać rozbudowana za pomocą motion, propa dostępnego w antd v5. Domyślnie animacja trwa 300 milisekund z krzywą cubic-bezier(0.215, 0.61, 0.355, 1). Możesz ją skrócić do 150 milisekund dla szybszych interfejsów albo wydłużyć do 500 milisekund dla bardziej eleganckich przejść. Pamiętaj, że zbyt szybka animacja sprawia wrażenie pośpiechu, a zbyt wolna irytuje użytkowników.
Ćwiczenie dla Ciebie
Stwórz komponent Collapse z trzema panelami. Pierwszy zawiera przycisk „Zamknij wszystko", drugi przycisk „Zamknij ten panel", a trzeci wyświetla liczbę aktualnie otwartych paneli. Cała logika ma żyć w rodzicu, żeby żaden z paneli nie zarządzał własnym stanem.
Co sprawdzić w kodzie
Przejdź do dowolnego repozytorium z komponentem Collapse i wyszukaj frazę „useEffect". Jeśli znajdziesz efekt synchronizujący stan lokalny z propsami, to prawdopodobnie masz do czynienia z antywzorcem. Jedynym uzasadnionym użyciem useEffect przy kontrolowanym Collapse jest side effect po faktycznej zmianie stanu, na przykład zapis do analityki.
Kontrolowany stan paneli w Ant Design v5 wymaga świadomej decyzji architektonicznej. Lift state up pozostaje najczytelniejszym rozwiązaniem dla większości scenariuszy, szczególnie gdy panel wchodzi w interakcje z resztą formularza. Referencja React sprawdza się w izolowanych przypadkach, gdzie przekazywanie propsów przez wiele poziomów byłoby uciążliwe. Context API wchodzi do gry, gdy w aplikacji istnieje więcej niż trzy poziomy zagnieżdżenia lub gdy stan paneli musi być dostępny w wielu miejscach jednocześnie.
Kluczowe liczby do zapamiętania: domyślna animacja trwa 300 milisekund, destroyInactivePanel domyślnie ma wartość false, a próg użycia context zamiast propsów to zazwyczaj trzy poziomy zagnieżdżenia. Świadome stosowanie tych wartości odróżnia kod produkcyjny od prototypu.
Zanim wkleisz kolejną bibliotekę do zarządzania stanem, spróbuj rozwiązać problem natywnymi mechanizmami React. useState, useRef i context wystarczą w 90% przypadków, w których programiści sięgają po Reduksa lub Zustanda. Ant Design Collapse nie jest tu wyjątkiem.
Źródła danych i dokumentacja: Oficjalna dokumentacja Ant Design v5 dla komponentu Collapse (ant.design/components/collapse), dokumentacja React dotycząca kontrolowanych komponentów (react.dev/reference/react-dom/components/common), RFC dotyczący wzorca lift state up w React (github.com/reactjs/rfcs), repozytorium React Testing Library (testing-library.com). Dane o animacjach i czasach trwania transition pochodzą z wewnętrznych implementacji biblioteki rc-motion używanej przez antd v5 (github.com/react-component/motion).