Redux - Pytania Rekrutacyjne dla Frontend Developera [2026]
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
- Store, Actions i Reducers
- Redux Toolkit (RTK)
- React-Redux
- Middleware
- Selektory i Reselect
- Testowanie Redux
- Zobacz też
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:
- Single Source of Truth - cały stan w jednym Store
- State is Read-Only - jedyny sposób zmiany to dispatch akcji
- 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ż:
- Kompletny Przewodnik - Rozmowa Frontend Developer - pełny przewodnik przygotowania do rozmowy frontend
- Najtrudniejsze Pytania React - zaawansowane koncepcje React
- React Hooks - useEffect, useMemo, useCallback - kiedy NIE używać hooków
- Najtrudniejsze Pytania JavaScript - zaawansowane koncepcje JS
- TypeScript dla Początkujących - podstawy i best practices
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.
