Redux - Pytania Rekrutacyjne dla Frontend Developera [2026]

Sławomir Plamowski 22 min czytania
frontend javascript pytania-rekrutacyjne react redux redux-toolkit state-management

Przygotowujesz się do rozmowy na stanowisko Frontend Developer i musisz znać Redux? Redux pozostaje jednym z najpopularniejszych rozwiązań do zarządzania stanem w aplikacjach React. Ten przewodnik zawiera 45 pytań rekrutacyjnych z odpowiedziami - od podstaw po Redux Toolkit, middleware i zaawansowane wzorce.

Spis treści


Podstawy Redux

Czym jest Redux i jaki problem rozwiązuje?

Odpowiedź w 30 sekund: Redux to przewidywalna biblioteka do zarządzania stanem aplikacji JavaScript. Rozwiązuje problem "prop drilling" (przekazywania props przez wiele poziomów) i rozproszonego stanu, tworząc centralne źródło prawdy (Store) dostępne z dowolnego komponentu.

Odpowiedź w 2 minuty:

Redux centralizuje stan aplikacji w jednym miejscu (Store), z którego każdy komponent może korzystać bez potrzeby przekazywania props przez wszystkie poziomy hierarchii.

┌─────────────────────────────────────────────────────────────┐
│                          STORE                               │
│                    (Single Source of Truth)                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  { user: {...}, products: [...], cart: [...] }      │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
        ▼                     ▼                     ▼
   ┌─────────┐          ┌─────────┐          ┌─────────┐
   │ Header  │          │ Product │          │  Cart   │
   │ (user)  │          │  List   │          │ (cart)  │
   └─────────┘          └─────────┘          └─────────┘

Problem bez Redux:

// Prop drilling - przekazywanie przez 5 poziomów
<App user={user}>
  <Layout user={user}>
    <Sidebar user={user}>
      <Navigation user={user}>
        <UserMenu user={user} />  // Dopiero tutaj używane!
      </Navigation>
    </Sidebar>
  </Layout>
</App>

Rozwiązanie z Redux:

// Każdy komponent pobiera tylko to, czego potrzebuje
function UserMenu() {
  const user = useSelector(state => state.user)
  return <div>{user.name}</div>
}
Kiedy Redux Kiedy NIE Redux
Współdzielony stan między wieloma komponentami Prosty stan w jednym komponencie
Potrzeba śledzenia historii zmian Mała aplikacja bez złożonego stanu
Skomplikowana logika aktualizacji Stan można łatwo podnieść wyżej
Zespół wymaga przewidywalności Prototyp / MVP

Jakie są trzy fundamentalne zasady Redux (Three Principles)?

Odpowiedź w 30 sekund:

  1. Single Source of Truth - cały stan w jednym Store
  2. State is Read-Only - jedyny sposób zmiany to dispatch akcji
  3. Changes with Pure Functions - reducery to czyste funkcje

Odpowiedź w 2 minuty:

Oto jak te trzy zasady działają w praktyce - każda gwarantuje przewidywalność i ułatwia debugowanie aplikacji.

// 1. SINGLE SOURCE OF TRUTH
// Jeden obiekt Store przechowuje cały stan aplikacji
const store = configureStore({
  reducer: {
    users: usersReducer,
    products: productsReducer,
    cart: cartReducer
  }
})

// Stan całej aplikacji:
console.log(store.getState())
// { users: {...}, products: [...], cart: [...] }


// 2. STATE IS READ-ONLY
// Nigdy bezpośrednio nie modyfikujemy stanu
// ❌ Źle
store.getState().users.name = "Jan"

// ✅ Dobrze - przez akcję
store.dispatch({ type: 'users/setName', payload: 'Jan' })


// 3. CHANGES WITH PURE FUNCTIONS
// Reducer: (previousState, action) => newState
function usersReducer(state = initialState, action) {
  switch (action.type) {
    case 'users/setName':
      // Zwracamy NOWY obiekt, nie mutujemy starego
      return {
        ...state,
        name: action.payload
      }
    default:
      return state
  }
}

Dlaczego te zasady?

  • Przewidywalność - zawsze wiadomo gdzie szukać stanu
  • Debugowanie - Redux DevTools śledzi każdą akcję
  • Testowalność - czyste funkcje łatwo testować
  • Time Travel - można "cofać" stan do poprzednich wersji

Czym jest jednokierunkowy przepływ danych?

Odpowiedź w 30 sekund: Dane płyną w jednym kierunku: View → Action → Reducer → Store → View. Użytkownik wywołuje akcję, reducer przetwarza ją i zwraca nowy stan, Store aktualizuje się, komponenty renderują się ponownie.

Odpowiedź w 2 minuty:

Dane w Redux płyną zawsze w tym samym kierunku, co sprawia, że aplikacja jest przewidywalna i łatwa do debugowania.

┌──────────────────────────────────────────────────────────────┐
│                    UNIDIRECTIONAL DATA FLOW                   │
│                                                                │
│    ┌─────────┐    dispatch    ┌─────────┐                     │
│    │  VIEW   │ ────────────▶  │ ACTION  │                     │
│    │(React)  │                │{type,   │                     │
│    └────▲────┘                │payload} │                     │
│         │                     └────┬────┘                     │
│         │                          │                          │
│    subscribe                       │                          │
│         │                          ▼                          │
│    ┌────┴────┐                ┌─────────┐                     │
│    │  STORE  │ ◀───────────── │ REDUCER │                     │
│    │ (state) │    new state   │(pure fn)│                     │
│    └─────────┘                └─────────┘                     │
│                                                                │
└──────────────────────────────────────────────────────────────┘
// 1. VIEW - użytkownik klika przycisk
function AddToCartButton({ product }) {
  const dispatch = useDispatch()

  const handleClick = () => {
    // 2. ACTION - dispatch akcji
    dispatch({
      type: 'cart/addItem',
      payload: product
    })
  }

  return <button onClick={handleClick}>Dodaj do koszyka</button>
}

// 3. REDUCER - przetwarza akcję
function cartReducer(state = [], action) {
  switch (action.type) {
    case 'cart/addItem':
      // Zwraca NOWY stan
      return [...state, action.payload]
    default:
      return state
  }
}

// 4. STORE aktualizuje się automatycznie
// 5. VIEW - komponenty z useSelector re-renderują się
function CartCount() {
  const count = useSelector(state => state.cart.length)
  return <span>Koszyk: {count}</span>  // Automatycznie się aktualizuje!
}

Store, Actions i Reducers

Czym jest Store i jakie metody udostępnia?

Odpowiedź w 30 sekund: Store to obiekt przechowujący cały stan aplikacji. Udostępnia: getState() - pobiera aktualny stan, dispatch(action) - wysyła akcję, subscribe(listener) - nasłuchuje zmian. W Redux Toolkit tworzymy go przez configureStore.

Odpowiedź w 2 minuty:

Store to centralny obiekt Redux, który udostępnia trzy główne metody do zarządzania stanem aplikacji.

import { configureStore } from '@reduxjs/toolkit'

// Tworzenie Store
const store = configureStore({
  reducer: {
    counter: counterReducer,
    todos: todosReducer
  },
  // RTK automatycznie dodaje:
  // - Redux DevTools
  // - redux-thunk middleware
  // - sprawdzanie mutacji (development)
})

// getState() - pobierz aktualny stan
const currentState = store.getState()
console.log(currentState)
// { counter: 0, todos: [] }

// dispatch(action) - wyślij akcję
store.dispatch({ type: 'counter/increment' })
store.dispatch(addTodo('Nauczyć się Redux'))

// subscribe(listener) - nasłuchuj zmian
const unsubscribe = store.subscribe(() => {
  console.log('Stan się zmienił:', store.getState())
})

// Przestań nasłuchiwać
unsubscribe()

Dlaczego jeden Store?

// ❌ Źle - wiele Store'ów
const userStore = createStore(userReducer)
const cartStore = createStore(cartReducer)
// Problem: jak zsynchronizować? Który jest prawdą?

// ✅ Dobrze - jeden Store z wieloma slice'ami
const store = configureStore({
  reducer: {
    user: userReducer,
    cart: cartReducer,
    // Logika relacji między nimi jest w jednym miejscu
  }
})

Czym jest Action i Action Creator?

Odpowiedź w 30 sekund: Action to zwykły obiekt JavaScript z polem type (string opisujący co się stało) i opcjonalnym payload (dane). Action Creator to funkcja zwracająca akcję - ułatwia tworzenie akcji i zapobiega literówkom.

Odpowiedź w 2 minuty:

Action to obiekt opisujący zdarzenie, a Action Creator to funkcja która tworzy takie obiekty - oto jak działają w praktyce.

// ACTION - zwykły obiekt
const addTodoAction = {
  type: 'todos/add',
  payload: {
    id: 1,
    text: 'Nauczyć się Redux',
    completed: false
  }
}

// ACTION CREATOR - funkcja zwracająca akcję
function addTodo(text) {
  return {
    type: 'todos/add',
    payload: {
      id: Date.now(),
      text,
      completed: false
    }
  }
}

// Użycie
dispatch(addTodo('Przeczytać dokumentację'))


// Z REDUX TOOLKIT - createSlice generuje action creators
const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    // Automatycznie tworzy action creator: todosSlice.actions.add
    add: (state, action) => {
      state.push(action.payload)  // Immer pozwala na "mutację"
    },
    toggle: (state, action) => {
      const todo = state.find(t => t.id === action.payload)
      if (todo) todo.completed = !todo.completed
    },
    remove: (state, action) => {
      return state.filter(t => t.id !== action.payload)
    }
  }
})

// Eksportuj action creators
export const { add, toggle, remove } = todosSlice.actions

// Użycie
dispatch(add({ id: 1, text: 'Test', completed: false }))
dispatch(toggle(1))
dispatch(remove(1))

Flux Standard Action (FSA):

// Rekomendowany format akcji
{
  type: 'todos/add',           // Wymagane
  payload: { ... },            // Dane akcji
  error: false,                // Czy to błąd?
  meta: { timestamp: ... }     // Metadane
}

Czym jest Reducer i dlaczego musi być czystą funkcją?

Odpowiedź w 30 sekund: Reducer to czysta funkcja (state, action) => newState. Musi być czysta (pure function), bo: gwarantuje przewidywalność, umożliwia time-travel debugging, ułatwia testowanie. Nigdy nie mutuje stanu - zawsze zwraca nowy obiekt.

Odpowiedź w 2 minuty:

Reducer to funkcja określająca jak stan się zmienia w odpowiedzi na akcję - musi być czysta, aby Redux mógł działać przewidywalnie.

// REDUCER - czysta funkcja
function todosReducer(state = [], action) {
  switch (action.type) {
    case 'todos/add':
      // ✅ Zwracamy NOWY array
      return [...state, action.payload]

    case 'todos/toggle':
      // ✅ Mapujemy na NOWY array z NOWYMI obiektami
      return state.map(todo =>
        todo.id === action.payload
          ? { ...todo, completed: !todo.completed }
          : todo
      )

    case 'todos/remove':
      // ✅ Filter zwraca NOWY array
      return state.filter(todo => todo.id !== action.payload)

    default:
      // ✅ Nieznana akcja - zwracamy oryginalny stan
      return state
  }
}

Dlaczego czysta funkcja?

// ❌ NIECZYSTA - mutuje argument
function badReducer(state, action) {
  state.push(action.payload)  // Mutacja!
  return state  // Ten sam obiekt
}

// Problem:
// 1. React nie wykryje zmiany (ta sama referencja)
// 2. Redux DevTools nie pokaże różnicy
// 3. Time-travel nie zadziała
// 4. Testy są nieprzewidywalne


// ❌ NIECZYSTA - efekty uboczne
function badReducer(state, action) {
  localStorage.setItem('state', JSON.stringify(state))  // Efekt uboczny!
  fetch('/api/save', { body: state })  // Efekt uboczny!
  return { ...state, saved: true }
}


// ✅ CZYSTA FUNKCJA
function pureReducer(state, action) {
  // 1. Nie mutuje argumentów
  // 2. Nie ma efektów ubocznych
  // 3. Dla tych samych inputów - zawsze ten sam output
  return {
    ...state,
    items: [...state.items, action.payload]
  }
}
Czysta funkcja Nieczysta funkcja
Nie mutuje argumentów Modyfikuje argumenty
Brak efektów ubocznych API calls, localStorage
Deterministyczna Zależy od czasu, losowości
Łatwa do testowania Trudna do testowania

Jak działa combineReducers?

Odpowiedź w 30 sekund: combineReducers łączy wiele reducerów w jeden root reducer. Każdy reducer zarządza swoim "slice'em" stanu. Przy dispatch akcji, wszystkie reducery są wywoływane, ale każdy obsługuje tylko swój fragment.

Odpowiedź w 2 minuty:

Funkcja combineReducers łączy wiele reducerów w jeden, przy czym każdy z nich zarządza własną częścią stanu.

import { combineReducers, configureStore } from '@reduxjs/toolkit'

// Osobne reducery dla różnych części stanu
function usersReducer(state = [], action) {
  switch (action.type) {
    case 'users/add':
      return [...state, action.payload]
    default:
      return state
  }
}

function productsReducer(state = [], action) {
  switch (action.type) {
    case 'products/add':
      return [...state, action.payload]
    default:
      return state
  }
}

function cartReducer(state = { items: [], total: 0 }, action) {
  switch (action.type) {
    case 'cart/add':
      return {
        items: [...state.items, action.payload],
        total: state.total + action.payload.price
      }
    default:
      return state
  }
}

// Łączenie reducerów
const rootReducer = combineReducers({
  users: usersReducer,
  products: productsReducer,
  cart: cartReducer
})

// Lub bezpośrednio w configureStore
const store = configureStore({
  reducer: {
    users: usersReducer,
    products: productsReducer,
    cart: cartReducer
  }
})

// Wynikowy stan:
// {
//   users: [],
//   products: [],
//   cart: { items: [], total: 0 }
// }

Jak to działa wewnętrznie:

// combineReducers robi mniej więcej to:
function combineReducers(reducers) {
  return function rootReducer(state = {}, action) {
    const nextState = {}

    for (const key in reducers) {
      // Każdy reducer dostaje TYLKO swój fragment stanu
      nextState[key] = reducers[key](state[key], action)
    }

    return nextState
  }
}

Redux Toolkit (RTK)

Czym jest Redux Toolkit i dlaczego jest rekomendowany?

Odpowiedź w 30 sekund: Redux Toolkit (RTK) to oficjalny zestaw narzędzi upraszczający pracę z Redux. Rozwiązuje problemy "vanilla Redux": za dużo boilerplate'u, trudna konfiguracja, ręczne pisanie immutable updates. Jest to teraz rekomendowany sposób pisania Redux.

Odpowiedź w 2 minuty:

Redux Toolkit drastycznie redukuje ilość kodu potrzebnego do konfiguracji Redux - porównajmy klasyczne podejście z RTK.

// ❌ VANILLA REDUX - dużo boilerplate'u
// actions.js
const ADD_TODO = 'ADD_TODO'
const TOGGLE_TODO = 'TOGGLE_TODO'

function addTodo(text) {
  return { type: ADD_TODO, payload: { id: Date.now(), text, completed: false } }
}

// reducer.js
function todosReducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [...state, action.payload]
    case TOGGLE_TODO:
      return state.map(todo =>
        todo.id === action.payload
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    default:
      return state
  }
}

// store.js
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(todosReducer, applyMiddleware(thunk))


// ✅ REDUX TOOLKIT - znacznie mniej kodu
import { createSlice, configureStore } from '@reduxjs/toolkit'

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    add: (state, action) => {
      // Immer pozwala "mutować" - pod spodem tworzy nowy obiekt
      state.push(action.payload)
    },
    toggle: (state, action) => {
      const todo = state.find(t => t.id === action.payload)
      if (todo) todo.completed = !todo.completed
    }
  }
})

export const { add, toggle } = todosSlice.actions

const store = configureStore({
  reducer: { todos: todosSlice.reducer }
  // Automatycznie: DevTools, thunk, sprawdzanie mutacji
})

Co daje RTK?

Feature Vanilla Redux Redux Toolkit
Action creators Ręcznie Automatyczne (createSlice)
Immutable updates Spread operator Immer (pozorna mutacja)
DevTools Ręczna konfiguracja Automatyczne
Middleware applyMiddleware Wbudowane thunk
Async logic redux-thunk osobno createAsyncThunk
TypeScript Trudne typowanie Świetne wsparcie

Jak używać createSlice?

Odpowiedź w 30 sekund: createSlice to funkcja RTK tworząca reducer i action creators jednocześnie. Definiujesz name (prefix dla action types), initialState i reducers (obiekty z funkcjami). Dzięki Immer możesz pisać kod wyglądający jak mutacja.

Odpowiedź w 2 minuty:

CreateSlice to najbardziej potężna funkcja RTK, która jednocześnie tworzy reducer, action creators i pozwala pisać kod wyglądający jak mutacja dzięki Immer.

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface Todo {
  id: number
  text: string
  completed: boolean
}

interface TodosState {
  items: Todo[]
  filter: 'all' | 'active' | 'completed'
  loading: boolean
}

const initialState: TodosState = {
  items: [],
  filter: 'all',
  loading: false
}

const todosSlice = createSlice({
  name: 'todos',  // Action types będą: 'todos/add', 'todos/toggle', etc.
  initialState,
  reducers: {
    // Immer pozwala pisać "mutujący" kod
    add: (state, action: PayloadAction<Omit<Todo, 'id'>>) => {
      state.items.push({
        id: Date.now(),
        ...action.payload
      })
    },

    toggle: (state, action: PayloadAction<number>) => {
      const todo = state.items.find(t => t.id === action.payload)
      if (todo) {
        todo.completed = !todo.completed
      }
    },

    remove: (state, action: PayloadAction<number>) => {
      // Dla filter musimy przypisać nowy array
      state.items = state.items.filter(t => t.id !== action.payload)
    },

    setFilter: (state, action: PayloadAction<TodosState['filter']>) => {
      state.filter = action.payload
    },

    // Prepare callback - transformuj payload przed reducerem
    addWithTimestamp: {
      reducer: (state, action: PayloadAction<Todo & { createdAt: string }>) => {
        state.items.push(action.payload)
      },
      prepare: (text: string) => ({
        payload: {
          id: Date.now(),
          text,
          completed: false,
          createdAt: new Date().toISOString()
        }
      })
    }
  }
})

// Eksport action creators (automatycznie wygenerowane)
export const { add, toggle, remove, setFilter, addWithTimestamp } = todosSlice.actions

// Eksport reducer
export default todosSlice.reducer

// Użycie:
dispatch(add({ text: 'Nauczyć się RTK', completed: false }))
dispatch(toggle(1))
dispatch(setFilter('active'))

Czym jest createAsyncThunk?

Odpowiedź w 30 sekund: createAsyncThunk to funkcja RTK do obsługi operacji asynchronicznych. Automatycznie generuje trzy action types: pending, fulfilled, rejected. Obsługujesz je w extraReducers slice'a. Wspiera anulowanie i warunki wykonania.

Odpowiedź w 2 minuty:

CreateAsyncThunk automatyzuje obsługę operacji asynchronicznych, generując akcje dla wszystkich stanów żądania (pending, fulfilled, rejected).

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// Tworzenie async thunk
export const fetchUsers = createAsyncThunk(
  'users/fetchAll',  // Action type prefix
  async (_, { rejectWithValue, signal }) => {
    try {
      const response = await fetch('/api/users', { signal })

      if (!response.ok) {
        throw new Error('Failed to fetch')
      }

      return await response.json()  // To będzie action.payload
    } catch (error) {
      return rejectWithValue(error.message)
    }
  }
)

// Thunk z argumentem
export const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId: number, { rejectWithValue }) => {
    const response = await fetch(`/api/users/${userId}`)
    if (!response.ok) {
      return rejectWithValue('User not found')
    }
    return response.json()
  }
)

// Slice obsługujący async actions
const usersSlice = createSlice({
  name: 'users',
  initialState: {
    items: [],
    loading: false,
    error: null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      // fetchUsers.pending - żądanie w trakcie
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true
        state.error = null
      })
      // fetchUsers.fulfilled - sukces
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false
        state.items = action.payload
      })
      // fetchUsers.rejected - błąd
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false
        state.error = action.payload ?? 'Unknown error'
      })
  }
})

// Użycie w komponencie
function UsersList() {
  const dispatch = useDispatch()
  const { items, loading, error } = useSelector(state => state.users)

  useEffect(() => {
    const promise = dispatch(fetchUsers())

    // Anulowanie przy unmount
    return () => promise.abort()
  }, [dispatch])

  if (loading) return <Spinner />
  if (error) return <Error message={error} />
  return <ul>{items.map(user => <li key={user.id}>{user.name}</li>)}</ul>
}

React-Redux

Jak działają useSelector i useDispatch?

Odpowiedź w 30 sekund: useSelector pobiera dane ze Store - przyjmuje funkcję selektor zwracającą fragment stanu. useDispatch zwraca funkcję dispatch do wysyłania akcji. Oba pochodzą z react-redux i działają tylko wewnątrz <Provider>.

Odpowiedź w 2 minuty:

Te dwa hooki to podstawa integracji React z Redux - useSelector do odczytu, useDispatch do aktualizacji stanu.

import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, incrementByAmount } from './counterSlice'

function Counter() {
  // useDispatch - pobierz funkcję dispatch
  const dispatch = useDispatch()

  // useSelector - pobierz dane ze Store
  const count = useSelector(state => state.counter.value)
  const status = useSelector(state => state.counter.status)

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
    </div>
  )
}

// Provider w głównym pliku
import { Provider } from 'react-redux'
import { store } from './store'

function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  )
}

Kiedy useSelector powoduje re-render?

// ❌ Zawsze nowy obiekt = zawsze re-render
const data = useSelector(state => ({
  user: state.user,
  count: state.counter
}))

// ✅ Osobne selektory = re-render tylko gdy zmieni się konkretna wartość
const user = useSelector(state => state.user)
const count = useSelector(state => state.counter)

// ✅ shallowEqual dla obiektów
import { shallowEqual } from 'react-redux'

const data = useSelector(
  state => ({
    user: state.user,
    count: state.counter
  }),
  shallowEqual  // Porównuje płytko właściwości obiektu
)

Jaka jest różnica między useSelector a connect (HOC)?

Odpowiedź w 30 sekund: connect to HOC (Higher-Order Component) - starsze API opakowujące komponent. useSelector/useDispatch to hooki - nowsze, prostsze API. Hooki są rekomendowane dla nowego kodu, ale connect nadal działa i ma automatyczną memoizację.

Odpowiedź w 2 minuty:

Connect był standardem przed React Hooks - porównajmy oba podejścia, aby zrozumieć różnice i kiedy używać którego.

// ❌ CONNECT (starsze API)
import { connect } from 'react-redux'

function Counter({ count, increment, decrement }) {
  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

const mapStateToProps = (state) => ({
  count: state.counter.value
})

const mapDispatchToProps = {
  increment,
  decrement
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter)


// ✅ HOOKS (nowsze API)
import { useSelector, useDispatch } from 'react-redux'

function Counter() {
  const count = useSelector(state => state.counter.value)
  const dispatch = useDispatch()

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  )
}

export default Counter
Cecha connect Hooks
Styl HOC (wrapper) Hooki wewnątrz komponentu
Boilerplate Więcej kodu Mniej kodu
Memoizacja Automatyczna Wymaga shallowEqual/createSelector
TypeScript Trudniejsze typowanie Łatwiejsze
Testowalność Łatwiejsza (props) Wymaga mock store
Rekomendacja Legacy code Nowy kod

Middleware

Czym jest Middleware w Redux?

Odpowiedź w 30 sekund: Middleware to funkcja przechwytująca akcje między dispatch a reducerem. Używany do: logowania, obsługi async (thunk/saga), walidacji, transformacji akcji. Middleware tworzą łańcuch - każdy może przekazać akcję dalej lub ją zablokować.

Odpowiedź w 2 minuty:

Middleware w Redux działa jak łańcuch przechwytujący każdą akcję przed dotarciem do reducera - idealny do logowania, obsługi async czy walidacji.

dispatch(action)
       │
       ▼
┌─────────────────────────────────────────────────────────────┐
│                      MIDDLEWARE CHAIN                        │
│                                                               │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐              │
│   │  Logger  │ ─▶ │  Thunk   │ ─▶ │ Custom   │ ─▶ reducer   │
│   └──────────┘    └──────────┘    └──────────┘              │
│                                                               │
└─────────────────────────────────────────────────────────────┘
// Prosty middleware - logger
const loggerMiddleware = (store) => (next) => (action) => {
  console.log('Dispatching:', action.type)
  console.log('Before:', store.getState())

  const result = next(action)  // Przekaż akcję dalej

  console.log('After:', store.getState())
  return result
}

// Middleware blokujący akcje
const validationMiddleware = (store) => (next) => (action) => {
  if (action.type === 'users/delete' && !action.payload.confirmed) {
    console.warn('Delete blocked - not confirmed')
    return  // Nie przekazuj dalej
  }
  return next(action)
}

// Konfiguracja z middleware
import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware()
      .concat(loggerMiddleware)
      .concat(validationMiddleware)
})

Typowe użycia middleware:

  • Logging - logowanie akcji do konsoli/serwera
  • Async - redux-thunk, redux-saga
  • Analytics - wysyłanie eventów do Google Analytics
  • Error reporting - raportowanie błędów do Sentry
  • Walidacja - sprawdzanie akcji przed reducerem

Jaka jest różnica między Redux Thunk a Redux Saga?

Odpowiedź w 30 sekund: Thunk używa funkcji zwracających funkcje - prostszy, mniej kodu, wystarczający dla większości przypadków. Saga używa generatorów i efektów - bardziej testowalny, lepszy dla złożonych przepływów, obsługuje anulowanie i race conditions.

Odpowiedź w 2 minuty:

Thunk i Saga to dwa najpopularniejsze rozwiązania do obsługi async w Redux - różnią się składnią, możliwościami i przypadkami użycia.

// REDUX THUNK - funkcja zwracająca funkcję
const fetchUser = (userId) => async (dispatch, getState) => {
  dispatch({ type: 'users/loading' })

  try {
    const response = await fetch(`/api/users/${userId}`)
    const user = await response.json()
    dispatch({ type: 'users/loaded', payload: user })
  } catch (error) {
    dispatch({ type: 'users/error', payload: error.message })
  }
}

// Użycie
dispatch(fetchUser(123))


// REDUX SAGA - generator z efektami
import { call, put, takeLatest } from 'redux-saga/effects'

function* fetchUserSaga(action) {
  try {
    yield put({ type: 'users/loading' })

    // call - wywołaj async funkcję (łatwe do mockowania w testach)
    const user = yield call(fetch, `/api/users/${action.payload}`)

    yield put({ type: 'users/loaded', payload: user })
  } catch (error) {
    yield put({ type: 'users/error', payload: error.message })
  }
}

function* watchFetchUser() {
  // takeLatest - anuluj poprzednie jeśli nowe przyszło
  yield takeLatest('users/fetch', fetchUserSaga)
}

// Root saga
export function* rootSaga() {
  yield all([
    watchFetchUser(),
    // inne sagi...
  ])
}
Cecha Redux Thunk Redux Saga
Składnia async/await Generatory (function*)
Krzywa uczenia Niska Wysoka
Testowalność Trudniejsza Łatwa (effects)
Anulowanie Ręczne (AbortController) Wbudowane (takeLatest, cancel)
Złożone przepływy Trudne Łatwe (fork, race, all)
Bundle size ~2KB ~25KB
Kiedy używać Proste API calls Złożona logika, websockety

Selektory i Reselect

Czym są selektory i jak działa createSelector?

Odpowiedź w 30 sekund: Selektory to funkcje wyciągające dane ze Store. createSelector z Reselect tworzy memoizowane selektory - oblicza derived data tylko gdy zmieniają się inputy. Zapobiega niepotrzebnym re-renderom i poprawia wydajność.

Odpowiedź w 2 minuty:

Selektory z memoizacją dzięki createSelector obliczają wartości tylko gdy zmieniają się zależności, co drastycznie poprawia wydajność.

import { createSelector } from '@reduxjs/toolkit'  // RTK re-eksportuje Reselect

// Proste selektory (input selectors)
const selectTodos = (state) => state.todos.items
const selectFilter = (state) => state.todos.filter

// Memoizowany selektor (output selector)
const selectFilteredTodos = createSelector(
  // Input selectors - gdy te się zmienią, przelicz
  [selectTodos, selectFilter],
  // Output selector - wywoływana tylko gdy inputy się zmieniły
  (todos, filter) => {
    console.log('Przeliczam filtered todos...')  // Logowane tylko gdy potrzeba

    switch (filter) {
      case 'active':
        return todos.filter(t => !t.completed)
      case 'completed':
        return todos.filter(t => t.completed)
      default:
        return todos
    }
  }
)

// Selektor kompozytowy
const selectTodoStats = createSelector(
  [selectTodos],
  (todos) => ({
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length
  })
)

// Użycie w komponencie
function TodoList() {
  // Memoizowany - nie powoduje re-renderu gdy inne części stanu się zmienią
  const filteredTodos = useSelector(selectFilteredTodos)
  const stats = useSelector(selectTodoStats)

  return (
    <div>
      <p>Aktywne: {stats.active} / {stats.total}</p>
      <ul>
        {filteredTodos.map(todo => (
          <TodoItem key={todo.id} todo={todo} />
        ))}
      </ul>
    </div>
  )
}

Dlaczego memoizacja jest ważna?

// ❌ Bez memoizacji - nowy obiekt przy każdym renderze
const selectExpensiveData = (state) => {
  return state.items.filter(x => x.active).map(x => x.name)  // Nowy array!
}
// useSelector wykrywa zmianę referencji = niepotrzebny re-render

// ✅ Z memoizacją - ten sam obiekt jeśli dane się nie zmieniły
const selectExpensiveData = createSelector(
  [state => state.items],
  (items) => items.filter(x => x.active).map(x => x.name)
)
// Zwraca tę samą referencję = brak re-renderu

Testowanie Redux

Jak testować Reducery i Action Creators?

Odpowiedź w 30 sekund: Reducery testujemy jako czyste funkcje - przekazujemy stan i akcję, sprawdzamy wynik. Action creators testujemy sprawdzając strukturę zwracanego obiektu. Z RTK używamy slice.reducer i slice.actions.

Odpowiedź w 2 minuty:

Testowanie Redux jest proste dzięki czystym funkcjom - reducery testujemy jak zwykłe funkcje JavaScript, a action creators sprawdzamy pod kątem struktury obiektów.

import todosReducer, { add, toggle, remove } from './todosSlice'

describe('todosSlice', () => {
  // Test reducer
  describe('reducer', () => {
    it('should return initial state', () => {
      expect(todosReducer(undefined, { type: 'unknown' }))
        .toEqual([])
    })

    it('should add a todo', () => {
      const initialState = []
      const todo = { id: 1, text: 'Test', completed: false }

      const result = todosReducer(initialState, add(todo))

      expect(result).toHaveLength(1)
      expect(result[0]).toEqual(todo)
    })

    it('should toggle a todo', () => {
      const initialState = [
        { id: 1, text: 'Test', completed: false }
      ]

      const result = todosReducer(initialState, toggle(1))

      expect(result[0].completed).toBe(true)
    })

    it('should not mutate state', () => {
      const initialState = [{ id: 1, text: 'Test', completed: false }]
      const frozenState = Object.freeze(initialState)

      // Nie powinno rzucić błędu
      expect(() => todosReducer(frozenState, toggle(1))).not.toThrow()
    })
  })

  // Test action creators
  describe('action creators', () => {
    it('add should create correct action', () => {
      const todo = { id: 1, text: 'Test', completed: false }

      expect(add(todo)).toEqual({
        type: 'todos/add',
        payload: todo
      })
    })
  })
})

// Test async thunk
import { fetchUsers } from './usersSlice'
import { configureStore } from '@reduxjs/toolkit'

describe('fetchUsers thunk', () => {
  it('should fetch users successfully', async () => {
    const mockUsers = [{ id: 1, name: 'John' }]
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockUsers)
    })

    const store = configureStore({
      reducer: { users: usersReducer }
    })

    await store.dispatch(fetchUsers())

    expect(store.getState().users.items).toEqual(mockUsers)
    expect(store.getState().users.loading).toBe(false)
  })
})

Zobacz też

Jeśli przygotowujesz się do rozmowy jako Frontend Developer, sprawdź również:


Powodzenia na rozmowie! Redux może wydawać się skomplikowany, ale z Redux Toolkit jest znacznie prostszy. Pamiętaj o trzech zasadach, memoizowanych selektorach i testowaniu reducerów jako czystych funkcji.

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.