Ось готовий урок, створений за твоїм майстер-промптом. Я дотримався стилю, структури та тону.
🎓 Тема уроку: NgRx Effects та робота з API
Привіт, друзі! Радий бачити вас. Це CS50 (умовно 😉), і сьогодні ми зануримося в одну з найважливіших тем сучасної Front-end розробки на Angular.
1. 🔥 Вступ: Проблема «Чистого ченця» та «Гінця»
Уявіть, що ви будуєте величезний банківський додаток. У вас є Store (Сховище) — це ваш "Священний Грааль" даних. Ви вже знаєте про Reducers (Редюсери).
Редюсер — це як монах-архіваріус, який живе в підвалі банку. Він працює ідеально, але в нього є суворе правило: він ніколи не виходить на вулицю. Ви кажете йому: "Запиши, що на рахунку стало на 100 грн більше" — він записує. Але якщо ви скажете: "Сходи в інший банк (на сервер API) і дізнайся курс валют", він відповість: "Ні. Я працюю тільки з тим, що маю тут і зараз. Я не вмію чекати".
У цьому й проблема: Редюсери — це чисті функції. Вони синхронні. Вони не можуть робити HTTP-запити, чекати відповіді сервера 2 секунди або обробляти помилки з’єднання.
Питання до вас: Якщо Редюсер не може спілкуватися з сервером, а Компонент не повинен (бо ми хочемо відокремити логіку від відображення), то хто, чорт забирай, зробить цей запит?
Тут на сцену виходить NgRx Effects.
Аналогія: * Component: Клієнт у ресторані, який робить замовлення (Action). * Reducer: Бухгалтер, який записує замовлення в книгу. * Effect: Офіціант. Він чує замовлення, йде на кухню (API), чекає, поки приготують, і приносить результат назад.
Без Effects ваш додаток — це просто красивий калькулятор, який не має зв’язку із зовнішнім світом.
2. 🧠 Теоретична база (Як це працює під капотом)
Давайте розберемо механіку. Не лякайтеся термінів, зараз все стане на місця.
NgRx Effects — це бібліотека для обробки Side Effects (побічних ефектів). Що таке "побічний ефект"? Це будь-яка дія, яка виходить за межі локальних обчислень: HTTP-запит, запис у LocalStorage, таймер тощо.
Головна формула (The Loop):
Це працює як нескінченний конвеєр.
- Ваш додаток викидає Action (наприклад,
[Users] Load Users). - Цей Action летить у Reducer (щоб, наприклад, поставити прапорець
loading: true). - ОДНОЧАСНО цей же Action перехоплює Effect.
- Effect бачить: "О, це для мене! Треба завантажити юзерів".
- Effect робить запит на сервер (через Service).
- Коли сервер відповідає, Effect генерує НОВИЙ Action (наприклад,
[Users] Load Successабо[Users] Load Failure). - Цей новий Action знову летить у Reducer, щоб оновити дані.
Що треба запам'ятати (The Mantra):
Effects слухають Actions і (зазвичай) повертають нові Actions.
Ключові оператори RxJS, які тут живуть:
Ви не зможете писати ефекти без RxJS. Ось "Свята Трійця", яку ви будете використовувати в 90% випадків:
switchMap: Найпопулярніший. Якщо користувач клацає кнопку "Завантажити" 10 разів поспіль,switchMapскасує попередні 9 запитів і виконає тільки останній. Це економить трафік.map: Трансформує дані (наприклад, відповідь сервера у Action успіху).catchError: Ловить помилки, щоб ваш додаток не впав (трансформує помилку в Action невдачі).
3. 🧪 Приклади (Від простого до реального)
Давайте подивимося на код. Я буду використовувати синтаксис createEffect (стандарт індустрії).
Приклад 1: "Ехо" (Найпростіший)
Уявіть ефект, який просто логує в консоль кожну дію.
logActions$ = createEffect(() => this.actions$.pipe(
tap(action => console.log('Я бачу цю дію:', action))
), { dispatch: false }); // Важливо! Ми не повертаємо нову дію.
Тут нічого складного, просто шпигун.
Приклад 2: Реальний HTTP-запит (Класика)
Завдання: Завантажити список фільмів.
Питання до студента: Як думаєте, скільки Actions нам знадобиться для однієї операції завантаження?
(Пауза для роздумів)
Правильна відповідь: Три.
1. loadMovies (Початок, вмикаємо спіннер).
2. loadMoviesSuccess (Успіх, дані прийшли).
3. loadMoviesFailure (Помилка, сервер впав).
Ось як це виглядає:
loadMovies$ = createEffect(() =>
this.actions$.pipe(
// 1. Слухаємо конкретну дію
ofType(MoviesActions.loadMovies),
// 2. Йдемо на сервер (використовуємо switchMap для безпеки)
switchMap(() => this.moviesService.getAll().pipe(
// 3. Якщо успіх — повертаємо Action з даними
map(movies => MoviesActions.loadMoviesSuccess({ movies })),
// 4. Якщо біда — повертаємо Action з помилкою
catchError(error => of(MoviesActions.loadMoviesFailure({ error })))
))
)
);
Чому результат такий?
Зверніть увагу: catchError знаходиться всередині switchMap. Це критично! Якщо ви винесете його назовні, то при першій же помилці ваш Effect "помре" і перестане слухати нові натискання кнопок.
Приклад 3: Пошук з автодоповненням (Магія RxJS)
Уявіть рядок пошуку. Користувач друкує швидко. Ми не хочемо "бомбити" сервер на кожну букву.
searchMovies$ = createEffect(() =>
this.actions$.pipe(
ofType(MoviesActions.search),
debounceTime(300), // Чекаємо 300мс тиші перед запитом
distinctUntilChanged(), // Не шукаємо те саме двічі
switchMap(action =>
this.moviesService.search(action.query).pipe(
map(movies => MoviesActions.searchSuccess({ movies })),
catchError(error => of(MoviesActions.searchFailure({ error })))
)
)
)
);
Аналіз: Цей код робить те, на що в "чистому" JavaScript пішло б 50 рядків коду з таймерами та перевірками. Ось вона, сила Effects!
4. 🛠 Практична частина
Тепер ваша черга. Відкривайте IDE (або уяву).
Завдання 1: Login Effect
Напишіть ефект для входу в систему.
* Вхідний Action: [Auth] Login (має username та password).
* Сервіс: authService.login(u, p).
* Успіх: [Auth] Login Success (з токеном).
* Невдача: [Auth] Login Failure (з повідомленням).
Завдання 2: "А що, якщо..." (Redirect)
Після успішного логіну ([Auth] Login Success) ми хочемо перенаправити користувача на сторінку /dashboard.
* Підказка: Ефекти можуть не тільки діспатчити нові екшени, але й просто виконувати код (наприклад, router.navigate). Не забудьте { dispatch: false }.
Завдання 3: Виправ помилку Junior-розробника Джуніор написав такий код:
loadUsers$ = createEffect(() => this.actions$.pipe(
ofType('[Users] Load'),
mergeMap(() => this.api.getUsers()), // Помилка тут!
map(users => ({ type: '[Users] Success', users }))
// А де catchError?
));
- Що станеться, якщо сервер поверне 500 (помилку)?
- Що станеться з потоком (Observable)? Він продовжить працювати чи завершиться?
Завдання 4: Міні-кейс Ви робите кнопку "Лайк". 1. Користувач тисне лайк. 2. Ви хочете миттєво оновити UI (оптимістичне оновлення), не чекаючи сервера. 3. Effect відправляє запит на сервер. 4. Якщо сервер повернув помилку — треба "відкотити" лайк назад. * Питання: Які Actions вам потрібні?
5. 💡 Мислення як у розробника
Як відрізнити новачка від профі в NgRx Effects?
Типові помилки новачків:
1. Логіка в Ефектах: Ефект починає фільтрувати масиви, рахувати суми... Ні! Ефект — це кур'єр. Обчислення — в Редюсері або Селекторах.
2. Забутий catchError: Один раз API впало — і ваша кнопка "Зберегти" більше не працює до перезавантаження сторінки.
3. Пекло вкладеності: Писати subscribe всередині Ефекту. Ніколи так не робіть. Використовуйте map/switchMap.
Як думає досвідчений інженер:
"Так, мені треба дані. Чи можу я отримати їх синхронно? Ні. Значить, Effect.
Який оператор злиття мені потрібен? switchMap для читання, concatMap для запису (щоб порядок зберігся), mergeMap для паралельних задач.
Як я оброблю помилку, щоб не вбити потік?"
Золоте правило: Effects повинні бути максимально худими. Отримав Action $\rightarrow$ викликав Service $\rightarrow$ повернув Action. Все.
6. 🧩 Підсумок
Отже, що ми сьогодні розібрали?
- Reducers — це "ченці" (синхронні, чисті). Їм не можна дзвонити на сервер.
- Effects — це "кур'єри" (асинхронні). Вони з’єднують ваш додаток із зовнішнім світом.
- Ми навчилися писати ланцюжок:
Action$\rightarrow$Effect$\rightarrow$API$\rightarrow$New Action. - Ми зрозуміли, чому обробка помилок всередині
switchMap— це питання життя і смерті вашого функціоналу.
Що ви тепер вмієте: Ви можете зробити свій додаток "живим". Він може ходити в інтернет, шукати дані, обробляти помилки і при цьому залишатися передбачуваним.
Тизер: Наступного разу ми поговоримо про Selectors (Селектори). Уявіть, що ви завантажили 10 000 товарів, але на екрані треба показати лише 5 червоних. Як це зробити, не змушуючи Angular гальмувати? Відповідь — мемоізація. Але про це — на наступному уроці.
Це був CS50... тобто, урок про NgRx. Щасти вам у коді!