TanStack, czyli logika bez wyglądu
TanStack to zestaw bibliotek zbudowanych wokół jednej zasady: dostajesz logikę, a wygląd piszesz sam. Tabela nie narzuca stylów, formularz nie renderuje pól, router nie ma opinii o tym, jak wygląda strona.
Ta filozofia rozwiązuje problem, który przy gotowych komponentach pojawia się prędzej czy później. Biblioteka z własnym wyglądem jest wygodna do momentu, w którym projekt wymaga czegoś, czego autor nie przewidział, a wtedy zaczyna się walka ze stylami.
Zestaw liczy dziś osiemnaście publicznych bibliotek, wypisanych na stronie projektu w pięciu grupach, i to jest pierwsza rzecz, którą trzeba powiedzieć wprost: różnią się dojrzałością drastycznie. Traktowanie tej nazwy jako jednej rzeczy prowadzi do wstawienia do produkcji czegoś, co jest we wczesnej fazie rozwoju.
Co jest stabilne, a co nie
Podział na połowę 2026 roku wygląda tak i warto go znać przed podjęciem decyzji.
Stabilne i gotowe do produkcji są: warstwa zarządzania danymi z serwera, router, tabela, formularze oraz wirtualizacja długich list. To są projekty z wieloletnią historią, ogromną liczbą wdrożeń i przewidywalnym cyklem wydań.
Framework pełnego stosu osiągnął etap kandydata do wydania pierwszej wersji. Interfejs jest uznany za stabilny, funkcje za kompletne, a produkty na nim oparte już działają, natomiast oznaczenie „przed wersją pierwszą" nadal obowiązuje.
Warstwa danych działająca lokalnie oraz narzędzia do pracy z modelami językowymi są w fazie beta. Magazyn stanu jest w fazie alfa.
Resztę listy dopisano w ostatnich miesiącach i tam etykiety są jeszcze wcześniejsze. Biblioteka do opóźniania i ograniczania częstotliwości wywołań jest w fazie beta, a skróty klawiszowe, model dokumentu dla treści oraz podświetlanie składni w fazie alfa. Cztery pozycje z listy to w ogóle nie biblioteki do aplikacji, tylko narzędzia okołoprojektowe: panel diagnostyczny, wspólna konfiguracja publikowanych pakietów, generator projektów i mechanizm dołączania instrukcji dla agentów do paczek npm.
Wniosek praktyczny jest prosty. Pierwsza grupa nadaje się wszędzie. Framework można rozważyć przy nowym projekcie, o ile akceptujesz, że wersja pierwsza jeszcze nie padła. Do reszty warto podejść jak do oprogramowania wczesnego, czyli używać w projektach pobocznych, a nie w systemie, który ma działać przez lata.
Warstwa danych z serwera
To jest najstarszy i najczęściej używany element zestawu, i zarazem ten, który zmienił sposób pisania aplikacji najbardziej.
Rozwiązuje problem, którego przez lata nikt nie nazywał wprost: dane pochodzące z serwera to co innego niż stan aplikacji. Wymagają pobierania, buforowania, odświeżania, ponawiania przy błędzie sieci i unieważniania po zmianie.
const { data, isPending, error } = useQuery({
queryKey: ['artykuly', kategoria],
queryFn: () => pobierzArtykuly(kategoria),
staleTime: 5 * 60 * 1000,
})Klucz zapytania jest tu pojęciem centralnym i najczęściej używanym nieprawidłowo. Określa tożsamość danych, więc musi zawierać wszystko, co wpływa na wynik. Pominięcie kategorii w powyższym przykładzie sprawia, że po zmianie kategorii widzisz stare dane, bo dla biblioteki to wciąż to samo zapytanie.
Przy większym projekcie warto zebrać klucze w jednym miejscu, zamiast wpisywać łańcuchy znaków w kilkunastu komponentach.
export const klucze = {
artykuly: {
wszystkie: ['artykuly'] as const,
lista: (kategoria: string) => ['artykuly', 'lista', kategoria] as const,
jeden: (id: string) => ['artykuly', 'jeden', id] as const
}
}Zysk jest podwójny. Literówka w kluczu przestaje być możliwa, bo kompilator sprawdza wywołanie funkcji, a unieważnianie całej gałęzi sprowadza się do podania wspólnego początku.
Drugim pojęciem wartym zrozumienia jest czas świeżości. Domyślnie dane są uznawane za nieaktualne natychmiast, więc biblioteka odświeża je przy powrocie do karty i przy ponownym zamontowaniu komponentu. To bywa pożądane i bywa nadmiarowe, a ustawienie go świadomie zmniejsza ruch do serwera bardziej niż jakakolwiek inna zmiana.
Trzecim jest unieważnianie po zapisie. Po wysłaniu zmiany trzeba powiedzieć bibliotece, które dane przestały być aktualne, inaczej użytkownik widzi stan sprzed swojej własnej edycji.
const klient = useQueryClient()
const zapisz = useMutation({
mutationFn: zapiszArtykul,
onSuccess: () => {
klient.invalidateQueries({ queryKey: ['artykuly'] })
}
})Podanie samego początku klucza unieważnia wszystkie zapytania, które się od niego zaczynają, więc jedno wywołanie obejmuje listę we wszystkich kategoriach naraz. To jest zachowanie warte zapamiętania, bo pozwala budować klucze hierarchicznie i unieważniać całe gałęzie jednym wywołaniem.
Router i framework
Router jest drugim projektem, który warto znać, głównie ze względu na jedną cechę.
Trasy są w pełni typowane, łącznie z parametrami ścieżki i parametrami zapytania. To znaczy, że odnośnik do nieistniejącej trasy albo brakujący parametr są błędem kompilacji, a nie pustą stroną odkrytą przez użytkownika.
Do tego dochodzi wczytywanie danych na poziomie trasy, powiązane z warstwą zarządzania danymi. Dane zaczynają się pobierać w momencie rozpoczęcia nawigacji, a nie po wyrenderowaniu komponentu, co usuwa charakterystyczne migotanie stanu ładowania.
Framework pełnego stosu stoi na tym routerze i dokłada renderowanie po stronie serwera oraz funkcje serwerowe. Różnica wobec Next.js sprowadza się do podejścia: tam punktem wyjścia jest serwer, a interaktywność dokłada się wyspami, tutaj punktem wyjścia jest aplikacja kliencka, do której dokłada się renderowanie serwerowe.
Ta różnica ma znaczenie przy wyborze. Aplikacja o dużej interaktywności, panel albo narzędzie za logowaniem pasuje lepiej do drugiego podejścia. Strona z przewagą treści, która ma się indeksować, pasuje lepiej do pierwszego.
Tabela i formularze
Tabela jest przykładem, na którym najlepiej widać sens podejścia bez własnego wyglądu.
Dostajesz sortowanie, filtrowanie, stronicowanie, grupowanie, zmianę szerokości kolumn i zaznaczanie wierszy. Nie dostajesz ani jednego znacznika HTML, więc renderujesz to sam, dokładnie tak, jak wygląda reszta Twojego interfejsu.
Przy tabeli z danymi biznesowymi to jest właściwe rozwiązanie, bo wymagania są zawsze nietypowe: kolumna z akcjami, wiersz rozwijany, komórka z wykresem. Gotowa tabela obsługuje trzy z tych rzeczy i przy czwartej zaczynasz walczyć.
Formularze idą tą samą drogą i rozwiązują problem, który przy większych formularzach jest realny: przeliczanie całego formularza przy każdym naciśnięciu klawisza. Biblioteka śledzi pola osobno, więc zmiana w jednym nie renderuje pozostałych trzydziestu.
Warto natomiast wiedzieć, że podejście bez własnego wyglądu ma cenę. Pierwsza tabela zajmuje dzień zamiast godziny, bo piszesz wszystko sam. Zwrot pojawia się przy trzeciej, gdy okazuje się, że każda wygląda inaczej i żadna nie wymagała obchodzenia gotowych stylów.
Wirtualizacja długich list
To najmniej znany element zestawu, a rozwiązuje problem, który przy dużych zbiorach danych jest nie do obejścia inaczej.
Przeglądarka renderuje każdy wiersz, który wstawisz do dokumentu. Przy pięciuset wierszach nikt tego nie zauważy, przy pięćdziesięciu tysiącach karta przestaje odpowiadać, a samo przewijanie zaczyna gubić klatki. Wirtualizacja polega na tym, że renderujesz wyłącznie te elementy, które faktycznie mieszczą się w widocznym obszarze, plus niewielki zapas nad i pod nim.
Biblioteka liczy pozycje elementów i mówi Ci, które z nich narysować oraz gdzie je umieścić. Znaczników znów nie dostajesz, więc wirtualizujesz dowolną strukturę: listę, siatkę kafelków, tabelę albo oś czasu.
Dwie rzeczy warto wiedzieć przed wdrożeniem. Elementy o stałej wysokości są prostym przypadkiem, bo pozycję każdego można policzyć mnożeniem. Elementy o zmiennej wysokości wymagają mierzenia po wyrenderowaniu, co biblioteka obsługuje, ale kosztem odrobiny skomplikowania i możliwego drgnięcia paska przewijania przy szybkim przeskakiwaniu.
Druga rzecz to wpływ na dostępność i wyszukiwanie w przeglądarce. Elementy poza widokiem nie istnieją w dokumencie, więc skrót wyszukiwania ich nie znajdzie, a czytnik ekranu nie odczyta. Przy liście wyników to zwykle akceptowalne, przy długim dokumencie tekstowym już nie i lepiej wtedy sięgnąć po stronicowanie.
Zapis danych i aktualizacja optymistyczna
Odczyt to połowa pracy z serwerem, a druga połowa sprawia więcej problemów, bo dotyczy stanu, który może się nie udać.
Podstawowy schemat jest prosty: wysyłasz zmianę, czekasz na odpowiedź, unieważniasz dane, które ta zmiana dotknęła. Interfejs pokazuje wtedy stan ładowania przez czas przelotu żądania, co przy szybkim łączu jest niewidoczne, a przy wolnym irytujące.
Aktualizacja optymistyczna odwraca kolejność. Zmieniasz dane w buforze od razu, wysyłasz żądanie w tle, a przy błędzie cofasz zmianę do stanu sprzed. Użytkownik widzi natychmiastową reakcję, co przy przełącznikach, polubieniach czy zaznaczaniu zadań jako zrobione jest różnicą między interfejsem żywym a ociężałym.
Cena jest taka, że musisz umieć wrócić do poprzedniego stanu. Dlatego przed zmianą zapisujesz poprzednią wartość, a w obsłudze błędu ją przywracasz.
const przelacz = useMutation({
mutationFn: przelaczZrobione,
onMutate: async (id) => {
await klient.cancelQueries({ queryKey: ['zadania'] })
const poprzednie = klient.getQueryData(['zadania'])
klient.setQueryData(['zadania'], (stare: Zadanie[]) =>
stare.map((z) => (z.id === id ? { ...z, zrobione: !z.zrobione } : z))
)
return { poprzednie }
},
onError: (_blad, _id, kontekst) => {
klient.setQueryData(['zadania'], kontekst?.poprzednie)
},
onSettled: () => {
klient.invalidateQueries({ queryKey: ['zadania'] })
}
})Anulowanie zapytań w pierwszej linii jest tu częścią najczęściej pomijaną, a rozwiązuje realny problem: odpowiedź z zapytania wysłanego przed Twoją zmianą mogłaby dotrzeć później i nadpisać ją starymi danymi. Pominięcie tego kroku daje interfejs, który po nieudanym zapisie pokazuje zmianę, która nigdy nie doszła do serwera, i to jest gorsze niż brak optymistycznej aktualizacji.
Warto też rozróżnić dwa przypadki. Przy akcji odwracalnej i mało istotnej, jak zwinięcie sekcji, optymistyczna aktualizacja jest właściwa. Przy operacji nieodwracalnej albo kosztownej, jak wysłanie płatności, lepiej pokazać stan oczekiwania i poczekać na potwierdzenie z serwera, bo cofnięcie takiej zmiany w interfejsie nie cofa jej nigdzie indziej.
TanStack kontra alternatywy
| Rozwiązanie | Mocna strona | Słabość | Kiedy wybrać |
|---|---|---|---|
| TanStack | Logika bez wyglądu, typowanie, dojrzała warstwa danych | Więcej pracy na starcie, różna dojrzałość projektów | Aplikacja z własnym systemem wizualnym |
| Gotowe komponenty | Wygląd w komplecie, szybki start | Walka ze stylami przy nietypowych wymaganiach | Panel wewnętrzny, prototyp |
| Zustand | Prosty magazyn stanu klienckiego | Nie zajmuje się danymi z serwera | Stan wspólny dla aplikacji |
| Mechanizmy frameworka | Zero dodatkowych zależności | Mniej możliwości przy złożonym buforowaniu | Aplikacja mocno oparta o renderowanie serwerowe |
Ostatni wiersz warto rozważyć uczciwie, bo przy aplikacjach renderowanych na serwerze część problemów, które ta warstwa rozwiązuje, po prostu nie występuje. Dane pobrane na serwerze i przekazane do komponentu nie wymagają buforowania po stronie klienta.
Warstwa danych z serwera i magazyn stanu klienckiego nie konkurują ze sobą, tylko pokrywają różne rzeczy. Typowa aplikacja używa obu: pierwszej do danych z zaplecza, drugiej do stanu istniejącego wyłącznie w przeglądarce.
Jak wybierać z zestawu
Praktyczna wskazówka, bo pokusa wzięcia wszystkiego naraz jest realna, a to zwykle zły pomysł.
Zacznij od warstwy danych z serwera. To ona daje największy zysk przy najmniejszym koszcie wdrożenia i wpina się w istniejący projekt bez zmiany czegokolwiek innego.
Tabelę dokładaj wtedy, gdy masz konkretną tabelę do zbudowania i wiesz, że gotowa nie wystarczy. Nie warto uczyć się tego zawczasu.
Formularze rozważ przy formularzach mających kilkanaście pól albo więcej. Przy formularzu z trzema polami zwykły stan komponentu jest prostszy i wystarczający.
Router i framework to decyzja architektoniczna, którą podejmuje się na początku projektu i której nie zmienia się później bez przepisywania. Warto ją rozstrzygnąć świadomie, a nie dlatego, że reszta zestawu się sprawdziła.
Projektów we wczesnej fazie nie wstawiaj do systemów produkcyjnych. To nie jest ocena ich jakości, tylko prosty rachunek: oprogramowanie w fazie alfa zmienia interfejs, a Ty za każdym razem płacisz przepisaniem.
Typowe błędy
Pierwszy to niepełny klucz zapytania. Pominięcie parametru wpływającego na wynik sprawia, że po zmianie widzisz stare dane bez żadnego błędu.
Drugi to pozostawienie domyślnego czasu świeżości. Dane odświeżane przy każdym powrocie do karty generują ruch, którego zwykle nie potrzebujesz.
Trzeci to brak unieważnienia po zapisie. Użytkownik widzi wtedy stan sprzed własnej edycji, co wygląda jak zgubiona zmiana.
Czwarty to trzymanie danych z serwera w magazynie stanu klienckiego. Powstaje wtedy ręczna warstwa odtwarzająca buforowanie i odświeżanie, czyli to, co warstwa danych robi sama i lepiej.
Piąty to traktowanie całego zestawu jako jednakowo dojrzałego. Dojrzałość rozciąga się od projektów z wieloletnią historią po fazę alfa.
Szósty to sięganie po tabelę bez własnego systemu wizualnego. Podejście bez wyglądu opłaca się wtedy, gdy masz do czego się dopasować.
Siódmy to wirtualizowanie list, które tego nie potrzebują. Przy dwustu wierszach zysk jest niemierzalny, a kosztem jest utrata wyszukiwania w przeglądarce i dodatkowa warstwa kodu do utrzymania. Warto najpierw zmierzyć, czy przewijanie faktycznie gubi klatki, a dopiero potem sięgać po to rozwiązanie.
Ósmy to aktualizacja optymistyczna bez zapisanego stanu poprzedniego. Interfejs pokazuje wtedy po nieudanym zapisie zmianę, która nigdy nie dotarła do serwera, i użytkownik dowiaduje się o tym dopiero po odświeżeniu strony.
FAQ
Które biblioteki z zestawu są stabilne?
Warstwa danych z serwera, router, tabela, formularze i wirtualizacja list. Framework pełnego stosu jest kandydatem do wydania pierwszej wersji, warstwa danych lokalnych i narzędzia do modeli językowych są w fazie beta, a magazyn stanu w fazie alfa. Pozostałe pozycje z listy, dopisane najpóźniej, mają etykietę beta albo alfa.
Czym różni się warstwa danych od magazynu stanu?
Przeznaczeniem. Pierwsza obsługuje dane pochodzące z serwera, razem z buforowaniem, odświeżaniem i ponawianiem. Drugi trzyma stan istniejący wyłącznie w przeglądarce, na przykład otwarte menu. Typowa aplikacja używa obu do różnych rzeczy.
Dlaczego widzę stare dane po zmianie parametru?
Najczęściej dlatego, że klucz zapytania nie zawiera tego parametru. Klucz określa tożsamość danych, więc musi obejmować wszystko, co wpływa na wynik, inaczej biblioteka uznaje to za wciąż to samo zapytanie.
Czy framework pełnego stosu zastąpi Next.js?
Przy aplikacjach o dużej interaktywności bywa lepszym dopasowaniem, bo wychodzi od aplikacji klienckiej i dokłada renderowanie serwerowe. Przy stronach z przewagą treści Next.js ma dojrzalszy ekosystem i dłuższą historię wdrożeń.
Czy warto brać cały zestaw naraz?
Nie. Zacznij od warstwy danych z serwera, bo daje największy zysk przy najmniejszym koszcie. Resztę dokładaj wtedy, gdy masz konkretny problem do rozwiązania, a projektów w fazie beta i alfa nie wstawiaj do systemów produkcyjnych.
Dokumentacja stoi na stronie projektu, a informacje o wydaniach na blogu zespołu.