Rozumowanie mechanizmów React Hooks poprzez zrzuty ekranu renderowania i analizę kompromisów.
Zaawansowany model mentalny dotyczący React i React Native Hooks: zrzuty ekranu renderowania, efekty, referencje, memoizacja, niestandardowe Hooks oraz sposoby wyjaśniania kompromisów podczas rozmów kwalifikacyjnych.
Źródła: React & React Native Hooks: The Complete Senior Interview Guide
Rozmowy kwalifikacyjne z doświadczonymi programistami React i React Native rzadko kończą się na pytaniu „co robi ten Hook?”. Pytanie o useState w ciągu minuty przeradza się w temat grupowania operacji i aktualizacji funkcyjnych; pytanie o useEffect prowadzi do dyskusji na temat pętli nieskończonych, zastarzałych zamknięć, anulowania wywołań oraz sytuacji, gdy efekt w ogóle nie powinien istnieć; useMemo i useCallback natychmiast prowadzą do pytania „czy to rzeczywiście przyspiesza aplikację?”. Wymuszone definicje nie wytrzymują takiego rodzaju dalszych pytań. To, co przetrwa, to spójny model, a ten przewodnik go buduje: renderowanie, następnie stan, zamknięcia, efekty, referencje, memoizacja, niestandardowe Hooks, wydajność, a na końcu architektura, z przykładami React Native tam, gdzie platforma zmienia obraz sytuacji.
Model renderowania, od którego zależy wszystko inne
Zacznij od jednej idei i trzymaj się jej: gdy React renderuje komponent, ponownie wywołuje funkcję tego komponentu od początku. Użyj najmniejszego możliwego licznika.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
Naturalne jest wyobrażenie sobie, że kliknięcie przycisku dociera do komponentu i zmienia zmienną count. Tak jednak nie jest. Funkcja setter prosi React o nowy render, a następne wywołanie Counter otrzymuje nową wartość jako zupełnie nową stałą.
Render 1
count = 0
↓
User clicks
↓
setCount(1)
↓
React schedules another render
↓
Render 2
count = 1
Każda renderizacja posiada zatem kopię swoich właściwości i stanu. Nic w tej kopii nie zmienia się później. To proste zaobserwowanie wyjaśnia zjawisko zamknięć funkcji na starych wartościach, przestarzały stan w timerach, powód istnienia zależności efektów, zachowanie funkcji zwrotnych i obsługujących zdarzenia oraz to, dlaczego memoizacja dotyczy identyczności. Pytający, którzy zagłębiają się poza powierzchnię API, zazwyczaj sprawdzają, czy rozumiesz ten model.
useState: stan należący do renderizacji
useState dostarcza komponentowi funkcyjnemu fragment stanu, który wpływa na jego wynik.
const [count, setCount] = useState(0);
Pary, które otrzymujesz w odpowiedzi, pełnią dwie różne role: wartość dla bieżącej renderizacji oraz funkcja, która żąda aktualizacji.
count
↓
current state
setCount
↓
request a state update
Wywołanie funkcji ustawiającej nie zmienia nic na miejscu; zamiast tego planuje kolejną renderizację, w której komponent dostrzeże nową wartość.
Jak rosną trudność pytań
Typowy przebieg nauki wygląda następująco:
- Podstawy: jakie problemy rozwiązuje
useState? - Poziom średni: w czym różnica między stanem a zwykłą lokalną zmienną?
- Poziom zaawansowany: kiedy funkcja aktualizująca jest właściwym wyborem?
- Głębszy poziom: co faktycznie daje wywołanie
setCount(count + 1)trzy razy w jednym obsługiwaczu?
Odpowiedź „Przechowuje stan” odpowiada tylko na pierwsze pytanie. Silniejsza odpowiedź obejmuje cały model:
useState przechowuje wartość, która wpływa na to, co renderuje komponent. Wywołanie funkcji ustawiającej umieszcza w kolejce nowy render z zaktualizowaną wartością, więc stan najlepiej traktować jako część zrzutu obrazu tego rendera, a nie jako zmienną do modyfikacji. Gdy następna wartość zależy od poprzedniej, funkcja aktualizująca jest bezpiecznym wyborem, ponieważ React grupuje aktualizacje, a zapisana wartość może już być przestarzała.
Zwykła zmienna natomiast jest tworzona na nowo ze swoją początkową wartością przy każdym renderowaniu, a jej zmiana nigdy nie zmusza React do ponownego renderowania, dlatego nie może sterować interfejsem użytkownika.
Dlaczego ważne są aktualizacje funkcyjne
Oto trzy operacje zwiększania wartości wykonywane na podstawie zapisanej wartości.
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
Wszystkie trzy operacje odczytują tę samą wartość count z bieżącego stanu, więc po zgrupowaniu operacji wynikiem jest jedno zwiększenie, a nie trzy. Taka forma aktualizatora to naprawia:
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
React przekazuje każdemu aktualizatorowi najnowszą wartość, więc trzy wywołania działają poprawnie. Należy stosować tę formę, gdy kilka aktualizacji następuje jednocześnie lub jest kolejkowanych, gdy funkcja zwrotna może zostać wywołana z przestarzałym stanem, oraz ogólnie wtedy, gdy nowa wartość jest obliczana na podstawie starej.
Koszyk zakupów w aplikacji React Native to realistyczny przykład. Ilość produktów jest wyświetlana, więc musi znajdować się w stanie aplikacji:
const [quantity, setQuantity] = useState(1);
Obsługi zdarzeń obliczają nową wartość na podstawie poprzedniej, a funkcja zmniejszania ogranicza ją do wartości 1, dzięki czemu koszyk nigdy nie może pokazywać ilości zero lub ujemnej.
const increaseQuantity = () => {
setQuantity(prev => prev + 1);
};const decreaseQuantity = () => {
setQuantity(prev => Math.max(1, prev - 1));
};
Nic się nie zmienia pomiędzy wersją webową a natywną
Hook jest identyczny na obu platformach. W wersji webowej rozwijana lista może śledzić, czy jest otwarta:
const [isOpen, setIsOpen] = useState(false);
Ekran w React Native może w ten sam sposób śledzić widoczność modalu:
const [isModalVisible, setModalVisible] = useState(false);
Mechanizm stanu jest wspólny; różnią się jedynie elementy interfejsu, które przez niego są sterowane.
useEffect: synchronizacja z otoczeniem
No Hook nie generuje większej ilości dyskusji podczas rozmowy. Powszechny opis „wykonuje kod po renderowaniu” jest prawdziwy, ale pomija jego cel. Lepszym sformułowaniem jest to, że efekt ten utrzymuje komponent w synchronizacji z systemem, którego React nie kontroluje.
W sieci tym systemem mogą być zdarzenia DOM, API przeglądarki, WebSockets, subskrypcje, timerzy lub żądania sieciowe. W React Native jest to często AppState, moduł Keyboard, moduły natywne, odbiorniki powiadomień typu push, stan sieciowy, API urządzenia, WebSockets oraz timerzy.
Jak rozwijają się pytania
- Bazowe: do czego służy
useEffect? - Średniozaawansowane: wyjaśnienie funkcjonowania tablicy zależności.
- Zaawansowane: co powoduje, że efekt działa w nieskończoność?
- Zaawansowane+: w jakich sytuacjach efekt jest niewłaściwym narzędziem?
- W produkcji: jak zakończyć subskrypcję WebSocket?
Porównaj „useEffect służy do wywołań API” z tym:
useEffect istnieje głównie po to, aby utrzymać komponent zsynchronizowany z zewnętrznym systemem. Tablica zależności wymienia wartości reaktywne, które efekt odczytuje, i musi być dokładna; reguła lintingu Hooks wykrywa pominięcia. Gdy efekt otwiera subskrypcję, uruchamia timer lub przyłącza słuchacz, zwraca funkcję czyszczenia, dzięki której stary zasób jest zwalniany przed ponownym uruchomieniem efektu lub gdy komponent jest usuwany.
Odczytywanie tablicy zależności
Gdy istnieje zależność, efekt jest wykonywany po pierwszym renderowaniu oraz za każdym razem, gdy ta wartość ulega zmianie pomiędzy renderowaniami.
useEffect(() => {
fetchUser(userId);
}, [userId]);
Gdy nie ma żadnej tablicy, efekt jest wykonywany po każdym renderowaniu:
useEffect(() => {
// ...
});
Gdy tablica jest pusta, efekt jest wykonywany po zamontowaniu komponentu:
useEffect(() => {
// ...
}, []);
Uważaj na sformułowanie „działa raz”. W trybie rozwojowym Strict Mode celowo montuje, czyszczy i ponownie montuje skutki, które nie są prawidłowo usuwane. Dokładniejsze stwierdzenie brzmi, że skutek z pustym tablicą jest wykonywany po montowaniu, a tryb rozwojowy Strict Mode może dodać dodatkowy cykl konfiguracji i czyszczenia. Kod, który zawodzi podczas tego podwójnego wykonywania, zazwyczaj ma rzeczywisty błąd.
Czyszczenie zapobiega wyciekom zasobów
Subskrypcja, która polega na userId, otwiera połączenie w ciele skutku:
useEffect(() => {
const subscription = subscribeToUpdates(userId);
i zwraca funkcję, która je zamyka:
return () => {
subscription.unsubscribe();
};
}, [userId]);
Gdy userId się zmienia, React wykonywa poprzednie czyszczenie przed uruchomieniem nowego skutku, dzięki temu nigdy nie ma więcej niż jednej aktywnej subskrypcji:
Old effect
↓
cleanup old subscription
↓
new effect
↓
subscribe using new userId
Podczas demontowania wykonywane jest ostatnie czyszczenie i nic nie pozostaje w tle:
component unmounts
↓
cleanup
↓
subscription removed
Ten sam mechanizm służy do anulowania pracy asynchronicznej. W przypadku operacji fetch można przerwać żądanie za pomocą AbortController podczas czyszczenia, lub ustawić flagę ignore, aby późna odpowiedź od starego userId nie nadpisała nowszych danych.
Przykład w React Native: reakcja na AppState
Bardziej przekonującym efektem niż pobieranie danych jest taki, który łączy komponent z cyklem życia aplikacji natywnej. Ten słuchacz odświeża dane, gdy aplikacja wraca na pierwszy plan, i usuwa się po demontażu komponentu.
useEffect(() => {
const subscription = AppState.addEventListener(
"change",
nextState => {
if (nextState === "active") {
refreshData();
}
}
);
return () => {
subscription.remove();
};
}, []);
To jest synchronizacja z zewnętrznym systemem w swojej najprostszej formie i pokazuje rekruterowi, że rozumiesz cel efektów.
Błąd: synchronizacja wartości pochodnych za pomocą efektu
Załóżmy, że formularz przechowuje imię i nazwisko:
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
Często spotykanym antypatternem jest dodawanie trzeciego elementu stanu oraz efektu w celu jego aktualizacji:
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
To powoduje nadmiarowy stan oraz dodatkową renderizację za każdym razem, gdy zmienia się nazwa, a przy jednej renderizacji pełna nazwa jest przestarzała. Ponieważ wartość jest w całości określana przez istniejący stan, należy ją obliczyć podczas renderizacji:
const fullName = `${firstName} ${lastName}`;
Zasada, którą warto podkreślić: jeśli wartość może być obliczona synchronicznie na podstawie propsów lub stanu, należy ją wyznaczyć podczas renderizacji zamiast przechowywać ją i aktualizować w efekcie. Ten wzorzec jest omawiany bardziej szczegółowo w stop syncing state with useEffect.
Błąd: niestabilne zależności od obiektów
Tutaj literal obiektu jest tworzony wewnątrz komponentu i wymieniany jako zależność.
const filters = {
status: "active",
};
useEffect(() => {
fetchUsers(filters);
}, [filters]);
Przy każdym renderowaniu tworzony jest nowy obiekt filters, a zależności porównywane są przez odniesienie, więc efekt za każdym razem widzi „zmianę”. Jeśli efekt aktualizuje stan, cykl żywi się sam:
Render
↓
new filters object
↓
dependency changed
↓
effect
↓
setState
↓
render
↓
new filters object
↓
effect again
Rozwiązaniem refleksyjnym jest otoczenie obiektu mechanizmem memoizacji:
useMemo(...)
Zanim to zrobisz, sprawdź, czy obiekt w ogóle musi być tworzony wewnątrz komponentu. Stała może zostać przeniesiona poza komponent; wartość utworzona na podstawie propów często może zostać zastąpiona poprzez korzystanie z jej pierwotnych pól (na przykład status) lub poprzez jej utworzenie wewnątrz efektu. Poprawa przepływu danych to lepsze rozwiązanie; dodawanie memoizacji w celu jej ukrycia nie jest takie dobre.
useRef: pamięć, która nie jest renderowana
useRef zwraca zmienne kontener, który przetrwa różne renderowania, a którego zmiany nigdy nie wywołują kolejnego renderowania.
const valueRef = useRef(0);
Czytasz i zapisujesz jego zawartość za pomocą właściwości current:
valueRef.current
Przypisanie nowej wartości, jak poniżej, aktualizuje kontener, ale pozostawia ekran w takim stanie, w jakim był, dopóki coś innego nie spowoduje ponownego renderowania.
valueRef.current = 10;
Pracodawcy zazwyczaj przechodzą od pytania „w czym różnica między ref a stanem?” do „podaj prawdziwy przykład” i na koniec do „dlaczego tutaj wybrać ref zamiast stanu?”. Dobra odpowiedź:
Stan to dane, które służą do renderowania, więc ich zmiana uruchamia proces renderowania. Ref utrzymuje zmienną wartość przez kolejne renderowania, bez konieczności ponownego renderowania przy zmianie
.current. Refy nadają się do przechowywania imperatywnych identyfikatorów, ID timerów, poprzednich wartości oraz innych zmiennych danych, które interfejs nie wyświetla.
W sieci ref umożliwia imperatywny dostęp do węzła DOM, na przykład w celu skupienia uwagi na polu wprowadzania danych:
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();
W React Native refy są równie powszechne, ponieważ interfejsy użytkownika w urządzeniach mobilnych często wymagają poleceń imperatywnych, takich jak przewijanie listy z powrotem na górę:
const scrollRef = useRef<ScrollView>(null);
scrollRef.current?.scrollTo({
y: 0,
animated: true,
});
Wybór między stanem a refem
Pytanie decydujące jest proste: czy zmiana tej wartości musi wpłynąć na to, co widzi użytkownik? Jeśli tak, wybierz
useState
a jeśli nie, wybierz
useRef
Aktywna karta oczywiście wpływa na interfejs użytkownika, więc to jest stan:
const [selectedTab, setSelectedTab] = useState("home");
Obsługa timera istnieje tylko po to, aby można je było później wyłączyć; nic go nie renderuje, więc ref jest odpowiednim rozwiązaniem:
const timerRef = useRef<number | null>(null);
useMemo: przechowywanie wartości obliczonej
useMemo przechowuje wynik obliczenia i ponownie go oblicza tylko wtedy, gdy zmienia się jakaś zależność. Klasycznym przykładem jest filtrowana i posortowana lista użytkowników:
const filteredUsers = useMemo(() => {
return users
.filter(user => user.active)
.sort(sortUsers);
}, [users]);
Dopóki users zachowuje tę samą referencję, filtrowanie i sortowanie są pomijane podczas kolejnych renderów. Należy zwrócić uwagę na ukryty wymóg: sortUsers również musi być stabilny lub musi być wymieniony jako zależność, a funkcja sort modyfikuje dane na miejscu – co jest bezpieczne tylko dlatego, że filter już stworzył nowy tablicę.
Następne pytanie to niemal zawsze „czy powinniśmy go używać wszędzie?”. Odpowiedź wymagająca oceny:
Nie. Funkcja useMemo nie jest darmowa: dodaje monitorowanie zależności, zużywa pamięć i wymaga więcej kodu do przeczytania. Ma sens wtedy, gdy kosztowne obliczenia są powtarzane bez potrzeby lub gdy stabilna referencja ma znaczenie dla późniejszych operacji, a analiza wydajności powinna poprzedzić jej dodanie.
Nadużywaniem jest przechowywanie w pamięci wyników łączenia ciągów znaków:
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);
Praca jest banalna, więc zwykły wyrażenie jest zarówno szybszy do odczytania, jak i przynajmniej tak samo szybki w wykonywaniu:
const fullName = `${firstName} ${lastName}`;
Teraz rozważmy aplikację do handlu w React Native, która pokazuje tysiące transakcji, filtrowanych według symbolu i okna czasowego oraz posortowanych od najnowszych na początku.
const visibleTrades = useMemo(() => {
return trades
.filter(trade => trade.symbol === selectedSymbol)
.filter(trade => trade.timestamp >= startTime)
.sort((a, b) => b.timestamp - a.timestamp);
}, [trades, selectedSymbol, startTime]);
Gdy ta transformacja jest znacząco kosztowna, memoizacja staje się niezbędna. Mimo to należy to potwierdzić za pomocą narzędzia do analizy wydajności, a nie zakładać tego bezpodstawnie.
useCallback: zachowanie tożsamości funkcji
useCallback zwraca ten sam obiekt funkcji pomiędzy renderami, dopóki nie zmienią się jej zależności.
const handlePress = useCallback(() => {
submitOrder();
}, [submitOrder]);
Różnica między useMemo a useCallback polega jedynie na tym, co jest zapisywane w pamięci cache. Jeden z nich zapisuje wartość:
useMemo
→ memoises a value
A drugi zapisuje odwołanie do funkcji:
useCallback
→ memoises a function reference
W rzeczywistości useCallback(fn, deps) zachowuje się jak useMemo(() => fn, deps). „Aby poprawić wydajność” to słaba odpowiedź na pytanie „dlaczego tego używać?”. Lepsza odpowiedź brzmi:
useCallback utrzymuje stabilność tożsamości funkcji, gdy jej zależności pozostają niezmienne. Warto go używać tam, gdzie rzeczywiście ma znaczenie równość referencji, na przykład w przypadku callbacka przekazywanego do zmemorizowanego komponentu potomnego lub funkcji, którą inny Hook wymienia jako zależność. Nie powinien być dodawany domyślnie, ponieważ memoryzacja ma swój własny koszt i utrudnia zrozumienie kodu.
Jak łączy się z React.memo
React.memo pomija ponowną renderizację komponentu, gdy wszystkie jego props są powierzchownie równe poprzednim.
const UserRow = React.memo(function UserRow({
user,
onPress,
}) {
return (
// ...
);
});
Jeśli komponent nadrzędny przekazuje nową funkcję strzałkową przy każdej renderizacji, zawsze jedna z właściwości będzie inna:
<UserRow
user={user}
onPress={() => openUser(user.id)}
/>
Ponieważ odniesienie się zmienia, płytkie porównanie zawodzi i wiersz zapisany w pamięci jest mimo to renderowany, co może potajemnie zniwelować korzyści płynące z React.memo. Stabilny obsługa wygląda tak:
const handlePress = useCallback(() => {
openUser(user.id);
}, [user.id]);
i jest przekazywany zamiast funkcji inline:
<UserRow
user={user}
onPress={handlePress}
/>
Pamiętaj o zastrzeżeniu: rób to wtedy, gdy masz dowody na to, że renderowanie dziecka jest kosztowne, a nie jako odruch.
Zamknięcia i problem starych wartości
Zamknięcia to funkcja JavaScript, a nie Hook, ale bez nich trudno jest rozumieć Hooks. Zamknięcie to funkcja, która zachowuje dostęp do zmiennych ze środowiska, w którym została stworzona.
function createLogger(name: string) {
return () => {
console.log(name);
};
}
Funkcja wewnętrzna nadal widzi name długo po tym, jak createLogger zwróciło się.
Jak w React pojawia się stare zamknięcie
Rozważmy komponent z licznikiem:
function Counter() {
const [count, setCount] = useState(0);
i dwa przyciski – jeden do zwiększania liczby, a drugi do zapisywania jej wartości po upływie jednej sekundy:
const handleClick = () => {
setTimeout(() => {
console.log(count);
}, 1000);
}; return (
<>
<button onClick={handleClick}>
Log Count
</button> <button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</>
);
}
Załóżmy, że obsłużnik logowania został utworzony podczas renderowania:
count = 0
Jeśli klikniesz „Zapisz liczbę” i kilka razy zwiększysz ją w ciągu tej sekundy, nadal zostanie wyświetlony komunikat o timeoutie
0
mimo że kolejne renderowania już się zakończyły
count = 1
count = 2
count = 3
Funktor powrotny timeoutu odwołuje się do zmiennej count z renderowania, w którym została utworzona, a ta wartość nigdy się nie zmienia.
Wyjaśnianie tego na rozmowie kwalifikacyjnej
Komponenty funkcyjne działają w ramach „render snapshots”: każde renderowanie ma swoje własne wartości oraz swoje własne zamykania funkcji. Funkcja zwrotna utworzona w jednym renderowaniu przechwytuje wartości tego renderowania, dzięki czemu elementy asynchroniczne, takie jak timer, mogą obserwować stary stan. Rozwiązanie zależy od potrzeb: aktualizacja funkcyjna, poprawne zależności efektów lub użycie ref, gdy naprawdę potrzebna jest najnowsza zmienna wartości bez wywoływania nowego renderowania.
To rozwiązanie wskazuje przyczynę problemu i proponuje trzy sposoby naprawy wraz z warunkami ich zastosowania, co dokładnie szuka doświadczony rozmówca.
Custom Hooks: pakietowanie zachowań
Custom Hooks to miejsce, gdzie łączą się poprzednie koncepcje. Custom Hook to funkcja, której nazwa zaczyna się od use i która wywołuje inne Hooks, umożliwiając w ten sposób wyodrębnienie zachowań związanych ze stanem i ich ponowne użycie. Ładowacz profili zaczyna od deklaracji swojego stanu:
function useUserProfile(userId: string) {
const [data, setData] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
Następnie wykonywane jest żądanie za pomocą efektu powiązanego z userId, a zwracane są trzy wartości:
useEffect(() => {
// fetch user
}, [userId]); return {
data,
loading,
error,
};
}
Ekran wykorzystuje je, nie wiedząc, jak działa proces ładowania:
function ProfileScreen({ userId }) {
const {
data,
loading,
error,
} = useUserProfile(userId);
i renderuje to, czego potrzebuje, na podstawie uzyskanego rezultatu:
// UI
}
Należy pamiętać, że każdy komponent wywołujący własny Hook ma swój niezależny stan; Hook dzieli się logiką, a nie danymi.
Co tak naprawdę sprawdza rekruter
Pytanie „czy potrafisz napisać własny Hook?” dotyczy w rzeczywistości umiejętności rozpoznawania ponownie używalnych abstrakcji behawioralnych. Dobrzy kandydaci zachowują się w ten sposób:
useAuth()
useUserProfile()
useNetworkStatus()
usePagination()
useDebounce()
useKeyboard()
useAppState()
usePermissions()
useInfiniteScroll()
W React Native przykładem może być Hook do śledzenia stanu sieci – subskrybuje się na zmiany łączności, przechowuje informację o tym w stanie i zwraca funkcję anulowania subskrypcji jako element czyszczenia.
function useNetworkStatus() {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const unsubscribe = subscribeToNetworkChanges(
state => {
setIsConnected(state.isConnected);
}
);
return unsubscribe;
}, []);
return {
isConnected,
};
}
Ekran realizacji zakupów następnie podejmuje różne działania w zależności od uzyskanego rezultatu:
function CheckoutScreen() {
const { isConnected } = useNetworkStatus();
if (!isConnected) {
return <OfflineMessage />;
}
return <Checkout />;
}
Ekran nie ma pojęcia o tym, jak działają subskrypcje sieciowe, a właśnie ta ignorancja stanowi całą wartość abstrakcji.
Pozostałe narzędzia z zestawu Hook
Rozmowy kwalifikacyjne na wyższe stanowiska często wykraczają poza pięć najczęściej używanych Hooków, więc warto znać szerszy zakres tych narzędzi.
useContext
Czyta wartość dostarczoną z wyższych poziomów struktury.
const theme = useContext(ThemeContext);
Podoba się do rozwiązywania wspólnych, przekrojowych problemów: tematyki, autoryzacji, lokalizacji i konfiguracji. Nie jest automatycznie biblioteką do zarządzania stanem; każdy użytkownik ponownie renderuje się, gdy zmienia się dostarczona wartość, co ma znaczenie przy częstym aktualizowaniu danych.
useReducer
Gdy kilka elementów stanu zmienia się jednocześnie, oddzielne funkcje ustawiające rozproszają logikę przejścia:
setLoading(...)
setData(...)
setError(...)
Reducer centralizuje tę logikę:
const [state, dispatch] = useReducer(reducer, initialState);
Komponenty opisują to, co się wydarzyło, a reducer decyduje o następnym stanie:
dispatch({
type: "FETCH_SUCCESS",
payload: data,
});
Modelowanie przejść jako nazwanych działań ułatwia przewidywanie i testowanie złożonych stanów.
useLayoutEffect
Ma tę samą strukturę co useEffect, ale jest wykonywany synchronicznie po aktualizacji drzewa przez React i przed narysowaniem przez przeglądarkę, co jest niezbędne, gdy trzeba zmierzyć układ i dokonać korekty, zanim użytkownik zauważy jakikolwiek efekt wizualny. W React Native istnieją podobne przypadki, gdzie timing układu ma znaczenie. Nie jest to szybszy useEffect; ponieważ blokuje proces rysowania, nadmierne jego używanie pogarsza responsywność.
useId
Generuje stabilne, unikalne identyfikatory, głównie do celów związanych z dostępnością, takich jak łączenie etykiety z polem wprowadzania danych. Najlepiej sprawdza się w środowisku internetowym; w React Native, który nie posiada DOM-u przeglądarki, zastosowania są ograniczone.
useImperativeHandle
Umożliwia to komponentowi dokładne określenie tego, co ujawnia przypisany ref, zamiast ujawniania podlegającego mu węzła. Komponent wielokrotnie używalny może na przykład ujawniać funkcję skupiania uwagi:
ref.current?.focus();
lub przewijanie na górę:
ref.current?.scrollToTop();
Taka kontrolowana powierzchnia imperatywna jest szczególnie przydatna w przypadku współdzielonych komponentów React Native.
useSyncExternalStore
Jest to obsługiwany sposób odczytu z magazynu znajdującego się poza React, co zapewnia spójne zrzuty stanu nawet przy jednoczesnym renderowaniu. Jest to rozwiązanie zaawansowane, ale pojawia się w dyskusjach o architekturze i stanowi podstawę dla bibliotek do zarządzania stanem.
useTransition
Oznacza ono niektóre aktualizacje jako nieważne. Koncepcyjnie prace ważne zapewniają responsywność interfejsu:
Urgent update
↓
Keep UI responsive
podczas gdy prace nieważne mogą być planowane z niższym priorytetem:
Non-urgent update
↓
Can be scheduled with lower priority
To ma znaczenie w dużych aplikacjach, gdzie kosztowne ponowne renderowanie konkuruje z pisaniem lub naciskaniem przycisków.
useDeferredValue
Odkłada ono wartość, dzięki czemu pilne interakcje pozostają szybkie. Klasyczny scenariusz:
User typing
↓
Keep input responsive
↓
Defer expensive filtering
Dane wejściowe są aktualizowane natychmiast, podczas gdy intensywne filtrowanie przetwarza odroczoną kopię zapytania.
Zasady Hooków
Dwie zasady zapewniają poprawne działanie Hooków, ponieważ React identyfikuje każdy Hook na podstawie kolejności jego wywołania.
Wywoływaj Hooki na najwyższym poziomie
Nigdy nie umieszczaj Hooka w warunku, pętli ani instrukcji early return:
if (isLoggedIn) {
useEffect(() => {});
}
Zamiast tego umieść warunek wewnątrz Hooka:
useEffect(() => {
if (!isLoggedIn) {
return;
}
i wymień wartość, od której zależy:
// logic
}, [isLoggedIn]);
Wywoływaj Hooki tylko z funkcji React
Hooki należą do komponentów funkcyjnych oraz własnych Hooków, a nie do dowolnych funkcji pomocniczych czy metod klas.
Rozmowa o wydajności
"Ten ekran jest wolny, co byś zrobił?" to często zadawane pytanie. Odpowiedź „dodaj useMemo i useCallback” wskazuje na błędny instynkt. Lepsza odpowiedź:
Pierwszym krokiem jest ustalenie, gdzie faktycznie traci się czas: jak często renderowane są komponenty, które obliczenia są kosztowne, gdzie znajdują się granice komponentów, jak renderowane są listy, jak propagują się aktualizacje stanu oraz jakie zadania związane są z platformą. Następnie należy przeanalizować i naprawić rzeczywisty wąskie gardło.
Wydajność React Native obejmuje więcej niż tylko renderowanie
Koszt ekranu mobilnego jest rozkładany między kilka warstw:
React rendering
↓
JavaScript execution
↓
Native platform work
↓
Layout
↓
Images
↓
Animations
↓
Lists
↓
Network
Zatem gdy ekran w React Native działa nierówno, przyczyną może nie być
useState
lub
useEffect
W ogóle. Może to być intensywna praca JavaScriptu, zbyt wiele renderowań, duże listy, przetwarzanie obrazów, ładowanie animacji, operacje natywne, opóźnienia sieciowe lub subskrypcje stanu, które budzą zbyt wiele komponentów. Umiejętność określenia, gdzie szukać w tych warstwach, odróżnia doświadczonych specjalistów od projektujących aplikacje mobilne.
Duża lista transakcji
Wyobraź sobie aplikację finansową renderującą tysiące transakcji. Prosta wersja przelicza cały proces na każdym renderowaniu:
const visibleTransactions = transactions
.filter(...)
.sort(...)
.map(...);
Memoizacja funkcji filtrowania i sortowania to rozsądny krok:
const visibleTransactions = useMemo(() => {
return transactions
.filter(...)
.sort(...);
}, [transactions]);
Rzadko jest to pełna odpowiedź. Potrzebna jest również wirtualizacja listy, tanie renderowanie elementów, stabilne klucze, komponenty wierszy z pamięcą podręczną, wąskie subskrypcje stanu, rozsądne ładowanie obrazów oraz responsywne interakcje. W React Native dobór FlatList lub FlashList oraz ich prawidłowa konfiguracja zazwyczaj ma znacznie większe znaczenie niż użycie jednego useMemo. Pytanie, które należy zadać, brzmi: gdzie leży wąskie gardło, a nie które Hook należy dodać.
Własność stanu i architektura
Context versus biblioteka do przechowywania danych
Na pytanie „Kiedy użyłbyś Context zamiast Redux lub Zustand?” nie ma jednoznacznej odpowiedzi.
Zależy to od stanu. Kontekst sprawdza się dobrze w przypadku dość stabilnych, wspólnych wartości takich jak temat czy konfiguracja. W przypadku złożonego stanu aplikacji, który jest często aktualizowany i wymaga selektorów, przewidywalnych przejść lub szczegółowych subskrypcji, zazwyczaj lepszym rozwiązaniem jest dedykowana biblioteka do zarządzania stanem.
Kluczowe koncepcje to właściciel danych oraz częstotliwość ich aktualizacji.
Nie wszystko powinno znajdować się w globalnym magazynie
Powszechnym błędem architektonicznym jest umieszczanie wszystkich wartości w jednym magazynie:
Everything
↓
Global store
Lepiej zapytać, kto jest właścicielem każdego elementu stanu. Lokalny stan komponentu obejmuje krótkotrwałe szczegóły interfejsu użytkownika:
isModalOpen
inputValue
selectedTab
Stan serwera to dane pobierane z API; ich lepszym zarządzaniem zajmuje się dedykowana biblioteka do stanu serwera, która obsługuje cacheowanie i ponowne pobieranie danych:
user profile
products
orders
transactions
Globalny stan klienta to niewielki zbiór wartości, które rzeczywiście są wspólne dla wielu ekranów:
theme
authentication state
application preferences
W sieci niektórych danych stanu należy umieścić w URL, aby przetrwały ponowne załadowanie i mogły być udostępniane jako link:
filters
search
pagination
Zasada do pamiętania: stan powinien znajdować się w najwęższym możliwym zakresie, który ma sens.
Kombinowanie tego na ekranie inwestycyjnym
Załóżmy ekran inwestycyjny w React Native, który wymaga danych rynkowych, wybranego symbolu, danych z wykresu, danych portfela, obsługi AppState, możliwości przewijania oraz kosztownej transformacji wykresu. Uproszczona struktura:
InvestmentScreen
│
├── useMarketData()
│
├── usePortfolio()
│
├── useAppState()
│
├── useRef()
│
├── useMemo()
│
└── useCallback()
│
▼
UI
Poniżej znajduje się zarys komponentu. Należy zwrócić uwagę na dwie kwestie: fragment kończy się kilkoma linijami prozy oraz separatorami, które nie stanowią ważnego kodu, więc należy go traktować jako pseudokod; ponadto funkcja setSymbol z useState jest już stabilna, więc użycie otoczki useCallback jest opcjonalne – można bezpośrednio przekazać funkcję ustawiającą.
function InvestmentScreen() {
const [symbol, setSymbol] = useState("AAPL");
const scrollRef = useRef<ScrollView>(null);
const { data: marketData } = useMarketData(symbol);
const { portfolio } = usePortfolio();
const chartData = useMemo(() => {
return transformMarketData(marketData);
}, [marketData]);
const handleSymbolChange = useCallback(
(nextSymbol: string) => {
setSymbol(nextSymbol);
},
[]
);
return (
<ScrollView ref={scrollRef}>
<MarketHeader
symbol={symbol}
onSymbolChange={handleSymbolChange}
/>
<PriceChart data={chartData} />
<PortfolioSummary
portfolio={portfolio}
/>
</ScrollView>
);
}
---
Now each concept has a clear responsibility.
---
Każdy Hook ma teraz jedną, jasno określoną funkcję:
useState
→ selected symboluseRef
→ imperative ScrollView accessuseMemo
→ expensive chart transformationuseCallback
→ stable handler where usefulCustom Hooks
→ market and portfolio behaviouruseEffect
→ internal synchronization within those Hooks
Efekty pozostają wewnątrz dostosowanych Hooków, gdzie synchronizują się z strumieniem danych rynkowych oraz cyklem życia aplikacji, a sama ekranika przypomina opis swoich danych.
Odpowiadanie jak doświadczony inżynier
Pójść o krok dalej niż to pytanie
Częstym błędem jest odpowiadanie tylko na to, o co zostało zapytane. Na pytanie „co to jest useMemo?” odpowiedź „memoizuje wartość” jest poprawna, ale łatwa do zapomnienia. Pełniejsza odpowiedź:
useMemo przechowuje w pamięci przeliczoną wartość na podstawie jej zależności. Nie jest funkcją domyślną, ponieważ wiąże się z dodatkowymi kosztami i zwiększa złożoność. Ma sens, gdy drogie obliczenia są wykonywane niepotrzebnie lub gdy ważny jest stabilny referencja, najlepiej po potwierdzeniu wąskiego gardła przez analizę wydajności.
W zaledwie kilku zdaniach pokazałeś znajomość API, świadomość wydajności, umiejętność oceny kompromisów oraz zdolność do podejmowania decyzji.
Struktura odpowiedzi w pięciu krokach
Niezawodny szablon na każde pytanie dotyczące Hooków:
1. What is it?
2. Why does it exist?
3. Real example
4. Trade-off / pitfall
5. React Native example when relevant
Zastosowane do useRef:
- Co: trwała, zmienna wartość, która nie powoduje ponownego renderowania.
- Dlaczego: dla danych, które muszą przetrwać renderowanie, ale nie są częścią interfejsu użytkownika.
- Przykład: identyfikator timera lub identyfikator komponentu imperatywnego.
- Kompromis: ponieważ jego aktualizacja nie powoduje renderowania, nie może przechowywać stanu interfejsu użytkownika.
Kwestie do przygotowania
W przypadku useState bądź gotowy omówić funkcje aktualizacji oraz grupowanie kilku wywołań, dlaczego modyfikacja zwykłej zmiennej nie zmienia ekranu, co powoduje ponowną renderizację, gdzie powinna znajdować się dana wartość stanu oraz jak unikać stanu pochodnego.
W przypadku useEffect spodziewaj się pytań dotyczących roli tablicy zależności, przestarzałych zamknięć funkcji, wielokrotnego wykonywania efektów, do czego służy funkcja czyszczenia, anulowania trwających operacji asynchronicznych, sytuacji, w których efekt jest niewłaściwym narzędziem, oraz podwójnego wywołania w trybie Strict Mode podczas rozwoju.
W przypadku useRef przygotuj się, by wyjaśnić, dlaczego zapisywanie do .current nie powoduje renderizacji, w jakich sytuacjach ref jest lepszy od stanu, jak refs są przypisywane do elementów DOM lub widoków natywnych oraz co oznacza imperatywna API.
Dla useMemo należy jasno określić, czy zawsze przyspiesza działanie aplikacji, w jaki sposób różni się od useCallback, co oznacza równość referencyjna, kiedy usuwanie tego mechanizmu jest właściwym rozwiązaniem oraz jak udowodnić, że przyniosło to korzyść.
Dla useCallback trzeba znać powody, dla których tożsamość funkcji może mieć znaczenie, w jaki sposób łączy się ona z React.memo, kiedy nie dodaje żadnej wartości, czy może spowolnić działanie aplikacji oraz co powinno znaleźć się na liście zależności.
Ćwiczenie z debugowaniem
Pracodawcy lubią dostawać komponenty z błędami. Oto taki przykład zawierający kilka problemów.
function UserList({ users }) {
const [search, setSearch] = useState("");
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.includes(search)
);
}, [users]);
useEffect(() => {
console.log("Search:", search);
}, []);
return (
<List
data={filteredUsers}
renderItem={({ item }) => (
<UserRow
user={item}
onPress={() => selectUser(item.id)}
/>
)}
/>
);
}
Brak zależności w useMemo
Memo jest tworzone tylko na podstawie użytkowników:
}, [users]);
ale filtr również odczytuje dane
search
dlatego lista nigdy się nie aktualizuje podczas pisania przez użytkownika. Zależności muszą obejmować oba te elementy:
}, [users, search]);
Zasada lintingu natychmiast by to zauważyła.
Efekt zamrożony na pierwszej wartości
Ten efekt ma pusty tablicę:
useEffect(() => {
console.log(search);
}, []);
dlatego jest wykonywany dopiero po montowaniu i zapisuje początkowe search. Jeśli celem jest rejestrowanie każdej zmiany, należy od niego zależeć:
useEffect(() => {
console.log(search);
}, [search]);
Wewnętrzna funkcja zwrotna, obsługiwana z rozwagą
Obsługa wiersza
onPress={() => selectUser(item.id)}
jest nową funkcją przy każdym renderowaniu. To samo w sobie nie jest błędem. Najpierw należy ustalić, czy
UserRow
jest kosztowne i czy rzeczywiście powoduje niepotrzebne ponowne renderowanie. Tylko na podstawie tych dowodów można by zastosować
useCallback
lub przebudować komponenty, na przykład poprzez przekazywanie id oraz stabilnej funkcji obsługi do wiersza. Pokazanie takiej umiaru jest częścią oczekiwanego rozwiązania.
Dobre, solidne i zaawansowane odpowiedzi
Progresję tę łatwo zapamiętać. Dobry programista zna useCallback. Silny programista wie, kiedy go używać. Doświadczony programista wie, kiedy go nie używać, i potrafi wyjaśnić związane z tym kompromisy. Ta sama struktura dotyczy każdego Hooka. Celem nigdy nie jest
Maximum Hooks
lecz raczej
Correct abstraction
+
Correct data flow
+
Correct performance trade-offs
Jedna zbiór koncepcji dla środowiska web i natywnego
Nie potrzebujesz odrębnej wiedzy na temat Hooków dla React i React Native. W środowisku web przykłady zazwyczaj dotyczą
DOM
browser events
Web APIs
WebSockets
browser storage
podczas gdy dyskusje o urządzeniach mobilnych dotyczą
AppState
Keyboard
native modules
device APIs
push notifications
network state
native event subscriptions
Mimo to podstawowe narzędzia są takie same w obu przypadkach – React.
useState
useEffect
useRef
useMemo
useCallback
useContext
useReducer
Custom Hooks
closures
dependency arrays
Dlatego najpierw wyjaśnij koncepcję React, a następnie pokaż, jak ją zastosować na danym platformie. Dzięki temu będziesz wiarygodny zarówno w rolach związanych ze środowiskiem web, mobilnym, jak i hybrydowym.
Szybka tabela referencyjna
useState
Określa to, co powoduje ponowną renderizację po aktualizacji. Typowe zastosowania:
form values
selected tabs
modal visibility
loading state
local UI state
useEffect
Synchronizacja z systemami zewnętrznymi. Typowe zastosowania:
subscriptions
timers
WebSockets
AppState
native events
network operations
Unikaj tego w przypadku wartości, które można obliczyć podczas renderizacji.
useRef
Trwała, zmienne wartość, która nie powoduje renderizacji. Typowe zastosowania:
DOM/native refs
timers
previous values
imperative APIs
mutable non-UI data
useMemo
Wartość obliczona i zapisana w pamięci. Używaj jej, gdy
calculation is expensive
or
referential identity matters
najpierw sprawdzisz jej wartość.
useCallback
Referencja funkcji zapisana w pamięci. Warto to robić, gdy
function identity matters
React.memo is involved
another Hook depends on function identity
Nie dodawaj jej automatycznie.
useContext
Czyta udostępnione dane kontekstowe, takie jak:
theme
locale
authentication
configuration
useReducer
Modeluje złożone przejścia. Przydatne, gdy masz
many related state variables
complex transitions
state machine-like behaviour
Custom Hooks
Powtarzalne zachowania związane ze stanem, na przykład:
useAuth()
useNetworkStatus()
useAppState()
usePagination()
useKeyboard()
useUserProfile()
Model mentalny zamiast glosariusza
Zamiast zapamiętywać jednowyrazową definicję dla każdego Hooka,
useState = state
useEffect = side effect
useRef = reference
useMemo = memoisation
useCallback = callback
wyobraź sobie, jak one wpływają na renderowanie: stan interfejsu przepływa przez useState, co powoduje ponowne renderowanie, wartości pochodne są generowane za pomocą useMemo, zmienne dane niebędące częścią interfejsu przechowywane są w useRef, a useEffect łączy renderowany interfejs z API, zdarzeniami i systemami natywnymi, przy czym wszystko to jest otoczone specjalnymi Hookami.
React Render
│
┌──────────┼──────────┐
│ │ │
UI State Derived Mutable
│ Values Data
│ │ │
useState useMemo useRef
│
▼
Re-render
│
▼
UI
│
▼
External Systems
│
▼
useEffect
│
▼
APIs / Events / Native
│
▼
Custom Hooks
Pod tym wszystkim kryje się kilka podstawowych zasad:
Rendering
+
Closures
+
Referential Equality
+
Data Flow
+
Performance
Gdy te zasady będą dobrze poznane, poszczególne Hooki staną się proste w użyciu.
Pytania do zadawania podczas każdej oceny kodu
Hooki to narzędzia do zarządzania różnymi rodzajami zachowań w aplikacjach reaktywnych, a kluczową umiejętnością jest wiedza o tym, dlaczego używamy każdego z nich. Te pytania sprawdzają się zarówno podczas ocen kodu, jak i rozmów kwalifikacyjnych:
- Czy ten stan musi istnieć, czy można go wygenerować?
useState?Podsumowanie
Hooks stają się prostsze, gdy przestaniesz traktować je jako odrębne API i zobaczysz w nich etapy jednego procesu:
Rendering
↓
State
↓
Closures
↓
Effects
↓
References
↓
Memoisation
↓
Custom Hooks
↓
Performance
↓
Architecture
Biorąc to pod uwagę, przechodzenie między React Web a React Native staje się o wiele mniej przerażające. Gdy ktoś pyta, dlaczego użyłeś useMemo w konkretnym miejscu, pomiń tradycyjną definicję i opowiedz historię: problem, który zaobserwowałeś, co go spowodowało, dlaczego ten Hook się nadaje, jakie kompromisy rozważyłeś oraz w jaki sposób oceniłeś rezultat.
Przed rozmową kwalifikacyjną upewnij się, że potrafisz swobodnie mówić o:
- Zasadach podstawowych: przyczynach renderowania, czym jest zrzut stanu renderowania, różnicach między stanem a zwykłą zmienną, zamknięciach funkcji, równości referencyjnej oraz procesie synchronizacji.
useContext i useReducer, specjalistyczne (useLayoutEffect, useId, useImperativeHandle, useSyncExternalStore, useTransition, useDeferredValue) oraz sposoby projektowania własnych Hooków.React.memo porównuje propsy, koszt memozacji, profilowanie renderów, długie listy, intensywne obliczenia oraz zbyt szerokie subskrypcje magazynu.Celem nie jest zapamiętanie odpowiedzi na wszystko, lecz wystarczająca wiedza, by móc rozwiązywać nieznane problemy podczas rozmowy kwalifikacyjnej.
Literatura pokrewna
- Trade-offy w React Native na dużą skalę: stan, listy, urządzenia, tokeny, aktualizacje — Pięć problemów w React Native, które ujawniają potrzebę rozsądnego podejmowania decyzji inżynieryjnych: stan serwera w porównaniu ze stanem klienta, wolne FlatLists, urządzenia o niskich specyfikacjach, przechowywanie tokenów oraz aktualizacje stopniowe.
- Jak React-Redux decyduje o ponownym przerysowaniu: selectorzy, równość i hooki z typeami — Przewodnik po mechanizmach wewnętrznych React-Redux: Provider, zasada równości useSelector, selectorzy zapisane w pamięci, funkcja connect(), hooki z typeami przy użyciu withTypes() oraz rzadkie przypadki krawędziowe.