Advanced

Архітектура та Best Practices

Тепер, коли ви знаєте всі інструменти Redux Toolkit, давайте поговоримо про те, як організувати код, щоб проєкт залишався підтримуваним навіть при зростанні.

Архітектура та Best Practices

Тепер, коли ви знаєте всі інструменти Redux Toolkit, давайте поговоримо про те, як організувати код, щоб проєкт залишався підтримуваним навіть при зростанні.

Основні принципи

1. Redux Toolkit завжди

Використовуйте тільки RTK. createStore, combineReducers, action types руками — ні!

2. Feature-based структура

Групуйте код за features, а не за типами (reducers, actions)

3. Що не треба зберігати

Форми, UI state, тимчасові дані — НЕ в Redux

4. Нормалізація колекцій

Entity Adapter для всіх списків з ID

Структура проєкту

❌ Погана структура (за типами)

src/
├── actions/
│   ├── userActions.js
│   ├── postActions.js
│   └── commentActions.js
├── reducers/
│   ├── userReducer.js
│   ├── postReducer.js
│   └── commentReducer.js
├── constants/
│   └── actionTypes.js
├── components/
│   ├── UserList.jsx
│   └── PostList.jsx
└── store.js

Проблеми:

  • Логіка однієї feature розкидана по папках
  • Складно видалити feature
  • Важко знайти пов'язаний код

✅ Хороша структура (за features)

src/
├── features/
│   ├── auth/
│   │   ├── authSlice.js
│   │   ├── authApi.js (RTK Query)
│   │   ├── LoginForm.jsx
│   │   ├── useAuth.js (custom hook)
│   │   └── index.js (exports)
│   ├── posts/
│   │   ├── postsSlice.js
│   │   ├── postsApi.js
│   │   ├── PostsList.jsx
│   │   ├── PostDetail.jsx
│   │   ├── CreatePost.jsx
│   │   └── index.js
│   └── comments/
│       ├── commentsSlice.js
│       ├── commentsApi.js
│       ├── CommentsList.jsx
│       └── index.js
├── app/
│   ├── store.js
│   ├── rootReducer.js
│   └── App.jsx
└── shared/
    ├── components/
    ├── hooks/
    └── utils/

Переваги:

  • Вся логіка feature в одному місці
  • Легко видалити/перенести feature
  • Code splitting природний
  • Команди можуть працювати паралельно

Що НЕ зберігати в Redux

Не кладіть у Redux:
  1. UI State (modals, tooltips, accordion expanded)
    // ❌ НЕ Redux
    const [isModalOpen, setIsModalOpen] = useState(false)
    
  2. Form State (поточні значення inputs)
    // ❌ НЕ Redux, використовуйте React Hook Form
    const { register, handleSubmit } = useForm()
    
  3. Тимчасові дані (hover states, scroll position)
    // ❌ НЕ Redux
    const [scrollY, setScrollY] = useState(0)
    
  4. Дані, що не діляться між компонентами
    // ❌ Якщо лише один компонент використовує — local state
    const [count, setCount] = useState(0)
    
  5. Non-serializable (Functions, Promises, Date objects)
    // ❌ НІКОЛИ
    state.callback = () => {}
    state.fetchPromise = fetch('/api')
    state.createdAt = new Date() // Зберігайте як ISO string!
    
Правило: Redux для глобального state, що діляться кількома компонентами або потребує persistence/debugging.

Коли використовувати Redux

✅ Використовуйте Redux для:

  • Auth state (user, token, permissions)
  • App-wide preferences (theme, language, sidebar state)
  • Server cache (users, posts, products)
  • Entity collections (списки з CRUD операціями)
  • Complex shared state (shopping cart, multi-step wizards)
  • Real-time data (notifications, chat messages)

❌ НЕ використовуйте Redux для:

  • Form state → React Hook Form, Formik
  • Component UI state → useState
  • Routing state → React Router
  • Server state (якщо простий CRUD) → RTK Query, React Query
  • Temporary UI → component state

State Shape Design

Добра структура state

{
  // Auth
  auth: {
    user: { id: 1, name: 'John' },
    token: 'abc123',
    isAuthenticated: true,
  },

  // Entities (normalized)
  posts: {
    ids: [1, 2, 3],
    entities: {
      1: { id: 1, title: '...', authorId: 5 },
      2: { id: 2, title: '...', authorId: 5 },
    },
  },

  // UI preferences
  ui: {
    theme: 'dark',
    sidebarOpen: true,
    language: 'uk',
  },

  // RTK Query cache
  api: {
    queries: { /* auto-managed */ },
    mutations: { /* auto-managed */ },
  },
}

Anti-patterns

// ❌ Погано: nested entities
{
  users: [
    {
      id: 1,
      posts: [ // ❌ Дублювання!
        { id: 10, comments: [/* ... */] }
      ]
    }
  ]
}

// ✅ Добре: normalized
{
  users: { ids: [1], entities: { 1: {...} } },
  posts: { ids: [10], entities: { 10: { userId: 1 } } },
  comments: { ids: [...], entities: {...} }
}

Slice Organization

Template слайсу

features/todos/todosSlice.js
import { createSlice, createSelector, createEntityAdapter } from '@reduxjs/toolkit'
import { fetchTodos, createTodo } from './todosThunks'

// 1. Entity Adapter (якщо є колекція)
const todosAdapter = createEntityAdapter()

// 2. Initial State
const initialState = todosAdapter.getInitialState({
    loading: 'idle',
    error: null,
    filter: 'all',
})

// 3. Slice
const todosSlice = createSlice({
    name: 'todos',
    initialState,
    reducers: {
        // Синхронні actions
        filterChanged(state, action) {
            state.filter = action.payload
        },
        todoToggled(state, action) {
            const todo = state.entities[action.payload]
            if (todo) {
                todo.completed = !todo.completed
            }
        },
    },
    extraReducers: (builder) => {
        // Async thunks
        builder.addCase(fetchTodos.fulfilled, (state, action) => {
            todosAdapter.setAll(state, action.payload)
            state.loading = 'succeeded'
        })
    },
})

// 4. Selectors
const selectTodosState = (state) => state.todos

export const todosSelectors = todosAdapter.getSelectors(selectTodosState)

export const selectFilteredTodos = createSelector(
    [todosSelectors.selectAll, (state) => state.todos.filter],
    (todos, filter) => {
        if (filter === 'completed') return todos.filter((t) => t.completed)
        if (filter === 'active') return todos.filter((t) => !t.completed)
        return todos
    },
)

// 5. Exports
export const { filterChanged, todoToggled } = todosSlice.actions
export default todosSlice.reducer

Performance Best Practices

1. Memoized Selectors

// ❌ Створює новий масив кожного разу
const selectCompletedTodos = (state) => state.todos.filter((t) => t.completed)

// ✅ Memoized
const selectCompletedTodos = createSelector([(state) => state.todos], (todos) => todos.filter((t) => t.completed))

2. Нормалізація даних

// ❌ O(n) lookup
const todo = state.todos.find((t) => t.id === 5)

// ✅ O(1) lookup з Entity Adapter
const todo = state.todos.entities[5]

3. Уникайте глибоких копій

// ❌ Погано
reducers: {
  updateDeep(state) {
    return JSON.parse(JSON.stringify(state)); // Повільно!
  }
}

// ✅ Добре: Immer робить це ефективно
reducers: {
  updateDeep(state) {
    state.deeply.nested.value = 'new'; // Immer оптимізує
  }
}

Testing Strategies

Test Slices

import todosReducer, { todoAdded } from './todosSlice'

test('todoAdded додає todo', () => {
    const initialState = { items: [] }
    const newState = todosReducer(
        initialState,
        todoAdded({
            id: 1,
            text: 'Test',
            completed: false,
        }),
    )

    expect(newState.items).toHaveLength(1)
    expect(newState.items[0].text).toBe('Test')
})

Test Selectors

import { selectCompletedTodos } from './todosSlice'

test('selectCompletedTodos filters', () => {
    const state = {
        todos: [
            { id: 1, completed: true },
            { id: 2, completed: false },
        ],
    }

    const result = selectCompletedTodos(state)
    expect(result).toHaveLength(1)
    expect(result[0].id).toBe(1)
})

Test Thunks

import { fetchTodos } from './todosThunks'
import { configureStore } from '@reduxjs/toolkit'

test('fetchTodos завантажує todos', async () => {
    const store = configureStore({ reducer: { todos: todosReducer } })

    global.fetch = jest.fn(() =>
        Promise.resolve({
            json: () => Promise.resolve([{ id: 1, text: 'Test' }]),
        }),
    )

    await store.dispatch(fetchTodos())

    const state = store.getState()
    expect(state.todos.items).toHaveLength(1)
})

TypeScript Best Practices

Типізація Store

import { configureStore } from '@reduxjs/toolkit'
import todosReducer from './features/todos/todosSlice'

export const store = configureStore({
    reducer: {
        todos: todosReducer,
    },
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

Typed Hooks

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

Typed Slice

interface TodosState {
    items: Todo[]
    loading: 'idle' | 'pending' | 'succeeded' | 'failed'
    error: string | null
}

const initialState: TodosState = {
    items: [],
    loading: 'idle',
    error: null,
}

const todosSlice = createSlice({
    name: 'todos',
    initialState,
    reducers: {
        todoAdded: (state, action: PayloadAction<Todo>) => {
            state.items.push(action.payload)
        },
    },
})

Common Mistakes


Migration Strategy

З класичного Redux на RTK

  1. Встановіть RTK
    npm install @reduxjs/toolkit
    
  2. Замініть createStore на configureStore
    // Було
    const store = createStore(rootReducer, applyMiddleware(thunk))
    
    // Стало
    const store = configureStore({ reducer: rootReducer })
    
  3. Перепишіть slices по одному
    • Почніть з найпростішого
    • Об'єднайте constants + actions + reducer в createSlice
    • Тестуйте після кожного slice
  4. Додайте Entity Adapters де потрібно
  5. Розгляньте RTK Query для API

Рекомендовані інструменти

Redux DevTools

Browser extension для debugging

Immer

Вбудовано в RTK для мутабельного синтаксису

Reselect

Вбудовано в RTK (createSelector)

Redux Persist

Для збереження state в localStorage

Checklist для ревью коду

  • Використовується Redux Toolkit (не класичний Redux)
  • Feature-based структура папок
  • Entity Adapter для колекцій
  • createSelector для складних обчислень
  • Немає non-serializable даних в state
  • Немає form/UI state в Redux (якщо не треба ділитися)
  • RTK Query для CRUD операцій
  • TypeScript типізація (якщо TS проєкт)
  • Tests для reducers та селекторів
  • DevTools увімкнені в development

Висновок

Золоті правила Redux:

  1. ✅ Redux Toolkit завжди
  2. ✅ Feature-based структура
  3. ✅ Нормалізуйте колекції
  4. ✅ Мемоізація селекторів
  5. ✅ RTK Query для API
  6. ❌ НЕ зберігайте form/UI state
  7. ❌ НЕ мутуйте state поза Immer
  8. ❌ НЕ ігноруйте DevTools
Найважливіше: Redux — це інструмент для глобального state. Не використовуйте його для всього підряд. Іноді useState — найкраще рішення!

Корисні ресурси

Вітаю! 🎉 Ви освоїли Redux Toolkit та готові будувати масштабовані додатки!

Copyright © 2026