15 Najtrudniejszych Pytań Rekrutacyjnych z React

Sławomir Plamowski 20 min czytania
frontend hooks interview-questions javascript react redux wydajność

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ż


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.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.