Ось готовий урок, створений спеціально для тебе у стилі David Malan. Вмикай уяву, ми починаємо! 🚀
🎓 Тема: Вступ до NgRx та Redux-патерну
«Керуємо хаосом, або чому вашому додатку потрібен "Головний Бухгалтер"»
1. 🔥 Вступ: проблема та мотивація
Уявіть, що ви будуєте не просто маленький сайт-візитку, а величезний інтернет-магазин на Angular. У вас є компонент «Кошик» у верхньому правому куті, компонент «Список товарів» по центру, і десь у підвалі — віджет «Останні переглянуті».
Ви натискаєте кнопку «Купити» в списку товарів. Що має статися? 1. Цифра в кошику має змінитися. 2. Кнопка на товарі має стати неактивною (або показати "Додано"). 3. Загальна сума замовлення має перерахуватися.
А тепер скажіть мені чесно: як ви це зробите?
Будете передавати дані через @Input() і @Output() через 10 батьківських компонентів вгору і вниз? Або створите «брудний» сервіс, де кожен компонент може змінювати дані як йому заманеться?
Це призводить до Spaghetti Code. Коли щось ламається (наприклад, сума не оновилася), ви гадки не маєте, хто саме і коли змінив дані. Це хаос.
Аналогія з життя: Уявіть банк. Чи можете ви просто зайти у сховище, взяти гроші й піти? Ні! 1. Ви підходите до касира (Component). 2. Ви заповнюєте квитанцію про наміри: «Хочу зняти 100 грн» (Action). 3. Касир перевіряє правила і сам оновлює баланс у системі (Reducer). 4. Ви отримуєте SMS із новим балансом (Selector).
Без цієї системи в банку (і в вашому додатку) настане анархія. NgRx — це і є ця сувора банківська система для ваших даних.
2. 🧠 Теоретична база (без сухої академічності)
NgRx — це бібліотека для Angular, яка реалізує патерн Redux.
Головна ідея: Єдине Джерело Правди (Single Source of Truth). Весь стан вашого додатку (дані користувача, кошик, налаштування теми) зберігається в одному великому об'єкті.
Як це працює "під капотом"? (Цикл життя Redux)
Запам'ятайте цю трійцю, як "Отче наш":
- Store (Сховище): Це просто великий JavaScript-об'єкт, де лежать усі ваші дані. Це база даних на клієнті. Важливо: Ви не можете змінювати його напряму! Тільки читати.
- Action (Дія): Це звичайний об'єкт, який каже ЩО сталося. Наприклад:
{ type: '[Cart] Add Item', itemId: 42 }. Це як поштовий лист, який ви відправляєте в систему. - Reducer (Редюсер): Це чиста функція. Вона бере старий стан і акцію, і повертає НОВИЙ стан.
- Інтуїтивно: Це як кухар. У нього є набір інгредієнтів (State) і замовлення (Action). Він готує нову страву (New State).
❗️ Залізне правило (Immutability): Редюсер ніколи не змінює старі дані (не робить
state.count = 5). Він створює копію даних зі змінами. Чому? Щоб Angular міг миттєво зрозуміти, що щось змінилося, просто порівнявши посилання на об'єкти, а не перевіряючи кожне поле.
3. 🧪 Приклади (від простого до реального)
Приклад 1: Лічильник (Hello World у світі NgRx)
У нас є кнопка "+1".
Крок 1: Action (Подія) Ми просто оголошуємо намір.
import { createAction } from '@ngrx/store';
export const increment = createAction('[Counter] Increment');
export const reset = createAction('[Counter] Reset');
Крок 2: Reducer (Логіка) Це мозок операції.
import { createReducer, on } from '@ngrx/store';
import { increment, reset } from './counter.actions';
export const initialState = 0; // Початковий стан
export const counterReducer = createReducer(
initialState,
// Якщо прийшла акція increment -> поверни (state + 1)
on(increment, (state) => state + 1),
// Якщо прийшла акція reset -> поверни 0
on(reset, (state) => 0)
);
Крок 3: Використання в компоненті
export class MyComponent {
// Observable! Дані прилетять самі, коли зміняться.
count$ = this.store.select('count');
constructor(private store: Store<{ count: number }>) {}
onIncrement() {
// Ми не змінюємо змінну. Ми "диспатчимо" (відправляємо) акцію.
this.store.dispatch(increment());
}
}
Питання до вас: Що станеться, якщо я натисну кнопку 5 разів підряд? Відповідь:
dispatchспрацює 5 разів, редюсер 5 разів створить новий стан, іcount$у шаблоні 5 разів оновить цифру. Магія реактивності!
Приклад 2: Складніше — передача даних (Payload)
А що, як ми хочемо додати не 1, а конкретне число? Або додати товар у кошик?
Action:
import { createAction, props } from '@ngrx/store';
// props — це "вантаж", який несе акція
export const addToCart = createAction(
'[Cart] Add Item',
props<{ productId: string; price: number }>()
);
Reducer:
export const cartReducer = createReducer(
initialState, // припустимо, це пустий масив []
on(addToCart, (state, action) => {
// ❌ ПОМИЛКА: state.push(action.productId) — мутація!
// ✅ ПРАВИЛЬНО: створюємо новий масив
return [...state, { id: action.productId, price: action.price }];
})
);
Бачите? Логіка завжди передбачувана. Якщо вхідні дані однакові, результат завжди однаковий.
4. 🛠 Практична частина
Час забруднити руки кодом. Уявіть, що ви працюєте над модулем "Список завдань" (Todo List).
Завдання 1: Створення Акції
Напишіть Action для видалення завдання. Вона має називатися [Todo] Remove Item і приймати id (string) як props.
Завдання 2: Логіка Редюсера
Напишіть фрагмент on(...) для редюсера, який обробляє цю акцію.
Підказка: Вам знадобиться метод масиву .filter(), щоб створити новий масив без видаленого елемента.
Завдання 3: "А що, якщо..."
Що станеться, якщо в редюсері ви випадково напишете state.items.pop()? Чому Angular компонент може не оновитися на екрані, навіть якщо дані в пам'яті змінилися? (Згадайте про immutability).
Завдання 4: Міні-кейс
У вас є перемикач "Показати тільки виконані завдання". Це теж стан!
Де б ви це зберігали:
а) Локально в компоненті (змінна showCompleted)?
б) У глобальному Store?
Обгрунтуйте вибір. (Правильної відповіді немає, все залежить від того, чи потрібно іншим компонентам знати про цей фільтр).
5. 💡 Мислення як у розробника
Як відрізнити новачка від сеньйора в NgRx?
-
Помилка новачка: Пхати все в Store.
- Приклад: Стан відкриття/закриття дрібного випадаючого меню, яке нікому більше не цікаве.
- Порада: Якщо дані потрібні тільки тут і зараз — залиште їх у компоненті. Store — для даних, які є спільними або мають жити довго.
-
Страх перед Boilerplate (Багато писанини):
- Так, NgRx вимагає написати багато файлів (Actions, Reducers, Selectors).
- Думка профі: "Я пишу більше коду зараз, щоб через 3 місяці, коли проект виросте в 10 разів, я точно знав, де і як змінюються мої дані". Це інвестиція в стабільність.
-
Redux DevTools:
- Це суперсила. Встановіть розширення для браузера. Ви зможете "подорожувати в часі" — відміняти дії, дивитися історію змін. Використовуйте це з першого дня!
6. 🧩 Підсумок
Отже, що ми сьогодні розібрали?
- Проблема: Хаотичні зв'язки між компонентами в великих додатках.
- Рішення: NgRx (Redux) — централізований банк даних.
- Flow: Action (намір) -> Reducer (зміна стану) -> Store (сховище) -> Selector (відображення).
- Правило: Дані незмінні (Immutable). Ми завжди створюємо нову версію стану.
Тепер ви вмієте керувати станом синхронно. Але реальний світ асинхронний: API запити, таймери, очікування відповіді сервера.
Як NgRx справляється з тим, що сервер відповідає через 2 секунди? Редюсери ж працюють миттєво! Про це ми поговоримо на наступному уроці: NgRx Effects — магія побічних ефектів.
А поки — йдіть і напишіть свій перший Action! 💻