Ось твій урок у стилі CS50. Вмикай уяву, ми починаємо! 🚀
🎓 Урок: NgRx Store, Actions та Reducers
(Або: Як перестати губити дані у великому застосунку)
Привіт, друзі! Мене звати [Твоє Ім'я], і це — введення в NgRx.
Сьогодні ми не просто вивчимо нову бібліотеку. Ми змінимо те, як ви думаєте про дані у ваших Angular-застосунках.
1. 🔥 Вступ: Хаос на кухні
Уявіть собі великий, гамірний ресторан. Це ваш застосунок. У вас є Офіціант (Component A), Шеф-кухар (Component B) і Менеджер залу (Component C).
Клієнт замовляє піцу без оливок. 1. Офіціант біжить до Шефа: "Піца, без оливок!" 2. Шеф кричить Менеджеру: "Стіл №5 зайнятий!" 3. Менеджер махає Офіціанту: "Забери замовлення!"
А тепер уявіть, що Офіціант забув сказати про оливки, а Менеджер не почув Шефа. Що ми маємо? Хаос. Клієнт незадоволений, дані розсинхронізовані.
У світі Angular це називається Input/Output пекло. Коли ви передаєте дані через 10 компонентів вглиб, і десь посередині вони губляться.
❓ Риторичне питання:
Хіба не було б чудово, якби в центрі ресторану висіла величезна біла дошка, де записано абсолютно все, що відбувається зараз? * "Стіл 5: чекає піцу". * "Кухня: готує піцу". * "Склад: залишилось 10 кг борошна".
Кожен дивиться на дошку і знає правду. Ніхто не шепочеться по кутках. Ось ця "Дошка" — і є NgRx Store.
Навіщо це нам? Щоб стан вашого додатку був передбачуваним. Щоб ви завжди знали, хто, коли і як змінив дані.
2. 🧠 Теоретична база (Що там «під капотом»?)
NgRx реалізує патерн Redux. Це звучить страшно, але насправді це всього лише три кити, на яких тримається світ.
1. Store (Сховище / Наша Дошка)
Це єдине джерело правди. Це просто великий JavaScript-об'єкт (JSON), який містить усі дані вашого додатка. * Інтуїтивно: Це база даних у пам'яті браузера.
2. Actions (Екшени / Події)
Це те, ЩО сталося.
У звичайному коді ви б викликали метод service.updateUser(). В NgRx ви не змінюєте дані напряму. Ви "оголошуєте" на весь ресторан: "Увага! Користувач натиснув кнопку 'Купити'!".
* Екшн — це простий об'єкт з типом події і (опціонально) даними.
* Запам'ятай: Екшени — це просто повідомлення. Вони самі нічого не змінюють.
3. Reducers (Редюсери / Архіваріуси)
Це серце логіки. Редюсер — це функція, яка бере старий стан + екшн і видає новий стан. * Аналогія: Уявіть суворого бухгалтера. * Старий баланс: 100 грн. * Екшн: "Витратив 20 грн". * Бухгалтер рахує (100 - 20) і записує новий рядок: 80 грн. * Важливо: Редюсер ніколи не змінює (мутує) старі дані. Він створює копію даних із внесеними змінами. Це називається Immutability (Незмінність).
3. 🧪 Приклади: Від простого до реального
Давайте побудуємо найпростіший приклад: Лічильник (Counter).
Крок 1: Оголошуємо події (Actions)
Що ми можемо робити з лічильником? Збільшувати, зменшувати і скидати.
import { createAction } from '@ngrx/store';
// Ми даємо унікальне ім'я події у квадратних дужках, щоб легко знайти її в логах
export const increment = createAction('[Counter Component] Increment');
export const decrement = createAction('[Counter Component] Decrement');
export const reset = createAction('[Counter Component] Reset');
Крок 2: Пишемо логіку (Reducer)
Що ви очікуєте тут побачити? Якусь функцію, яка каже: "Якщо прийшов increment, додай +1". Саме так!
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset } from './counter.actions';
export const initialState = 0; // Початковий стан — нуль
export const counterReducer = createReducer(
initialState,
// Синтаксис: on(ЯкаПодія, (поточнийСтан) => новийСтан)
on(increment, (state) => state + 1),
on(decrement, (state) => state - 1),
on(reset, (state) => 0)
);
Крок 3: Використання в компоненті (Store)
Тепер компоненту не треба самому рахувати count++. Він просто:
1. Слухає зміни (select).
2. Відправляє команди (dispatch).
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { increment, decrement, reset } from './counter.actions';
@Component({
selector: 'app-my-counter',
template: `
<button (click)="onIncrement()">Increment</button>
<div>Current Count: {{ count$ | async }}</div>
<button (click)="onDecrement()">Decrement</button>
<button (click)="onReset()">Reset</button>
`,
})
export class MyCounterComponent {
// Знак долара $ натякає, що це потік (Observable)
count$: Observable<number>;
constructor(private store: Store<{ count: number }>) {
// Ми підписуємось на шматочок 'count' з нашої глобальної дошки
this.count$ = store.select('count');
}
onIncrement() {
// Ми не кажемо "зміни змінну". Ми кажемо "Відправ повідомлення!"
this.store.dispatch(increment());
}
onDecrement() {
this.store.dispatch(decrement());
}
onReset() {
this.store.dispatch(reset());
}
}
Чому результат такий?
Коли ви тиснете кнопку → dispatch(increment) летить у Store → Store будить Reducer → Reducer бере 0, додає 1, повертає 1 → Store оновлюється → count$ в компоненті автоматично отримує нове значення і оновлює екран. Магія! ✨
4. 🛠 Практична частина
Час забруднити руки кодом. Не просто читайте — спробуйте це!
Завдання 1: "База"
Створіть новий Angular проєкт, встановіть NgRx (ng add @ngrx/store) і реалізуйте приклад з лічильником вище. Переконайтеся, що цифри змінюються.
Завдання 2: "Payload" (Вантаж)
Нам нудно додавати по +1.
* Створіть новий Action incrementBy, який приймає число (наприклад, props<{ value: number }>()).
* Додайте кнопку "+5", яка відправляє incrementBy({ value: 5 }).
* Оновіть Reducer, щоб він використовував це значення: (state, props) => state + props.value.
Завдання 3: "Логіка бізнесу"
Зробіть так, щоб лічильник не міг стати меншим за нуль.
* Де ви це напишете? В компоненті? Ні! Вся логіка живе в Reducer.
* Підказка: on(decrement, (state) => state > 0 ? state - 1 : 0)
Завдання 4: "А що, якщо..." (Bug Hunt)
Уявіть, що ви натискаєте Reset, але нічого не відбувається. Консоль чиста.
* Де шукати проблему?
* Перевірте, чи додали ви StoreModule.forRoot({ count: counterReducer }) у ваш app.module.ts. Без реєстрації редюсера Store — це просто порожня кімната.
5. 💡 Мислення як у розробника
Як відрізнити новачка від профі в NgRx?
❌ Помилка новачка:
Використовувати Store як смітник.
* Action: setData
* Action: updateData
* Action: changeData
Це погано! Ви втрачаєте контекст.
✅ Думка профі:
Actions — це унікальні події. Називайте їх так, як вони звучать у реальному житті.
* [Login Page] User Login Success
* [Product List] Load Products Failed
* [Settings] Dark Mode Toggled
Коли ви відкриєте Redux DevTools (спеціальний плагін для браузера) і побачите історію:
1. Login Success
2. Load Products
3. Add to Cart
Ви одразу зрозумієте, що робив користувач. Це як читати книгу, а не розбирати спагеті-код.
Порада з практики:
Ніколи не змінюйте state напряму в Reducer (наприклад, state.users.push(newUser)). Це зламає механізм відстеження змін Angular. Завжди повертайте новий об'єкт/масив (оператор spread ... ваш найкращий друг: [...state.users, newUser]).
6. 🧩 Підсумок
Отже, що ми маємо в сухому залишку?
- Store — це "Дошка оголошень" (єдине джерело правди).
- Actions — це "Повідомлення" (наміри щось змінити).
- Reducers — це "Правила" (чисті функції, що оновлюють стан).
- Компоненти більше не займаються складною логікою даних, вони лише відображають їх і диспатчать команди.
Що ви тепер вмієте? Ви можете створити додаток, де стан керується централізовано, передбачувано і красиво.
👀 Тизер наступного уроку: Але стривайте! Ми працювали тільки з синхронними даними (1, 2, 3...). А що, коли нам треба завантажити дані з сервера (HTTP запит)? Редюсер повинен бути миттєвим, він не може чекати на сервер! Тут на сцену виходить NgRx Effects. Це місце, де живе "брудна робота" з API.
Але про це... наступного разу.
Успіхів у коді! Це був CS50... тобто, урок про NgRx. 👋