Модуль 28

NgRx Store, Actions та Reducers

Ось твій урок у стилі 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. 🧩 Підсумок

Отже, що ми маємо в сухому залишку?

  1. Store — це "Дошка оголошень" (єдине джерело правди).
  2. Actions — це "Повідомлення" (наміри щось змінити).
  3. Reducers — це "Правила" (чисті функції, що оновлюють стан).
  4. Компоненти більше не займаються складною логікою даних, вони лише відображають їх і диспатчать команди.

Що ви тепер вмієте? Ви можете створити додаток, де стан керується централізовано, передбачувано і красиво.

👀 Тизер наступного уроку: Але стривайте! Ми працювали тільки з синхронними даними (1, 2, 3...). А що, коли нам треба завантажити дані з сервера (HTTP запит)? Редюсер повинен бути миттєвим, він не може чекати на сервер! Тут на сцену виходить NgRx Effects. Це місце, де живе "брудна робота" з API.

Але про це... наступного разу.

Успіхів у коді! Це був CS50... тобто, урок про NgRx. 👋