15 Najtrudniejszych Pytań Rekrutacyjnych z React
Po przeprowadzeniu setek rozmów rekrutacyjnych i byciu po obu stronach stołu, wybrałem 15 pytań, które naprawdę weryfikują głębokość zrozumienia Reacta. To nie są pytania na które odpowiesz cytując dokumentację - wymagają praktycznego doświadczenia i zrozumienia mechanizmów działających pod maską.
1. Virtual DOM i Rekoncyliacja
Odpowiedź w 30 sekund
Virtual DOM to wirtualna reprezentacja interfejsu przechowywana w pamięci. React porównuje ją z prawdziwym DOM w procesie rekoncyliacji i aktualizuje tylko zmienione elementy. Dzięki temu manipulacje DOM są szybsze i efektywniejsze niż bezpośrednia modyfikacja.
Odpowiedź w 2 minuty
Kiedy rekruter pyta o Virtual DOM, tak naprawdę sprawdza czy rozumiesz dlaczego React jest szybki. Wyobraź sobie, że masz listę 1000 elementów i jeden się zmienia. W tradycyjnym podejściu musiałbyś przejść przez całą listę w prawdziwym DOM - operacja kosztowna, bo każda interakcja z DOM powoduje reflow i repaint przeglądarki.
React rozwiązuje to elegancko: tworzy lekką kopię DOM w pamięci JavaScript. Gdy stan się zmienia, React buduje nowy Virtual DOM, porównuje go ze starym (proces zwany diffing) i oblicza minimalny zestaw zmian potrzebnych do aktualizacji prawdziwego DOM.
Tu robi się ciekawie: React nie aktualizuje każdej zmiany osobno. Grupuje je w batche i wykonuje jedną operację na DOM. To jak różnica między wysyłaniem 100 osobnych SMS-ów a jednego zbiorczego maila.
Rekoncyliacja to cały algorytm odpowiedzialny za ten proces. React używa heurystyk, aby porównywanie było szybkie - na przykład zakłada, że elementy o różnych typach generują różne drzewa. Dlatego klucze w listach są tak ważne - pozwalają Reactowi identyfikować które elementy się zmieniły, dodały lub usunęły.
// Bez klucza React musi porównać każdy element
// Z kluczem wie dokładnie który element się zmienił
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
2. Różnica między useState a useReducer
Odpowiedź w 30 sekund
useState jest prosty - jeden stan, jedna funkcja do aktualizacji. useReducer działa jak mini-Redux - przyjmuje reducer i akcje, świetny gdy masz złożony stan z wieloma powiązanymi wartościami lub skomplikowaną logiką aktualizacji.
Odpowiedź w 2 minuty
Wzorzec, który mi się sprawdził: jeśli masz 2-3 niezależne wartości stanu, useState wystarczy. Ale gdy te wartości zaczynają od siebie zależeć, lub logika aktualizacji staje się złożona - sięgnij po useReducer.
Pokażę na przykładzie formularza rejestracji:
// useState - proste, ale może się skomplikować
function RegistrationForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// Każda walidacja wymaga dostępu do wielu stanów
// Łatwo o błędy i niespójności
}
// useReducer - lepsza kontrola nad złożonym stanem
const initialState = {
email: '',
password: '',
confirmPassword: '',
errors: {},
isSubmitting: false
};
function formReducer(state, action) {
switch (action.type) {
case 'UPDATE_FIELD':
return {
...state,
[action.field]: action.value,
errors: { ...state.errors, [action.field]: null }
};
case 'SUBMIT_START':
return { ...state, isSubmitting: true, errors: {} };
case 'SUBMIT_ERROR':
return { ...state, isSubmitting: false, errors: action.errors };
case 'SUBMIT_SUCCESS':
return { ...initialState };
default:
return state;
}
}
function RegistrationForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
// Każda akcja ma jasną intencję
// Stan jest zawsze spójny
dispatch({ type: 'UPDATE_FIELD', field: 'email', value: 'test@example.com' });
}
Dodatkowa zaleta useReducer: możesz wynieść reducer poza komponent, co ułatwia testowanie. Reducer to czysta funkcja - dla tych samych argumentów zawsze zwraca ten sam wynik.
3. Prop Drilling i Context API
Odpowiedź w 30 sekund
Prop drilling to przekazywanie danych przez wiele poziomów komponentów, które same tych danych nie potrzebują. Rozwiązaniem jest React Context - pozwala przekazać dane bezpośrednio do komponentów, które ich potrzebują, bez pośredników.
Odpowiedź w 2 minuty
Prop drilling sam w sobie nie jest zły - dla 2-3 poziomów to najprostsza i najbardziej czytelna metoda. Problem zaczyna się gdy masz głęboką hierarchię i przekazujesz props przez 5-10 komponentów, które są tylko "listonoszami".
// Klasyczny prop drilling - App zna theme, ale Button go potrzebuje
function App() {
const [theme, setTheme] = useState('dark');
return <Toolbar theme={theme} />;
}
function Toolbar({ theme }) {
// Toolbar nie używa theme, tylko przekazuje dalej
return <ThemedButton theme={theme} />;
}
function ThemedButton({ theme }) {
return <button className={theme}>Click</button>;
}
// Rozwiązanie z Context
const ThemeContext = React.createContext('light');
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
// Toolbar nie musi wiedzieć o theme
return <ThemedButton />;
}
function ThemedButton() {
// Pobiera theme bezpośrednio z kontekstu
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
Kandydaci, którzy robią wrażenie to ci, którzy znają też wady Context: każda zmiana wartości kontekstu powoduje re-render wszystkich konsumentów. Dla często zmieniających się wartości lepszym wyborem może być biblioteka jak Zustand czy Jotai.
4. useEffect - Pułapki i Dobre Praktyki
Odpowiedź w 30 sekund
useEffect to hook do obsługi efektów ubocznych - fetching danych, subskrypcje, manipulacje DOM. Kluczowa jest tablica zależności: pusta oznacza uruchomienie raz przy montowaniu, brak tablicy oznacza uruchomienie przy każdym renderze.
Odpowiedź w 2 minuty
useEffect to prawdopodobnie hook, który powoduje najwięcej bugów w aplikacjach React. Typowy błąd to pominięcie zależności lub dodanie zbyt wielu.
// Klasyczny problem - nieskończona pętla
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ZŁE - brak userId w zależnościach
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Nigdy nie pobierze nowego usera przy zmianie userId
// ZŁE - obiekt w zależnościach
useEffect(() => {
fetchUser(userId).then(setUser);
}, [{ userId }]); // Nieskończona pętla! Nowy obiekt przy każdym renderze
// DOBRZE
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Pobiera przy zmianie userId
}
Wzorzec, który mi się sprawdził przy operacjach asynchronicznych - obsługa race condition:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let cancelled = false;
async function search() {
const data = await fetchResults(query);
// Jeśli komponent został odmontowany lub query się zmienił
// nie aktualizujemy stanu
if (!cancelled) {
setResults(data);
}
}
search();
// Funkcja czyszcząca - uruchamiana przy odmontowaniu
// lub przed następnym wywołaniem efektu
return () => {
cancelled = true;
};
}, [query]);
return <ResultList items={results} />;
}
5. useMemo vs useCallback - Kiedy Które?
Odpowiedź w 30 sekund
useMemo zapamiętuje wynik obliczeń, useCallback zapamiętuje samą funkcję. useMemo używaj dla kosztownych kalkulacji, useCallback dla funkcji przekazywanych do zoptymalizowanych komponentów dziecka.
Odpowiedź w 2 minuty
Wielu developerów wpada w pułapkę przedwczesnej optymalizacji - opakowuje wszystko w useMemo i useCallback. To anty-wzorzec, bo te hooki też mają koszt: dodatkowe wywołania funkcji i porównywanie zależności.
Pokażę kiedy naprawdę warto:
// useMemo - kosztowne obliczenia
function DataGrid({ items, filter }) {
// BEZ useMemo - filtrowanie przy każdym renderze
const filteredItems = items.filter(item =>
item.name.includes(filter)
);
// Z useMemo - filtrowanie tylko gdy items lub filter się zmieni
const filteredItems = useMemo(() =>
items.filter(item => item.name.includes(filter)),
[items, filter]
);
return <Table data={filteredItems} />;
}
// useCallback - stabilna referencja funkcji
function ParentComponent() {
const [count, setCount] = useState(0);
// BEZ useCallback - nowa funkcja przy każdym renderze
// ExpensiveChild się re-renderuje nawet z React.memo
const handleClick = () => {
console.log('clicked');
};
// Z useCallback - ta sama referencja między renderami
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // Puste zależności = funkcja się nie zmienia
return <ExpensiveChild onClick={handleClick} />;
}
const ExpensiveChild = React.memo(({ onClick }) => {
// Ciężki komponent - nie chcemy zbędnych re-renderów
return <ComplexVisualization onClick={onClick} />;
});
Zasada kciuka: useCallback ma sens tylko gdy przekazujesz funkcję do komponentu opakowanego w React.memo() lub gdy funkcja jest zależnością w useEffect.
6. Higher Order Components (HOC)
Odpowiedź w 30 sekund
HOC to funkcja przyjmująca komponent i zwracająca nowy komponent z rozszerzoną funkcjonalnością. To wzorzec do współdzielenia logiki - na przykład jeden HOC do autoryzacji może chronić wiele różnych stron.
Odpowiedź w 2 minuty
HOC to wzorzec z czasów przed hookami, ale nadal ma swoje zastosowania. Koncepcja jest prosta: funkcja bierze komponent, dodaje mu coś (props, logikę, wrapper) i zwraca nowy komponent.
// HOC do dodania logowania aktywności użytkownika
function withActivityLogging(WrappedComponent) {
return function WithActivityLogging(props) {
useEffect(() => {
logActivity(`Viewed: ${WrappedComponent.name}`);
}, []);
const handleClick = (event) => {
logActivity(`Clicked in: ${WrappedComponent.name}`);
props.onClick?.(event);
};
return <WrappedComponent {...props} onClick={handleClick} />;
};
}
// HOC do autoryzacji
function withAuth(WrappedComponent, requiredRole) {
return function WithAuth(props) {
const { user, isLoading } = useAuth();
if (isLoading) return <Spinner />;
if (!user) return <Navigate to="/login" />;
if (requiredRole && user.role !== requiredRole) {
return <AccessDenied />;
}
return <WrappedComponent {...props} user={user} />;
};
}
// Użycie
const ProtectedDashboard = withAuth(Dashboard, 'admin');
const LoggedUserProfile = withActivityLogging(UserProfile);
Wady HOC: wrapper hell (wiele zagnieżdżonych HOC), kolizje nazw props, trudniejsze debugowanie. Dlatego dla nowej logiki preferuję custom hooks, ale HOC nadal spotykasz w starszych kodach i bibliotekach.
7. Error Boundaries
Odpowiedź w 30 sekund
Error Boundaries to komponenty wyłapujące błędy JavaScript w drzewie komponentów poniżej nich. Pozwalają wyświetlić fallback UI zamiast crashować całą aplikację. Aktualnie dostępne tylko jako komponenty klasowe.
Odpowiedź w 2 minuty
To pytanie często pada jako: "Jak obsługujesz błędy w React?". Jeśli odpowiesz tylko o try-catch, tracisz punkty - try-catch nie łapie błędów w renderowaniu i lifecycle methods.
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
// Wywoływane przy błędzie - aktualizuje stan
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// Wywoływane po błędzie - logowanie, raportowanie
componentDidCatch(error, errorInfo) {
// Wyślij do Sentry, LogRocket itp.
errorReportingService.log({
error,
componentStack: errorInfo.componentStack
});
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Coś poszło nie tak</h2>
<button onClick={() => this.setState({ hasError: false })}>
Spróbuj ponownie
</button>
</div>
);
}
return this.props.children;
}
}
// Użycie - strategiczne rozmieszczenie
function App() {
return (
<ErrorBoundary>
<Header />
<ErrorBoundary>
{/* Błąd w głównej treści nie zabije nawigacji */}
<MainContent />
</ErrorBoundary>
<Footer />
</ErrorBoundary>
);
}
Error Boundaries nie łapią błędów w: event handlers (użyj try-catch), asynchronicznym kodzie (użyj .catch()), SSR, oraz w samym Error Boundary. Kandydaci którzy to wiedzą pokazują głębokie zrozumienie.
8. React.memo, PureComponent i Optymalizacja Renderowania
Odpowiedź w 30 sekund
React.memo dla komponentów funkcyjnych i PureComponent dla klasowych - oba zapobiegają re-renderowaniu gdy props się nie zmieniły. Używają płytkiego porównania, więc dla złożonych obiektów możesz podać własną funkcję porównującą.
Odpowiedź w 2 minuty
Największy mit: "React.memo zawsze przyspiesza aplikację". W rzeczywistości porównywanie props też kosztuje. Dla prostych komponentów renderujących szybko, dodanie memo może być wolniejsze niż po prostu re-renderowanie.
// ZBĘDNE memo - komponent jest trywialny
const Badge = React.memo(({ label }) => (
<span className="badge">{label}</span>
));
// SENSOWNE memo - komponent jest złożony
const DataVisualization = React.memo(({ data, config }) => {
// Ciężkie obliczenia i renderowanie
const processedData = complexCalculation(data);
return <Chart data={processedData} {...config} />;
});
// Custom comparison dla głębokich obiektów
const UserCard = React.memo(
({ user }) => (
<div>
<img src={user.avatar} />
<h3>{user.name}</h3>
</div>
),
(prevProps, nextProps) => {
// Zwróć true jeśli props są "równe" (nie re-renderuj)
return prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name;
}
);
Praktyczna zasada: profiluj najpierw, optymalizuj potem. React DevTools Profiler pokaże ci które komponenty renderują się najczęściej i najdłużej.
9. Controlled vs Uncontrolled Components
Odpowiedź w 30 sekund
Controlled component ma wartość zarządzaną przez React (przez state), uncontrolled trzyma wartość w DOM i odczytujemy ją przez ref. Controlled daje pełną kontrolę i walidację, uncontrolled jest prostszy dla formularzy gdzie nie potrzebujesz reagować na każdą zmianę.
Odpowiedź w 2 minuty
To pytanie często prowadzi do dyskusji o formularzach. Każde podejście ma swoje miejsce.
// CONTROLLED - pełna kontrola nad każdą zmianą
function ControlledForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const value = e.target.value;
setEmail(value);
// Walidacja w czasie rzeczywistym
if (!value.includes('@')) {
setError('Nieprawidłowy email');
} else {
setError('');
}
};
return (
<form>
<input
type="email"
value={email}
onChange={handleChange}
/>
{error && <span className="error">{error}</span>}
</form>
);
}
// UNCONTROLLED - prostsze, wartość w DOM
function UncontrolledForm() {
const emailRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
// Odczytujemy wartość tylko przy submit
const email = emailRef.current.value;
submitForm({ email });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
ref={emailRef}
defaultValue="user@example.com"
/>
<button type="submit">Wyślij</button>
</form>
);
}
Kiedy które? Controlled gdy: potrzebujesz walidacji w czasie rzeczywistym, formatowania inputu (np. numer telefonu), lub gdy wartość zależy od innych pól. Uncontrolled gdy: masz prosty formularz, integrujesz z biblioteką nie-Reactową, lub zależy ci na wydajności (mniej re-renderów).
10. Lazy Loading i Code Splitting
Odpowiedź w 30 sekund
React.lazy pozwala ładować komponenty dynamicznie - użytkownik pobiera kod dopiero gdy go potrzebuje. Suspense wyświetla fallback podczas ładowania. Kluczowe dla wydajności dużych aplikacji.
Odpowiedź w 2 minuty
Wyobraź sobie aplikację z panelem admina, którego 90% użytkowników nigdy nie zobaczy. Bez lazy loading, każdy użytkownik pobiera ten kod. Z lazy loading - tylko admini.
// Dynamiczny import - kod ładowany on-demand
const AdminPanel = React.lazy(() => import('./AdminPanel'));
const UserSettings = React.lazy(() => import('./UserSettings'));
const Reports = React.lazy(() => import('./Reports'));
function App() {
return (
<Router>
{/* Suspense opakowuje lazy komponenty */}
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/admin" element={<AdminPanel />} />
<Route path="/settings" element={<UserSettings />} />
<Route path="/reports" element={<Reports />} />
{/* Route bez lazy - zawsze w głównym bundle */}
<Route path="/" element={<Home />} />
</Routes>
</Suspense>
</Router>
);
}
// Zaawansowane - preloading przy hover
function Navigation() {
const preloadAdmin = () => {
// Rozpocznij ładowanie przed kliknięciem
import('./AdminPanel');
};
return (
<nav>
<Link to="/">Home</Link>
<Link
to="/admin"
onMouseEnter={preloadAdmin}
>
Admin
</Link>
</nav>
);
}
Efekt: zamiast jednego bundle'a 2MB, masz główny bundle 500KB i kilka mniejszych ładowanych w razie potrzeby. Użytkownicy widzą aplikację szybciej.
11. useRef - Więcej niż Dostęp do DOM
Odpowiedź w 30 sekund
useRef tworzy mutowalny obiekt persystowany przez cały cykl życia komponentu. Służy do dostępu do DOM, ale też do przechowywania dowolnych wartości bez powodowania re-renderu przy ich zmianie.
Odpowiedź w 2 minuty
Wielu developerów zna useRef tylko jako "sposób na dostęp do DOM". Ale to znacznie więcej - to kontener na dowolną wartość, która przeżyje re-rendery bez ich wywoływania.
// Klasyczne użycie - dostęp do DOM
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return <input ref={inputRef} />;
}
// Zaawansowane - przechowywanie poprzedniej wartości
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current; // Zwraca wartość sprzed renderuj
}
// Użycie
function PriceDisplay({ price }) {
const previousPrice = usePrevious(price);
return (
<div>
<span>Aktualna: {price} zł</span>
{previousPrice && previousPrice !== price && (
<span className={price > previousPrice ? 'up' : 'down'}>
(poprzednio: {previousPrice} zł)
</span>
)}
</div>
);
}
// Zaawansowane - timer bez re-renderów
function Stopwatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setTime(t => t + 1);
}, 1000);
};
const stop = () => {
// Dostęp do intervalu bez re-renderu
clearInterval(intervalRef.current);
};
return (
<div>
<span>{time}s</span>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
Kluczowa różnica: zmiana ref.current NIE powoduje re-renderu. To czyni useRef idealnym dla wartości, które zmieniasz często, ale nie chcesz re-renderować komponentu.
12. Custom Hooks - Wzorce i Pułapki
Odpowiedź w 30 sekund
Custom hooks to funkcje zaczynające się od "use", które mogą używać innych hooków. Pozwalają wyodrębnić i współdzielić logikę stanową między komponentami. Każde użycie hooka tworzy niezależny stan.
Odpowiedź w 2 minuty
Custom hooks to najpotężniejszy wzorzec do reużycia logiki w React. Ale są pułapki, które rekruterzy lubią eksplorować.
// Hook do obsługi API z cache i loading state
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setData(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// Hook do localStorage z automatyczną synchronizacją
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function
? value(storedValue)
: value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Użycie
function UserPreferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const { data: user, loading } = useApi('/api/user');
// Każdy komponent używający useLocalStorage('theme', ...)
// ma WŁASNY stan, ale synchronizowany z localStorage
}
Pułapka: custom hooks nie współdzielą stanu między komponentami automatycznie. Każde wywołanie hooka tworzy nowy, niezależny stan. Jeśli potrzebujesz globalnego stanu, użyj Context w połączeniu z hookiem.
13. Concurrent Features - useTransition i useDeferredValue
Odpowiedź w 30 sekund
useTransition pozwala oznaczyć aktualizacje jako nieurgentne - React może je przerwać dla ważniejszych interakcji. useDeferredValue odracza aktualizację wartości do momentu gdy przeglądarka ma czas. Oba poprawiają responsywność UI.
Odpowiedź w 2 minuty
To nowsze API z React 18, które rozwiązuje problem: "dlaczego moja aplikacja laguje podczas ciężkich operacji?". Tradycyjnie wszystkie aktualizacje stanu były równie ważne. Teraz możesz priorytetyzować.
// useTransition - filtrowanie długiej listy
function FilteredList({ items }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
// Aktualizacja inputa - PILNA, natychmiast
setQuery(value);
// Filtrowanie listy - może poczekać
// Jeśli user szybko pisze, poprzednie renderowania są anulowane
startTransition(() => {
setFilteredItems(
items.filter(item => item.includes(value))
);
});
};
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<List items={filteredItems} />
</>
);
}
// useDeferredValue - podobny efekt, prostsze API
function SearchResults({ query }) {
// Odroczenie wartości query
const deferredQuery = useDeferredValue(query);
// Komponent używa starej wartości podczas wpisywania
// aktualizuje się gdy przeglądarka ma czas
const results = useMemo(
() => searchItems(deferredQuery),
[deferredQuery]
);
// Pokazuje że wyniki są "nieaktualne"
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<ResultList items={results} />
</div>
);
}
Różnica: useTransition gdy kontrolujesz setState, useDeferredValue gdy otrzymujesz wartość z zewnątrz (props). Oba sprawiają, że UI pozostaje responsywny nawet przy ciężkich operacjach.
14. Server Components i RSC
Odpowiedź w 30 sekund
React Server Components to komponenty renderowane na serwerze, których kod JavaScript nigdy nie trafia do przeglądarki. Mają dostęp do bazy danych i systemu plików. Klient otrzymuje gotowy HTML i tylko interaktywny JS dla komponentów klienckich.
Odpowiedź w 2 minuty
To najnowszy kierunek rozwoju Reacta, zaimplementowany w Next.js 13+. Zmienia sposób myślenia o aplikacjach.
// Server Component - NIE trafia do bundle'a klienta
// Może bezpośrednio czytać z bazy danych
async function ProductList() {
// To działa tylko na serwerze!
const products = await db.query('SELECT * FROM products');
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{/* AddToCart to Client Component - interaktywny */}
<AddToCartButton productId={product.id} />
</li>
))}
</ul>
);
}
// Client Component - oznaczony dyrektywą
'use client'
function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
await addToCart(productId);
setLoading(false);
};
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Dodawanie...' : 'Dodaj do koszyka'}
</button>
);
}
Korzyści: mniejsze bundle'e (kod serwera nie trafia do klienta), bezpośredni dostęp do danych (bez API), lepszy SEO. Ograniczenia: Server Components nie mogą używać useState, useEffect, ani event handlers - do tego potrzebujesz Client Components.
15. Testowanie Komponentów React
Odpowiedź w 30 sekund
React Testing Library to standard - testuje zachowanie komponentów z perspektywy użytkownika, nie implementację. Szukasz elementów jak użytkownik (przez tekst, role, label), symulujesz interakcje (click, type), sprawdzasz wynik.
Odpowiedź w 2 minuty
Pytanie o testowanie ujawnia doświadczenie produkcyjne. Dobry developer wie co i jak testować.
// Komponent do przetestowania
function LoginForm({ onSubmit }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
if (!email.includes('@')) {
setError('Nieprawidłowy email');
return;
}
await onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="Email"
value={email}
onChange={e => setEmail(e.target.value)}
aria-label="Email"
/>
<input
type="password"
placeholder="Hasło"
value={password}
onChange={e => setPassword(e.target.value)}
aria-label="Hasło"
/>
{error && <span role="alert">{error}</span>}
<button type="submit">Zaloguj</button>
</form>
);
}
// Testy - z perspektywy użytkownika
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('LoginForm', () => {
it('wyświetla błąd przy nieprawidłowym emailu', async () => {
render(<LoginForm onSubmit={jest.fn()} />);
// Wpisz nieprawidłowy email
await userEvent.type(
screen.getByLabelText('Email'),
'nieprawidlowy'
);
await userEvent.type(
screen.getByLabelText('Hasło'),
'haslo123'
);
// Kliknij submit
await userEvent.click(screen.getByRole('button', { name: 'Zaloguj' }));
// Sprawdź czy pojawił się błąd
expect(screen.getByRole('alert')).toHaveTextContent('Nieprawidłowy email');
});
it('wywołuje onSubmit z poprawnymi danymi', async () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await userEvent.type(screen.getByLabelText('Email'), 'test@example.com');
await userEvent.type(screen.getByLabelText('Hasło'), 'haslo123');
await userEvent.click(screen.getByRole('button', { name: 'Zaloguj' }));
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'haslo123'
});
});
});
});
Kluczowe zasady: testuj zachowanie (co użytkownik widzi), nie implementację (jaki state). Używaj aria-label i ról dostępności - dobre testy wymuszają dobrą dostępność.
Na Co Rekruterzy Naprawdę Zwracają Uwagę
Po przeprowadzeniu setek rozmów rekrutacyjnych, mogę powiedzieć że nie chodzi tylko o techniczne odpowiedzi. Oto co wyróżnia kandydatów:
Rekruterzy szukają przede wszystkim zrozumienia "dlaczego", a nie tylko "jak". Każdy może nauczyć się składni hooków, ale wyjaśnienie dlaczego useEffect potrzebuje cleanup function przy subskrypcjach pokazuje głębsze zrozumienie. Podobnie ważne jest praktyczne doświadczenie - teoretyczna wiedza o optymalizacji to jedno, opowieść o tym jak sprofilowałeś i naprawiłeś bottleneck w produkcji to zupełnie co innego.
Świadomość trade-offów to kolejny kluczowy element. React.memo nie zawsze przyspiesza aplikację, Context nie rozwiązuje wszystkich problemów ze stanem, SSR ma swoje koszty. Kandydat który zna wady rozwiązań robi lepsze wrażenie. Wreszcie, umiejętność komunikacji technicznej odgrywa ogromną rolę - jasne wyjaśnienie złożonego tematu osobie nietechnicznej to skill, który sprawdza się na rozmowach tak samo jak w pracy zespołowej.
Praktyka na Koniec
Zanim pójdziesz na rozmowę, spróbuj odpowiedzieć na te pytania bez zaglądania do dokumentacji:
Wyjaśnij różnicę między useLayoutEffect a useEffect - kiedy użyłbyś każdego z nich? Następnie opisz jak zaimplementowałbyś własny hook useFetch z cache'owaniem, obsługą błędów i możliwością anulowania requestów. Kolejne wyzwanie: masz komponent, który re-renderuje się 50 razy na sekundę - jakie kroki podejmiesz aby zdiagnozować i naprawić problem? Na koniec spróbuj wyjaśnić Fiber architecture w React - po co powstał i jakie problemy rozwiązuje.
Te pytania nie mają jednej "poprawnej" odpowiedzi - chodzi o tok rozumowania i głębokość analizy.
Zobacz też
- React Hooks - Kiedy NIE używać useEffect, useMemo, useCallback - anti-patterns hooków React
- Angular vs React - Porównanie dla Programistów - porównanie dwóch największych frameworków
- Najtrudniejsze Pytania z Frameworków JavaScript - React, Angular, Vue i inne frameworki
Chcesz Więcej Pytań z React?
Ten artykuł to tylko fragment wiedzy potrzebnej na rozmowę. Nasze fiszki online zawierają 100+ pytań z React, Redux, i ekosystemu - od podstaw po zaawansowane tematy architektury.
Sprawdź Fiszki Online - React i Frontend
Możesz też najpierw zobaczyć przykładowe pytania w naszym bezpłatnym preview:
Bezpłatny Preview - Pytania React
Artykuł napisany na podstawie ponad 10 lat doświadczenia w programowaniu React i przeprowadzonych setek rozmów rekrutacyjnych w firmach technologicznych.
Chcesz więcej pytań rekrutacyjnych?
To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.
