Dwadzieścia pytań z wywiadu React, które odróżniają umiejętność używania od rzeczywistego zrozumienia
Virtual DOM, klucze, efekty, memoizacja, Context, SSR i hydratacja – wyjaśnione poprzez trudne pytania, na które faktycznie zadają rekruterzy, a nie definicje z podręczników.
Rozwijanie Reacta i wyjaśnianie jego zasad to różne umiejętności. Podczas rozmów rekrutacyjnych sprawdza się to drugie: co dzieje się przy wywołaniu setState, dlaczego klucze są ważne, kiedy ponownie uruchamiają się efekty. Poniżej znajduje się dwadzieścia pytań, które pojawiają się ciągle. Odpowiedzi opierają się na problemach, które rozwiązuje każda funkcja, oraz na pułapkach, z którymi można się spotkać w produkcji – a nie na powtarzaniu treści z podręczników.
1. Czym właściwie jest Virtual DOM
Wirtualny DOM to nie jakaś magiczna proszek przyspieszający pracę. To zwykły drzewo w JavaScriptzie, które opisuje, jak powinien wyglądać interfejs użytkownika. Gdy zmienia się stan, React tworzy nowe wirtualne drzewo, porównuje je diffowo z poprzednim i uzgadnia je, stosując do DOM-a przeglądarki tylko niezbędne modyfikacje. Rzeczywiste operacje na DOM-ie powodują zmiany w układzie i rysowaniu; modyfikowanie obiektów JavaScript jest tanie, więc React wykorzystuje zasoby procesora w pamięci, aby oszczędzać zasoby przeglądarki. Taki projekt ma na celu minimalizację obciążenia przeglądarki, a nie sprawienie, by każde porównanie było bezkosztowe. Stwierdzenie „Wirtualny DOM jest zawsze szybszy” bez tej niuansy to częsta pułapka początkujących.
2. Wirtualny DOM kontra DOM przeglądarki
Rzeczywiste aktualizacje DOM są kosztowne i mogą powodować przepłynięcie treści oraz ponowne narysowanie elementów. Aktualizacje wirtualne to różnice między obiektami przechowywanymi w pamięci; React grupuje kosztowne operacje zapisu do DOM zamiast wykonywać je za każdym razem po zmianie stanu. Edytowanie przy użyciu mechanizmu track-changes jest lepsze niż przepisywanie całego dokumentu za każdym błędem pisowni.
3. Dlaczego klucze list są ważne przy przesuwaniu wierszy
Klucze identyfikują elementy na przestrzeni różnych renderowań. Bez nich React korzysta z pozycji indeksu, co powoduje problemy przy dodawaniu, usuwaniu lub przestawianiu elementów. Użycie indeksu tablicy jako klucza „działa” dopóki przestawianie nie powoduje przyłączenia pól wprowadzania i poli wyboru do niewłaściwych wierszy – to fałszywy „wyciek stanu”, który w rzeczywistości jest błędem klucza. Lepiej używać stabilnych, unikalnych identyfikatorów pochodzących z danych; indeksy nadają się tylko do naprawdę statycznych list. Jeśli produkt umożliwia przestawianie elementów za pomocą przeciągania lub filtrowanie, indeksy jako klucze ostatecznie uszkodzą lokalny stan wierszy.
4. Wyjaśnienie useState na podstawie zasad
Zwykli użytkownicy aplikacji umierają, gdy funkcja się zakończy. useState zapewnia komponentowi funkcyjnemu trwałą pamięć i planuje ponowne renderowanie, gdy ta pamięć ulegnie zmianie:
const [count, setCount] = useState(0);
count to aktualna wartość; setCount żąda jej aktualizacji. Aktualizacja nie jest stosowana w trakcie renderowania: zapisanie wartości count bezpośrednio po wywołaniu setCount nadal pokazuje poprzednią wartość, ponieważ nowa wartość pojawia się podczas następnego renderowania.
5. Dlaczego aktualizacje useState wydają się opóźnione lub grupowane
React grupuje aktualizacje, które pochodzą z tego samego wydarzenia, w jedno renderowanie. Od wersji React 18 to grupowanie obejmuje również obietnice i timerzy, a nie tylko obsługi wydarzeń w React. Gdy potrzebujesz najnowszej wartości na podstawie poprzedniego stanu, użyj funkcjonalnego aktualizatora:
setCount(prev => prev + 1);
To pole wyświetla najnowszą wartość z kolejki, a nie przestarzały zapis stanu. Pytający często pytają dalej, co się stanie, jeśli zamkniesz zmienną count w ramach timeoutu bez funkcji aktualizacji.
6. Jakie problemy faktycznie rozwiązuje useEffect?
Renderowanie powinno być czystą funkcją przekształcającą props i stan w JSX. Aplikacje pobierają również dane, przypinają słuchacze zdarzeń, planują timerzy i modyfikują DOM – to efekty uboczne. useEffect wykonywa te nieczyste operacje po zapisaniu zmian, a nie podczas renderowania:
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id); // cleanup
}, []);
Funkcja czyszczenia zwracana jest wykonywana przed następnym efektem oraz przy demontażu komponentu. Jej pominięcie skutkuje pytaniami o dublowane timerzy i słuchacze, które nadal działają po demontażu.
7. Co faktycznie kontroluje tablica zależności?
Powiadamia ona React, kiedy należy ponownie wykonać efekt, poprzez płytkie porównania:
[]— raz po montowaniu[count]— ponownie gdy zmieni sięcount- pominięte — po każdym renderowaniu (rzadko pożądane)
Jeśli zapomnisz o wartości odwołanej w tablicy, powstanie stare zamknięcie: efekt ten zachowuje pierwszą zapisaną wartość na zawsze. Istnieją reguły sprawdzania zależności w celu wykrycia takich problemów, ponieważ ten typ błędu jest bardzo powszechny.
8. Komponenty kontrolowane vs niekontrolowane
Wejścia kontrolowane biorą wartość value ze stanu React i aktualizują się za pomocą onChange; to React decyduje o prawdziwej wartości.
Wejścia niekontrolowane przechowują stan DOM; są odczytywane za pomocą ref w razie potrzeby (często przy wysyłaniu formularza).
// Controlled
<input value={name} onChange={e => setName(e.target.value)} />
// Uncontrolled
<input ref={inputRef} defaultValue="Dev" />
Tryb kontrolowany umożliwia walidację i formatowanie w czasie rzeczywistym, ale kosztem ponownego renderowania przy każdym naciśnięciu klawisza. Tryb niekontrolowany pozostaje lżejszy, gdy potrzebny jest tylko ostateczna wartość. Możliwe są formularze mieszane – z niektórymi pólami kontrolowanymi, a innymi nie – ale trudniej jest nimi zarządzać podczas przeglądania.
9. Przekazywanie właściwości i kiedy przestać
Przekazywanie właściwości polega na przesyłaniu danych przez warstwy, które je jedynie przekazują dalej. Przenoszenie nazw wpływa na wiele plików. Kontekst jest przydatny w przypadku tematów, autoryzacji lub ustawień lokalnych; Redux lub Zustand pomagają, gdy grafy stanu stają się złożone. Nuanse: kontekst nie jest darmowy – każdy konsument ponownie renderuje się, gdy wartość się zmienia – więc nie jest standardem dla wszystkich współdzielonych pól. Przekazywanie właściwości na dwa poziomy jest często jaśniejsze niż tworzenie dostawcy kontekstu specjalnie dla jednorazowej wartości.
10. Kiedy użyć useReducer zamiast useState?
Należy użyć useReducer, gdy aktualizacje odbywają się w zależności od typu akcji, gdy następny stan zależy od poprzedniego w złożony sposób lub gdy kilka pól zmienia się jednocześnie:
function reducer(state, action) {
switch (action.type) {
case "increment": return { count: state.count + 1 };
case "reset": return { count: 0 };
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
Jest to mini-Redux wewnątrz komponentu. Przełącznik logiczny typu boolean pozostaje w useState; skomplikowana logika przejść powinna znajdować się w jednym reduktorze podlegającym testowaniu. Wyjęcie tej logiki z obsługi zdarzeń w JSX sprawia również, że testy jednostkowe stają się proste, bez konieczności renderowania całego drzewa.
11. Wyjaśnij różnicę między useMemo a useCallback, nie polegając tylko na cytowaniu dokumentacji
Oba mechanizmy pomijają zbędne operacje podczas kolejnych renderów dla różnych struktur danych:
useMemoprzechowuje w pamięci wartość obliczonąuseCallbackprzechowuje w pamięci odniesienie do funkcji
const sorted = useMemo(() => expensiveSort(list), [list]);
const handleClick = useCallback(() => doThing(id), [id]);
Tożsamość funkcji ma znaczenie, ponieważ każde odświeżenie tworzy nowy obiekt funkcji. Przekazanie zupełnie nowej funkcji do potomka typu memo unieważnia tę mechanizm memoizacji; useCallback zapewnia stabilność referencji. Nie należy stosować tych hooków wszędzie – memoizacja wiąże się z pewnymi kosztami. Należy je używać tylko wtedy, gdy jest to konieczne przy kosztownych operacjach lub w przypadku potomków, które zostały zmemorizowane. Przedwczesna memoizacja jest częstym błędem początkujących, którymi lubią się bawić przesłuchujący.
12. React.memo jest przydatne – ale łatwe do obejścia
React.memo pomija ponowne odświeżenie, gdy właściwości są tylko powierzchownie identyczne. Nowe literale obiektów lub tablic są uważane za różne, nawet jeśli ich zawartość jest identyczna, więc użycie wiersza { style: { color: 'red' } } sprawia, że memo staje się bezużyteczne, chyba że rodzice ustabilizują właściwości za pomocą useMemo/useCallback. W przeciwnym razie memo powoduje dodatkowe koszty porównywania, nie zapobiegając jednocześnie wykonywaniu operacji przez potomka.
13. Klucze jako identyfikator, a nie tylko ostrzeżenie konsoli
Klucze stanowią system identyfikacji w React pomiędzy renderowaniami. Błędne klucze powodują ponowne użycie niewłaściwego węzła DOM dla nieodpowiednich danych: stan formy pozostaje przy innej wierszu, animacje uruchamiają się na niewłaściwym elemencie, a funkcja useState w elementach listy przechowuje wartość poprzedniego elementu. Wygląda to jak uszkodzenie stanu, ale w rzeczywistości jest to błąd związany z kluczami. Pokazanie uszkodzonej listy z index-key w środowisku sandbox to jeden z najszybszych sposobów na opanowanie tej zasady.
14. Kontekst: właściwe i niewłaściwe zastosowania
Kontekst umożliwia udostępnianie wartości potrzebnych wielu komponentom bez konieczności przekazywania ich przez propy — np. autoryzowany użytkownik, temat, lokalizacja:
const ThemeContext = createContext();
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
Jest to słabe rozwiązanie dla stanów wysokiej częstotliwości (wartości formy na każdy nacisk klawisza) w dużych drzewach, ponieważ każdy użytkownik aktualizuje dane przy każdej zmianie bez selektywnego subskrybowania. Lepiej wybrać magazyn z bardziej precyzyjnymi subskrypcjami dla takiego formatu. Przełączniki tematów to klasyczny przykład dobrego dopasowania do kontekstu; pozycje kursora w edytorze współpracy zazwyczaj takim nie są.
15. Mapowanie cykli życia klas na efekty
Ogólne mapowanie klas:
componentDidMount→useEffect(..., [])componentDidUpdate→useEffect(..., [dep])componentWillUnmount→ czyszczenie efektów
Głębsza zmiana: cykle życia funkcjonują w oparciu o czas (uzyskanie dostępu/aktualizacja/odzyskanie dostępu); efekty natomiast opierają się na synchronizacji – konieczności utrzymania zgodności tego zewnętrznego systemu z tymi wartościami – dlatego efekty są uruchamiane ponownie, gdy zmieniają się zależności. Traktowanie efektów jako metod cyklu życia tłumaczonych słowo w słowo powoduje, że ludzie walczą z tablicą zależności.
16. Stan w porównaniu z propami
Propy to dane tylko do odczytu pochodzące od komponentu nadrzędnego. Stan to dane należące do konkretnego komponentu, które powodują ponowne renderowanie w momencie zmiany. Propy konfigurują komponent z zewnątrz; stan to to, co komponent pamięta o sobie samym. label przycisku to prop; to, czy jest wyłączony podczas trwania żądania, to stan. Mylenie tych dwóch pojęć prowadzi do antypatronów, takich jak próby modyfikacji propów lub umieszczanie zbyt wysoko tymczasowych flag interfejsu użytkownika.
17. Wykrywanie niepotrzebnego ponownego renderowania bez domysłów
Powodami są: przekazywanie przez rodziców nowych literali obiektów/maszyn tablic/funkcji, ponowne renderowanie wszystkich użytkowników z powodu częstych zmian kontekstu, lub przechowywanie stanu na zbyt wysokim poziomie. Nie zgaduj — użyj Profilera w React DevTools, nagraj interakcję i sprawdź, które właściwości uległy zmianie. Rozwiązania polegają zazwyczaj na przeniesieniu stanu niżej lub podziale komponentów, aby kosztowne poddrzewa nie były renderowane przy tanich aktualizacjach, zamiast najpierw używać useMemo. Profilery zamieniają wrażenie „wszystko jest wolne” na konkretną informację dotyczącą rodzica/właściwości, którą można naprawić.
18. SSR w porównaniu z CSR
CSR wysyła lekką skorupę HTML wraz z kodem JS; przeglądarka buduje stronę po pobraniu — szybko ją serwuje, ale wolniej wyświetla w pełni funkcjonalną wersję, a historycznie słabiej radzi sobie z SEO dopóki nie uruchomi się kod JS. SSR wysyła HTML na żądanie, a następnie hydrates strukturę poprzez dodanie słuchaczy — zapewnia lepsze szybkie wyświetlenie i lepsze wyniki w SEO, ale wymaga większej pracy serwera. Next.js i podobne narzędzia dodają funkcje generowania statycznych treści oraz strumieniowania, ale podstawowy kompromis pozostaje między czasem odpowiedzi serwera/SEO a kosztem i złożonością serwera. Stwierdzenie, że „SSR jest zawsze lepsze”, bez wskazania tego kompromisu, to słaba odpowiedź na pytanie w rozmowie kwalifikacyjnej.
19. Niespójności podczas procesu hydracji i sposób ich występowania
Hydration łączy React z HTML serwera, nie niszcząc przy tym kodu strony. Niezgodności występują, gdy HTML serwera różni się od pierwszego renderowania na stronie klienta: Date.now() lub Math.random() są używane podczas renderowania, a sprawdzania związane z window dają różne wyniki na serwerze, lub też występują rozszerzenia wstrzykujące nowe elementy. React ostrzega głośno i często ponownie renderuje treść na stronie klienta, aby to naprawić – co wiąże się z dodatkowym obciążeniem oraz widocznym błyskiem nieprawidłowej treści. Zwykłą metodą zapobiegania temu jest ukrywanie API dostępnych tylko w przeglądarce za pomocą useEffect lub komponentów specjalnych.
20. Dlaczego React otacza natywne zdarzenia obiektami SyntheticEvent
SyntheticEvent w React normalizuje specyficzne zachowania różnych przeglądarek (dzięki czemu onChange działa spójnie) i historycznie wykorzystywał delegację zdarzeń na poziomie korzenia zamiast jednego natywnego słuchacza na każdy węzeł. W React 17+ delegacja odbywa się do kontenera korzeniowego aplikacji zamiast do document, ale zasada pozostaje ta sama. Wcześniej stosowano gromadzenie obiektów zdarzeń, aby umożliwić asynchroniczny dostęp do wartości null; gromadzenie to zniknęło od wersji React 17, jednak świadomość warstwy pomiędzy zdarzeniem przeglądarki a obsługującym je kodem nadal wskazuje na jego poziom. Wspomnienie o tym, że e.nativeEvent nadal istnieje poniżej, pokazuje, że rozumie się, iż ta abstrakcja jest tylko otoczką, a nie zamiennikiem modelu zdarzeń DOM.
Czego naprawdę doceniają na rozmowach kwalifikacyjnych
Mocne odpowiedzi na pytania podczas rozmowy wyjaśniają problem, jego trudności oraz to, jak rozwiązałbyś sytuację z kolegą z zespołu – a nie tylko wyuczoną definicję. Rozmówcy mniej interesuje to, czy potrafisz zdefiniować useEffect, a bardziej to, czy problemy spowodowane przestarzałymi zamykaniami funkcji, brakiem czyszczenia zasobów lub błędami w zależnościach wpłynęły na twoją pracę, oraz czy potrafisz to wytłumaczyć. Przed rozmową spróbuj odtworzyć te błędy w środowisku testowym; doświadczone rozwiązania pokazują, że naprawdę rozumiesz temat. Definicje pomagają przetrwać pierwszą minutę rozmowy, natomiast analiza kompromisów i historii niepowodzeń prowadzi dalszą część rozmowy.
Miej przy sobie krótki osobisty notatnik z błędami, które naprawiłeś – przestarzałe efekty, błędne klawisze, niezgodności związane z hydratacją – i ćwicz wyjaśnianie każdego z nich w czasie krótszym niż minuta. Taka przygotowanie jest lepsze od uczenia się na pamięć sygnatur API w noc przed rozmową i pozwala podać konkretne przykłady, gdy rekruter poprosi o ilustrację Twojej pracy. Połącz każdy przykład z rozwiązaniem, które wdrożyłeś, aby odpowiedź skupiała się nie tylko na trudnościach, ale także na ocenie rozwiązania. To właśnie ten ostatni element odróżnia osoby, które tylko „czytały dokumentację”, od tych, które potrafią efektywnie zarządzać takim stackiem pod presją.