Полный курс по JavaScript + React - с нуля до результата (2022)

JavaScript React FrontEnd Redux

Основы JS

001 Что такое JS и как его подключить к странице

Подключается скрипт к сайту таким образом. Больше никаких тегов внутри прописывать не надо. Если присутствует тег «type», то его нужно будет убрать. Скрипт мы помещаем в самый конец, так как он работает только с существующей вёрсткой на сайте и так же может долго загружаться, что застопорит загрузку сайта

003 Переменные и строгий режим

Объявлять переменные через varнельзя. В современных структурах она может принести очень много вреда. Дело в том, что такая переменная не учитывает своё положение в коде и видна во всех областях видимости. Приведённый ниже код сработает и вернёт undefined:

Данная директива позволяет писать код только по современному защищённому стандарту, исключая старый

Стиль написания кода JS:

004 (д) Правила и типы названия переменных

Наименования названий переменных должны быть понятны для любого человека, который читает наш код

Стиль наименования переменной «snake_case» или написание в камеле, но с нижнего подчёркивания используется для написания константы (которую точно не стоит менять в коде)

005 Классификация типов данных в JavaScript

В первом столбце – примитивные типы данных, во втором – комплексные (могут хранить в себе разные типы данных)

Так же мы можем получить при определённых операциях Infinity (когда получаем бесконечность) и NaN (когда ожидаем выполнение числовой операции, но вступают в расчёт другие числа)

Объект представляет из себя набор свойств (ключ + значение). Массив – это частый случай от типа данных объект (в качестве ключа используется индекс). Поэтому работа с массивом и объектом достаточно похожа

006 (д) Разница между объектами и массивами и неочевидные синтаксические возможности

Разница между объектом и массивом заключается в разных прототипах (которые определяют функционал)

007 Простое общение с пользователем

Самый простой способ общения с пользователем – это alert(), confirm() и prompt(). Последняя функция всегда возвращает строковое значение от пользователя

Так же можно вернуть и число (динамическая типизация в число)

Так же можно сделать серию вызовов диалоговых окон и предоставить их пользователю.

Document.write()- перезатирает страницу и выводит нашу переменную

008 Интерполяция (ES6)

Интерполяция в JS реализуется с использованием ${действия}, либо через сложение строк

009 Операторы в JS

Все операторы присутствуют на MDN

013 Практика, ч.1. Начинаем создавать приложение

Задание:

Реализация:

014 Условия

015 (д) Логические операторы

016 Циклы

017 (д) Цикл в цикле и метки

Реализация вложенного цикла:

Метки. Можно отправить цикл выполнять другой цикл, который является его частью (тут если k === 2, то будет выполняться самый первый цикл).

continue и break работают похожим образом как и в других языках

018 Практика, ч2. Применяем условия и циклы

Тут стоит отметить, что если пользователь нажал на «отмену» при вводе значения в prompt, то к нам вернётся null

'use strict';
 
let numberOfFilms = +prompt('Сколько фильмов вы посмотрели?')
 
 
const personalMovieDB = {   
	count: numberOfFilms,   
	movies: {},   
	actors: {},   
	genres: [],   
	private: false,
}
 
	while (true){   
	if (numberOfFilms >= 0) {    
	    if(personalMovieDB.count < 10){   
			alert('Просмотрено довольно мало фильмов') 
			break 
		} else if(personalMovieDB.count >= 10 && personalMovieDB.count <= 30){    
			alert('Вы классический зритель')   
			break     
		} else if(personalMovieDB.count > 30){     
	          alert('Вы киноман')
	          break
		}
	} else {
		alert('Вы ввели не число или неверное число')   
	}
}
 
let name, mark;
 
cycle: for (let i = 0; i < 5;){  
	name = prompt('Один из последних просмотренных фильмов?');
	mark = prompt('На сколько оцените его по десятибальной шкале?', '5'); 
	if (name != null && name != ''
		&& name.length < 50
		&& mark != ''
		&& mark != null
		&& mark >= 0
		&& mark <= 10) {       
		personalMovieDB.movies[name] = mark;
		i++; 
	} else {     
		alert('Одно из введённых вами значений неверно');     
		continue cycle;
	}
}

И стоит отметить, что если мы тут оставим «+», то при отмене ввода значения пользователем, данный оператор переведёт значение null в 0

019 Функции, стрелочные ф-ции (ES6)

Виды функций:

020 (д) Еще раз про аргументы функций

021 (д) Про важность return

Оператор return в первую очередь нам нужен для того, чтобы вернуть результат из функции (например в переменную, которая вызвала эту функцию)

Так же можно вернуть значение из цикла (и цикл сразу же завершится)

Но так же важно сказать, что функция (когда она не имеет return) всегда возвращает undefined. И console.log() тоже всегда возвращает undefined

022 Методы и свойства строк и чисел

Основные методы строк и чисел:

023 Практика , ч3. Используем функции

Тут хочется показать не очевидное условие. Нам нужно, чтобы пользователь в любом случае ввёл число. Чтобы функция работала, как надо, нужно сформулировать такое условие:

let numberOfFilms;
 
function numberChecker() {
	numberOfFilms = +propmpt('Сколько фильмов вы посмотрели?');
	while (numberOfFilms == '' || numberOfFilms == null || isNan(numberOfFilms)) {
		numberOfFilms = +propmpt('Сколько фильмов вы посмотрели?');
	}
}
 
numberChecker();

024 (д) Метод trim()

Метод trim() стоит использовать почти везде, где есть ввод данных пользователем. Это позволяет избегать пустых строк и огромного количества пробелов, так как данный метод убирает лишние пробелы из строки

025 Callback- функции

Callback-функция - это функция, которую мы передаём в другую функцию и выполняем там же

Сюда мы передаём анонимную функцию (исчезнет после использования):

А сюда мы передаём стрелочную:

026 Объекты, деструктуризация объектов (ES6)

Тут мы получаем ключ и его значение из объекта, а так же проходимся по объектам внутри объекта

Это пример части деструктуризации объекта, где мы получаем из вложенного объекта movies его внутренние свойства

027 Массивы и псевдомассивы

030 Основы ООП, прототипно-ориентированное наследование

Тут описывается цепочка наследования и особенности ООП в JS, где все элементы языка являются объектами

const car = {
  model: 'Bentley',
  ride: () => {
    console.log('врум-врум');
  }
}
 
const Bentley = {
	model: 'H720P',
}
 
// Bentley.__proto__ = car; // устаревший способ указать прототип
// Object.setPrototypeOf(Bentley, car); // более современный способ создания прототипа
 
const Proshe = Object.create(car); // тут сразу реализован прототипно-наследуемый объект
 
Proshe.ride();

031 Практика , ч4. Используем объекты

Очень важный мем! Чтобы проверить, что наш пользователь ввёл число, нужно проверять не само равенство введённого значения к типу (count === Number), а тип введённого числа к типам, которые выдаёт typeof (пример на рисунке)

Выводим любимый жанр и его номер

Так же есть ещё один интересный способ, как можно принять значения от пользователя в виде массива. Метод split(‘, ’) позволяет указать разделитель для строчки и по этому разделителю разложить значения в массив.

Тут впринципе показана реализация, когда мы приводим сначала строку к нижнему регистру (потому что сортировка, которая идёт дальше, будет производиться по юникоду, а в юнкоде сначала идут заглавные буквы)

032 Отлавливаем ошибки в своем коде при помощи консоли разработчика. Breakpoints

На данной вкладке нашего инструмента разработчика находится инструмент просмотра сурсов и дебаггинга

На номерах строк мы ставим маркеры. Справа находятся кнопки стартаа программы, перехода в другую функцию (вызванную), входа внутрь функции, выхода из функции (она выполнится) и выполнения одного шага. Так же есть снятие маркера и паузы на ошибках

033 Динамическая типизация в JS

Способы перевода в другие типы данных:

034 Замыкание и лексическое окружение

В данном примере кода мы доходим до такого понятия как лексическое окружение или область видимости. Любая переменная – это свойство объекта лексического окружения. И мы имеем внутреннее лексическое окружение (например, область той же функции) и внешнее (всё, что находится на глобальном уровне).

Тут стоит пояснить, что бОльший приоритет имеют свойства внутренней области, но так же внутреннее окружение может брать значения из внешнего. Так же те же функции берут самое последнее значение переменной, которая может изменяться далеко дальше по коду

Тут функция так же будет брать только актуальное значение переменной (6, а затем уже 8). Так же тут же отметим, что при каждом вызове функции, у нас создаётся разное лексическое окружение, что говорит нам о том, что в функции в разных местах кода спокойно могут находиться разные значения переменных. Но так же эти пространства удаляются, когда они выполняются и полностью исчезают из выполненного программного кода

Тут так же можно увидеть, что при вызове функции у нас создаётся определённая область видимости, где сохраняются ссылки на переменные, которые были им доступны на время создания этих функций (создание == вызов)

035 Задачи с собеседований на понимание основ

036 Получение элементов со страницы

На нашей странице располагается бокс. Через метод getElementById мы можем получить (один) элемент по его id, указанному в HTML-вёрстке, и выводим его в консоль разработчика.

Обращаемся мы к document, так как это наша страница, с которой мы работаем.

Метод getElementsByTagName позволяет получить псевдомассив элементов по их тегу (div, button, li and etc…)

Здесь мы получаем уже не один элемент, а псевдомассив - это массив, который не имеет методов высшего порядка массивов

Но мы так же можем обратиться к определённому элементу этого псевдомассива и получить нужное нам значение через массивное указание индекса элемента ([число])

Тут стоит отметить, что даже если у нас на странице будет всего один элемент, то на выходе мы так же получим HTMLCollection (псевдомассив), но из одного элемента

Тут уже псевдомассив элементов вызываем по имени класса с помощью метода getElementsByClassName()

Все вышеописанные методы являются устаревшими. Сейчас для получения элементов со страницы используются два современных метода: querySelector и querySelectorAll. Они позволяют обращаться к элементам как при использовании CSS-селекторов. То есть тут уже для указания элемента нам нужно будет прописывать ., #, :, * и так далее. Такой способ рекомендуется к использованию и является более правильным. Так же он имеет один из методов массива - forEach()

037 Действия с элементами на странице

Так же мы можем добавить инлайновые стили определённому элементу страницы через имя_элемента.style.стиль = 'значение[px]'.

Тут так же нужно отметить, что мы в основном имеем дело в псевдомассивами, поэтому нужно не забывать обращаться к элементам по индексу.

Так же нужно отметить, что на элемент можно повесить стили через обращение к элементу cssText, который представляет из себя просто набор текста стилей элемента элемент.style.cssText = 'стили'

И вот тут представлено создание нового элемента нашей страницы с классом black (в CSS заранее прописаны свойства цвета, ширины и высоты данного блока по названию класса):

  • createElement() - позволит создать элемент на странице по тегу
  • classList - свойство объекта, которое позволяет производить манипуляции над классом элемента на странице
    • add()- метод добавления класса элементу
    • remove() - метод, который позволит удалить определённый стиль с элемента
  • append() - добавляет определённый элемент в конец
  • querySelector().append() - такая конструкция позволит добавить элемент в конец определённого блока кода

Это второй вариант записи обращения к элементу (тут мы запихиваем див во враппер)

Так же у нас есть возможность добавить элемент в начало объекта, но через другой метод: prepend()

Так же мы можем поместить элемент на страницу через метод before() до того элемента, на котором мы вызвали данный метод

Так же мы можем поместить элемент после того, на котором вызвали функцию after()

Но есть куда более старый метод, который тоже подойдёт для вставки элемента - insertBefore(). Ему нужно передать в качестве аргументов два объекта: вставляемый объект и тот, перед которым вставляем

Тут уже представлено удаление одного из трёх кружков. Осуществляется удаление элемент через вызов на нём метода remove()

Раньше был актуален метод removeChild(), который позволял удалить дочерний элемент из родительского

Так же мы имеем возможность крайне просто и быстро поменять объекты местами между друг другом с помощью метода заменяемый.replaceWith(заменяем_на)

И вот ещё один абсолютно аналогичный по логике выполнения код, но более старый по записи replaceChild()

И вот тут уже нужно познакомиться с двумя свойствами innerHTML и innerText.

Первый добавляет на страницу код HTML, а второй добавляет только текст (который мог ввести, например, пользователь)

Метод insertAdjacentHTML(позиция, значение) позволяет вставить HTML-код несколькими заранее определёнными способами. В качестве первого аргумента мы передаём способ вставки, в качестве второго – сам код.

  • afterbegin - вставит код в самом элементе, на котором вызываем данную функцию
  • afterend - вставит после блока
  • beforebegin - вставит до блока
  • beforeend - вставит как и первый в самом элементе, на котором вызываем данную функцию

Так же при инициализации HTML элемента в JS можно указывать откуда мы будем конкретно брать вложенные элементы. То есть, мы можем искать элементы не по всему документу, а внутри враппера, который мы импортнули в JS

038 Практика. Задание на отработку действий со страницей

Дано 5 задач и начальный объект, внутри которого располагается массив

Приведены мои решения и решения преподавателя

Тут хочется обратить внимание на textContent. Он позволяет обратиться к контенту внутри блока

039 События и их обработчики

Существует огромное количество событий в JS. Все они применяются при взаимодействии пользователя и нашей формы

Самое первое и простое взаимодействие – это нажатие кнопки пользователем, которое мы можем прописать прямо в теге (но так делать не рекомендуется)

Так будет правильнее писать событие, но всё равно этот метод достаточно устаревший. Этот код плох тем, что мы можем забыть и переназначить кнопке другое действие далее в коде. А это уже точно приведёт к сбою логики программы

Это уже современная форма записи через addEventListener(). Первым делом записывается название самого события (нажатие 'click'), уже потом вставляем колбэк-функцию, которая сработает при этом событии

'mouseenter' – вызывает срабатывание ивента при наведении мыши на элемент

Выводим сам ивент (так же можно ввести event.target – выведет элемент (на котором произошло событие), event.type – тип ивента)

Удаляет блок кода (сам элемент), на который навелись мышкой

Так же мы имеем removeEventListener(событие, функция), которая позволяет удалить само событие с элемента

Тут логика нашей программы описывается так: у нас есть чекер кликов, после нажатия на первую кнопку – она пропадает и вместе с ней удаляется чекер кликов со второй кнопки

Тут показаны вложенные события. Когда на родительсском и дочернем элементе находится один и тот же обработчик. Сначала всегда срабатывает вложенное событие

Свойство currentTarget указывает на событие каждого конкретного элемента по отдельности (сработали отдельно дочерний, потом родительский элементы)

event.preventDefault() – позволяет отменить выполнение стандартной логики для определённого элемента (например, отменяет перезагрузку страницы при нажатии на кнопку-сабмит (отправка) формы)

Так же мы можем через forEach() повесить событие сразу на все элементы псевдомассива

Так же в mdn можно увидеть, что метод, добавляющий ивенты, может принимать в себя до трёх значений, последний из которых - модификатор

Пример использования опции:

040 Навигация по DOM - элементам, data-атрибуты, преимущество forof

Мы можем легко вывести и увидеть элементы через JS. Тут хочется выделить такую особенность, что documentElement – это <html></html> тег, внутри которого всё находится.

Так же, когда мы используем childNodes (вывод всех дочерних элементов), мы можем увидеть очень много разных объектов. В качестве text выступает перенос строки, который мы можем увидеть в коде, но который не отображается

И тут уже стоит пояснить, что в ДОМ-дереве не всё является элементами или узлами. Элемент – это видимая часть HTML, а узел, в свою очередь, – это та связующая часть между элементами и текст. Например, кнопка – это элемент, а текст в кнопке – это узел (нода)

parentNode выводит родительский узел и повторять его можно много раз

Так же мы можем обращаться к определённым атрибутам в HTML-коде (тут так же работает логика CSS, так как обращение к атрибуту там идёт через [такой синтаксис])

Используя предыдущий синтаксис, мы можем обратиться к предыдущему и следующему объекту (тут как предыдущий, так и следующий объект – это текстовая нода переноса строки)

Но такими методами неудобно пользоваться, так как мы можем попасться на ноду переноса строки, что нам может быть и не нужно

Выводит предыдущий/следующий элемент после того, к которому обратились

firstElementChild - выводит первого ребёнка в выбранном элементе parentElement - выводит родителя выбранного элемента

И вот как данный функционал пишется: мы перебираем все элементы объекта body, и если имя элемента text, то данная итерация цикла пропускается (continue – пропускает итерацию цикла, break – завершает цикл досрочно)

041 Рекурсия

Рекурсия - это функция, которая выполняет саму себя конечное количество раз.

Рекурсия позволяет множество раз перевыполнять код до выполнения поставленной задачи.

Так же имеются основные понятия как: база рекурсии, шаг рекурсии и глубина рекурсии

Нужно сразу упомянуть, что рекурсии менее производительны, нежели чем стандартные функции языка по перебору информации. Однако рекурсии более удобны в большинстве остальных методов

Первый метод Object.values() переводит объект в массив, второй метод Array.isArray() проверяет является ли объект массивом

Тут представлен объект, который хранит в себе данные о студентах и завершённости прохождения курса

И нам нужно создать переборщик объекта, который выведет общий процент прхождения курсов. Основная проблема этого переборщика заключается в том, что при переборе объекта он не зайдёт в ещё более вложенные объекты кроме тех, что мы описали – это и есть ограничение нерекурсивных методов

И вот представление кода с рекурсией. Он уже может обработать объекты любой сложности и вложенности, так как при каждой встрече объекта, он переходит во второй условный блок кода, где располагается else и там запускает массив заново, чтобы дойти до массива, где сработает блок кода true

С представленным выше объектом оба массива справляются одинаково

Но если немного изменить структуру

042 Практика. Используем события на странице проекта

Задание:

DOMContentLoaded – браузер полностью загрузил HTML, было построено DOM-дерево, но внешние ресурсы, такие как картинки <img> и стили, могут быть ещё не загружены

Статья по событию DOMContentLoaded

обращение ко всем изображениям внутри класса

const ads = document.querySelectorAll('.promo__adv img');

Тут находятся все переменные в проекте (и необычные селекторы вызова)

Это одна из самых сложных частей данного сайта – формирование списка фильмов. Первым делом, в текст вкладываемого родителя мы вставляем пустую строку, которая нам позволит очистить имеющиеся айтемы списка <li>. Дальше мы сортируем массив. Потом мы заменяем элемент списка, и именно вставляем новый айтем включая добавление HTML-структуры. Добавляем структуру через «+=», так как мы добавляем айтем списка - <li>.

Потом мы добавляем функционал удаления фильма из писка элементов. Сначала вызываем иконки с классом delete, в которые через forEach вставляем ивент, который будет срабатывать при нажатии click. Внутри ивента будем обращаться к родителю иконки и удалять его. Удалять уже фильм из массива будем через метод splice, в который поместим номер элемента (второй аргумент forEach) и количество элементов. Потом уже вызовем рекурсией наш фонрмировщик списка (для формирования обновлённой структуры)

Это дополнительные методы. Первый удаляет рекламу со страницы. Второй вносит изменения в жанре и фоне. Третий сортирует массив. Четвёртый проводит проверку наличия фильма в массиве (вызываем тоже при нажатии кнопки)

Это полное описание события ‘submit’, которое хранит в себе порядок действий при нажатии на кнопку в форме

А тут мы вызываем методы для изменения нашего списка (внутри него и сортировка тоже), внесения изменений (смена жанра и заднего фона) и удаления рекламы

043 События на мобильных устройствах

На телефоне в основе своей нет кликов мышкой – есть тачи. Однако события с кликами нормально отрабатывают и в мобильных версиях браузеров

Мобильных событий всего 6:

  • touchstart – возникает при соприкосновении
  • touchmove – возникает при перемещении пальцем
  • touchend – палец отпускается от элемента с данным ивентлистенером
  • touchenter – срабатывает как только палец при скольжении попадает на элемент с данным ивентом
  • touchleave – срабатывает когда палец выходит за пределы элемента
  • touchcancel – срабатывает тогда, когда точка соприкосновения не регистрируется на поверхности (если палец выйдет за пределы браузера)

И вот примеры отображения ивентов в браузере. Первый ивент срабатывает при первом клике на объект, второй ивент срабатывает когда мы тыкаем по объекту и водим по нему, третий срабатывает, когда мы отпускаем палец с объекта

Так же стоит упомянуть, что у ивентов есть свои методы. И вот три из них, которые позволяют отслеживать тачи относительно объекта:

Сама информация о тачах представляет из себя: положение в пространстве, поворот, радиус, количество пальцев и так далее

Вот пример вывода всех методов о клике и вывода положения по X пальца, который перемещается по боксу

Ресурс, который позволит немного упростить работу с тачами - https://hammerjs.github.io/

044 Async, defer, динамические скрипты

Тут нужно упомянуть, что при расположении скрипта в <head> страницы у нас вылазит две проблемы:

  1. Так как скрипт загрузился, а страница нет, то скрипту не с чем будет работать и он выдаст ошибку
  2. Сам по себе скрипт стопорит загрузку HTML-страницы и при большом его весе у нас страница может долго загружаться

Но так же перед нами может встать другая проблема: очень большой проект. Сайт может содержать в себе тысячи элементов, которые могут очень долго загружаться. Это приведёт к тому, что скрипт может не сработать в тех местах, где нам это нужно было

Атрибут defer сообщает браузеру, чтобы он продолжал загружать страницу, но так же браузер должен загружать и скрипт в фоновом режиме, а затем запустить скрипт, когда он загрузится.

  1. Скрипты с defer никогда не блокируют страницу
  2. Скрипт запустится только тогда, когда DOM-дерево уже готово

Так же ещё одной особенностью является то, что скрипты выполняются последовательно друг за другом, даже если какой-то загрузился раньше.

Это позволяет нам закинуть те же скрипты в хедер сайта, но гугл сео-тест будет ругаться на такое расположение скриптов

Ещё один атрибут, который модифицирует работу скриптов – это async.

  1. Страница не ждёт асинхронных скриптов – содержимое загружается и обрабатывается
  2. Событие DOMContentLoaded и асинхронные скрипты не ждут друг друга

Тут уже нужно убедиться, что скрипты не зависят от DOM-структуры, так как выполняются они сразу же

И вот пример подключения скриптов по нужному порядку к сайту

045 Ресурсы для оттачивания навыков программирования

СсылОчки:

Freecodecamp Hackerrank Leetcode Codewars

Дополнительные основы JavaScript

002 (д) Оператор нулевого слияния (Nullish) ES11

И тут у нас представлен код, который изменяет размер элемента. Как мы видим, перед нами встаёт проблема: нам нужно проработать ситуацию, когда значение пользователь не передал. Чтобы быстро решить проблему с тем, что нам нужно заменить отсутствующее значение на значение по умолчанию, можно внутрь ${} поместить быструю проверку, которая будет срабатывать в том случае, если переданное значение будет иметь: null, undefined, 0, null, NaN.

Сейчас мы можем не передать неверное значение или не передавать его, но встаёт другая проблема: а если нам нужно передать 0?

В JS есть оператор Nullish, который возвращает результат в любом случае, кроме null и undefined

То есть, если username существует, то выведем User, в противном случае, то, что идёт дальше. И только в последнем случае username существует, поэтому выводится он (его значение)

И тут мы уже поправили логику программы. Теперь она может принять в себя 0 и выдать нужный результат. Так же можно использовать данный оператор в математических операциях

Нулиш операторы нельзя использовать вместе с логическими операторами

003 (д) Оператор опциональной цепочки (.) ES11

Оператор опциональной цепочки проверяет значение слева от него. Если оно имеет значение null или undefined, то выполнение после оператора останавливается.

В данном случае, нам нужно вывести контент блока, если тот существует. Чтобы сделать это обычным способом, нам нужно использовать конструкцию с if (потому что без проверки выпадет ошибка). Но можно сократить её и просто поставить ?. после этого блока, проверяя его

Однако проверка не работает на присвоение значения

Пример реального использования такого оператора. К нам пришло много огромных объектов от сервера и нужно всех их обработать. Нам нужно достать по элементу из каждого объекта, коих тысячи (а ведь в каком-то из них может и не быть нужной вложенности). Чтобы проверить наличие объекта для вывода и чтобы ничего не поломалось, мы должны прописать огромное условие на наличие каждой вложенности в объекте.

Мы можем избежать насилия наших мозгов и просто вставить оператор ?. там, где мы не уверены в наличии объекта в принципе (а он мог и не прийти)

Однако стоит отметить, что оператор нужно ставить только там, где предполагается ошибка. Дело в том, что сайт может начать ломаться, а найти проблему будет, благодаря операторам, сложно

Так же можно проверять наличие определённых методов через полное написание ?.. Использовать его стоит только для проверки потенциально несуществующих функций (хотя WebStorm явно укажет на существование функции в проекте)

004 (д) Живые коллекции и полезные методы

В консоли мы видим коллекции, которые были созданы благодаря querySelector и getElementByClassName

const boxesQuery = document.querySelectorAll('.box');
const boxesGet = document.getElementByClassName('box');
 
console.log(boxesQuery);
console.log(boxesGet);
console.log(document.body.children);

И сейчас перед нами встаёт разница между живыми коллекциями и статическими.

Современный способ обращения к объектам представляет из себя статическую коллекцию, которая статично хранит в себе состояние DOM-дерева. Старый способ обращения к объектам отображает текущее состояние DOM-дерева

const boxesQuery = document.querySelectorAll('.box');
const boxesGet = document.getElementByClassName('box');
 
boxesQuery[0].remove();
boxesGet[0].remove();
 
console.log(boxesQuery);
console.log(boxesGet);
console.log(document.body.children);

И встаёт иногда такая потребность, чтобы следить за состоянием DOM-дерева в течение его жизни. Мы можем сделать массив из данной коллекции, но тут уже встаёт проблема: массив – это уже статичный объект и следить за DOM-деревом не получится

// генерируем массив из псевдомассива элементов страницы
console.log(Array.from(boxesGet));

Тут мы добавляем дополнительно пять боксов через цикл

И тут представлены два метода, которые можно применять на псевдомассивах.

  1. matches() – принимает в себя селектор и при его нахождении в объекте возвращает true
  2. closest() – принимает в себя класс и выводит ближайший родительский элемент с таким классом.

Мы имеем структуру: wrapper > innerWrapper > box. И при поиске враппера выведется вся эта структура

005 (д) Тип данных Symbol

Символы используются для создания уникального идентификатора – они уникальны и неизменяемы

В качестве ключа у нас всегда используется строка. Мы можем записать её как в кавычках, так и без. Кроме строк свойствами объектов могут быть ещё и символыи только эти два типа данных

const person = {
	'name': 'Alexey'
};

Символы – это всегда уникальные идентификаторы (даже если есть идентификаторы с одинаковыми значениями). По точечной записи обратиться к ним нельзя. Создать через new – не получится

Символы позволяют создать скрытые при обычном доступе свойства, которые не показываются при переборе объекта

Получаем значение ключа:

Получаем сам ключ:

Мы можем написать отдельный метод через this[], чтобы получить символ из объекта

И так же есть отдельный метод, который позволяет получить массив символов из объекта

Основная причина создания символов – это защита данных. Символы исключают возможность перезаписи нужных данных в объекте. Когда в проекте огромное количество строк кода и множество библиотек, то сложно будет уследить за тем, не будут ли перезаписаны нужные нам данные

Тут мы записываем ключ id в объект, но не меняем при этом символ id

Когда мы формируем символ через синтаксис Symbol.for(‘id’), то у нас формируется глобальный реестр символов и теперь это описание не будет уникальным

Есть и обратный метод, который получает ИЗ глобального реестра значение

006 (д) Дескрипторы свойств и полезные методы объектов

Дескрипторы определяют как работают свойства. Ещё они называются флагами:

  • writable(true) – свойство можно изменить. False – свойство только для чтения
  • enumerable(true) – свойство будет перечисляться в циклах
  • configurable(true) – свойство можно удалить, а его атрибуты можно будет изменить

Для всех создаваемых свойств они всегда имеют значение true

Через метод отображения дескрипторов getOwnPropertyDescriptor ОДНОГО свойства (есть метод getOwnPropertyDescriptors, который выводит дескрипторы для нескольких свойств) можно отобразить ключ-значение и его дескрипторы

Через defineProperty() мы можем поменять значение флага

Если мы создадим свойство через defineProperty(), то по умолчанию все флаги будут иметь значение false

И тут мы запретили изменение значения свойства объекта

А тут мы создали это свойство через промпт и оно сразу имеет все значения в false

И так же можно прописать нужные флаги отдельно при создании свойства

И тут уже показан пример как скрыть метод в объекте через defineProperty()

После того, как свойство было установлено, оно уже не будет реагировать на данный метод и поменять его буде невозможно. Поставить так флаг – это дорога в один конец

Через defineProperties() мы можем поменять флаги сразу для нескольких значений объекта

И так же стоит отметить и символы. Даже при положительных значениях флагов они работают ровно так же, как и при отрицательных. Они не отображаются при переборе и им нельзя придать другое значение

И вот все методы объектов, которые по-хорошему нужно знать

007 () Итерируемые конструкции

Кратко повторим конструкцию for-in. Она проходится по всем элементам объекта (массива). Однако его главная проблема в том, что он может проходиться не по порядку

Когда мы говорим уже про конструкцию for-of, то она сразу уже выдаёт значение. Однако может выдать значение только итерируемого объекта. То есть перебрать массив или строку через for-of - возможно, а уже при переборе объекта выйдет ошибка, так как объект – это неитерируемая единица.

Основной плюс for-of заключается в том, что он выведет все значения из массива именно в том порядке, в котором они записаны.

И тут опять же можно увидеть, что for-of перебирает только те значения, которые являются собственными (проверить можно через hasOwnProperty())

Грубо говоря, итерируемые объекты – это те структуры, которые хранят в себе символ-итератор

Итератор – это метод, который возвращает объект с методом next()

Чтобы перебрать объект, нужно ему добавить итератор самостоятельно

Когда мы используем for-of, то он вызывает этот прописанный метод (на скрине) 1 раз и этот метод должен нам вернуть итератор (а именно объект с методом next()). И дальше for-of будет работать с тем объектом, который вернулся из этого метода.

Внутри метода находится само отображение выполнения возврата значения и само значение

И тут уже более подробно опишем:

Мы имеем объект salary. После него мы создаём через символ наш итератор. Итератор хранит в себе функцию. Функция возвращает объект, внутри которого имеется несколько свойств и метод next().

Метод next() в себе имеет определённую логику – тут мы остановим выполнение цикла только тогда, когда внутри объекта первое свойство будет иметь значение больше, чем последнее.

Цикл будет продолжаться, пока возвращаемый done будет иметь значение false. Чтобы передать значение в следующий цикл, нужно передать его через value. Останавливается цикл через done: true

Тут ещё нужно отметить, что функция должна иметь вид: function(){}. Стрелочная работать не будет

И вот через for-of наконец-то мы можем перебрать объект. Однако перебор будет выполнять определённую логику

Так же такой итератор можно вызывать и вручную. Мы можем присвоить итератор в переменную и так как это метод, то его можно вызвать сразу через ()

008 () Map

Тут хочется пояснить, что в качестве ключа объекта всегда используется строка. Даже если мы напишем число, то оно переведётся в строку. Однако, если вставить {} объект, то мы получим ошибку

Map – это специфический тип данных, который выглядит как объект, но по-сути может представлять из себя и массив, и строку, и число и любой другой тип данных

Тут можно увидеть, как map используется для создания объекта, у которого в качестве ключа используется объект

Так же можно сократить запись, если записывать через цепочку либо можно делать присваивание через цикл

Однако самым рациональным методом будет присваивать значения через цикл из массива или определённой базы данных, которая может к нам подгрузиться

Так же мы имеем метод get(), который позволяет получить map-значение из массива

Метод has() проверяет наличие значения в мапе

И вот несколько методов для работы с нашей картой

Map по сути своей представляет из себя массив массивов. Это можно было определить через [entries] в консоли браузера. Начальные данные через конструктор можно задать так:

Ну и через перебор можно вывести ключи из нашей карты

Тут представлен код, который в массив закинет ключи ключей объектов карты. Тут мы берём через метод keys() ключи из карты и внутри push() (закидывания элемента в массив) мы вычленяем из объекта - бывшего ключа (который до этого был ключом элемента карты) ключ

Коротко: keys() берёт ключи из карты, Object.keys() берёт ключ объекта (который является ключом карты)

Получается такое получение ключей

Сейчас мы получаем вторую часть от элементов карты – значения. Получаем их просто перебирая Map через метод values(), который возвращает значения

Тут мы получаем сами значения, записанные в карту

Так же можно произвести отдельно получение значений объекта-ключа и значения через деструктуризацию массива (карта – хранит каждое значение в виде массива)

Нужно отметить, что метод entries() возвращает как-раз таки массивы

Ну и самый рекомендуемый способ перебора – через встроенный метод forEach. В его колбэк-функцию передаётся значение, ключ и сама карта (к ней вполне можно обратиться)

Перевод объекта в карту

Перевод карты в объект

Все особенности Map():

  1. У карт ключами могут быть любые типы данных: строки, массивы, объекты, функции, числа (В объектах только строки)
  2. Карты добавляют новые значения всегда в конец и всегда в них соблюдается порядок (В объектах могут добавляться куда угодно)
  3. В картах при создании нет никаких наследуемых свойств через прототип (В объектах могут быть)
  4. Карты легко перебирать
  5. Размеры карты легко получить (size())

009 () Set

Это массив, где каждое значение встречается только один раз

И так же мы можем добавить новые значения в такой массив

Ну и так же нам доступна ещё и цепочная запись через точку

И второй способ – перебор через forEach(). Его особенность заключается в том, что у нас нет ключей в таком массиве и поэтому в качестве второго аргумента передаётся это же значение, но снова. Последний аргумент – сам сет

Ну и у нас так же есть методы, присущие структуре Map. Методы keys() и entries() созданы для обратной совместимости с Map

В практическом применении Set используют для быстрой фильтрации массивов. Писать фильтр самому – это долго и неэффективно. Сет изначально хорошо оптимизирован и будет работать быстрее, чем самописный фильтр

010 () BigInt

Таким способом мы можем вывести максимальное число в интеджере. Иногда появляются ситуации, когда нам нужно использовать числа больше (например, при работе в банках)

И для этого уже используется тип данных BigInt. Его можно написать двумя способами:

  1. Через n на конце
  2. Через метод BigInt. Этот метод может принимать как число, так и строку

Большие числа нельзя складывать с обычными и нельзя использовать внутри методов Math – это два их самых основных ограничения. Так же они работают почти со всеми унарными операторами (кроме +). Обычные операции с двумя большими числами – работают. При делении друг на друга большие числа округляются в меньшую сторону

К операциям сравнения большие числа относятся нормально и их можно сравнить даже с обычными числами

Чтобы сложить большие и обычные числа можно воспользоваться таким синтаксисом:

И самое важное:

  • BigInt нужно использовать только тогда, когда это реально необходимо. Потому что при переводе большого числа в обычное, все не входящие в диапазон цифры – отпадут.
  • Например, число 21342134721934721347921389421893231 откинет значения до 2134213472193472

011 Дополнительные ссылки

002 -https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator

003 -https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/Optional_chaining

004 -

https://drive.google.com/file/d/1TCuJlE6AYEXD9NjW9XinMsZ-1zxHBDAG/view?usp=sharing

005 -

https://tc39.es/ecma262/#sec-well-known-symbols

006 Object.keys-

https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/keys

006 Object.values-

https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/values

006 Object.entries-

https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/entries

007 The-Essential-Guide-to-JavaScript-Iterators - https://www.javascripttutorial.net/es6/javascript-iterator/

007 hasOwnProperty - https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty

007 for.in-versus-for.of - https://bitsofco.de/for-in-vs-for-of/

007 for-of - https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Statements/for…of

007 How-to-iterate-over-a-JavaScript-object - https://stackoverflow.com/questions/14379274/how-to-iterate-over-a-javascript-object

008 - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

009 find - https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Array/find

009 - https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Set

010 - https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/BigInt

JS в работе

002 ClassList и делегирование событий

С помощью приведённых ниже методов обычно создают изменения в классах для применения новых стилей на объекте и проявления элементов. Конкретно данный гамбургер был сделан через использование метода toggle()

Изначальный код на странице:

Сначала мы получим псевдомассив со всеми нашими кнопками со страницы. Через classList.length можно получить количество классов на объекте. Через classList.item(индекс) можно получить наименование класса объекта под определённым индексом

Метод classList.toggle() крайне полезен и юзабелен. Данный метод добавит класс, если он отсутствует на объекте и удалит, если уже присуствует. Он может заменить целый блок с условной конструкцией, так как в его нутрянке и есть подобная проверка

Метод contains() проверяет наличие класса в выбранном объекте. С помощью него можно прописать условие, которое будет срабатывать при наличии определённого класса

И вот пример добавления/удаления класса через условную конструкцию и через toggle()

className – устаревший способ изменения классов. Он хранит в себе классы как строчку и использовать его неудобно

И сейчас нужно поговорить про добавление события для нескольких элементов на странице. Дело в том, что если нам нужно одинаковое событие на странице для нескольких элементов, то мы могли бы добавить addEventListener через forEach на все такие элементы. Однако такой подход немного устарел и имеет свои минусы в виде того, что если элемент отсутствовал на странице, то тот при появлении не будет иметь данный ивент.

Чтобы упростить задачу, нам нужно применить данный ивент на сам враппер, который в себе содержит эти элементы

Далее хорошо бы было узнать название элемента в самой HTML-структуре через дир - event.target (название элемента можно увидеть при нажатии на него на странице)

И теперь при нажатии любой кнопки внутри wrapper (даже если кнопка появилась позже в нём) у нас будет срабатывать event. При нажатии на сам враппер, ничего происходить не будет

Такой способ называется делегированием

Так же можно присвоить ивенты только тем кнопкам, которые имеют определённый класс (тут – blue)

И вот простой пример. Созданная кнопка после объявления ивента, уже имеет данный ивент, потому что он делегируется дочерним элементам

При таком коде новый ивент не будет добавлен на кнопку и она будет грустить одна такая☹

И так же есть продвинутая техника обращения к элементу через метод matches(), внутрь которого мы положим наименование элемента и селектор класса

003 Создаем табы в новом проекте

Вот хороший пример структуры CSS-документа, когда разные стили находятся в разных блоках. Такой подход упростит создание большого сайта

Ну и немного дополнительных стилей, с которыми можно будет порабоать из JS

Так выглядят наши переключатели табов

фитнес|500

Так выглядят сами табы

Для начала мы запишем весь наш код в ивентлистенер документа, который будет срабатывать при загрузке всей дом-структуры

Данной функцией мы прячем весь контент наших табов (добавляем хайд и снимаем шоу с анимацией фэйда на всех элементах через цикл)

И дальше реализована функция, которая показывает нужные табы по индексу. По умолчанию индекс равен нулю. Тут мы ремувим хайд и добавляем шоу и фэйд. Так же добавляем класс активности для переключателя табов

И самая сложная часть: смена активности таба. Её мы будем реализовывать через проверку тыкнутого элемента с event.target. Его айди закинем в метод showTabContent()

Ивент будет срабатывать при клике внутри родителя.

Для начала мы упростим обращение к таргету. Потом мы создадим условие, которое будет срабатывать, если элемент имеет нужный нам класс (класс элемента внутри родителя).

Дальше мы внутри условия будем перебирать все наши кнопки для переключения изображений. В качестве аргументов используем саму кнопку и её индекс.

Внутри создаём ещё одно условие - если кнопка будет совпадать с таргетом, то будем прятать весь контент и показывать нашу кнопку по индексу

004 Скрипты и время их выполнения. setTimeout и setInterval

Примерно так можно написать setTimeout() в обычных условиях. Первым аргументом функция принимает колбэк-функцию, вторым аргументом – таймаут. Все остальные аргументы – это аргументы для вложенной колбэк-функции

Так же мы можем вложить именованную функцию в таймаут. Тут нужно заметить, что внури мы не вызываем функцию, а просто вкладываем её имя

Когда мы передаём setTimeout() в переменную, мы передаём числовой идентификатор этой функции. Делается это для того, чтобы чётко определять различные setTimeout в коде, так как таких асинхронных функций может быть много

И так же мы можем очистить интервал и отменить его выполнение

Таким способом мы можем задать таймаут для определённой кнопки. Так же через таймаут часто задают всплытие каких-нибудь модальных окон на сайтах

И так же мы можем воспользоваться не только таймаутом, но и задать выполнение функции по интервалу (в примере каждые 3 секунды) через setInterval()

И уже в таком случае наш код сможет добраться до переменной с интервалом. Однако тут мы встречаемся с другой проблемой: clearInterval не выполнится, так как он идёт в синхронном потоке кода. Дело в том, что на момент выполнения у него не будет никакого значения, вместо него в нём будет undefined (clearInterval(longTO = undefined))

Чтобы очистка интервала не выходила из потока, нам нужно задать в самой функции определённое условие, по которому будет выполнятся очистка интервала изнутри самого этого интервала

И сейчас мы подбираемся к проблеме, которая заключается в том, что setInterval не учитывает то, сколько времени выполняется функция внутри него. Он просто выполняет функцию раз в определённое время, которое ему задали. Сама же функция внутри него может выполняться и гораздо дольше.

И тут уже приходит рекурсивный setTimeout(). Он, в свою очередь, сначала выполняет функцию и уже только потом ожидает выделенное время. Создаётся такой таймаут через перевызов по его идентификатору (переменной)

И тут уже представлено создание анимации блока, который перемещается по боксу при нажатии на кнопку.

Первым делом, получаем нашу кнопку, через которую будем запускать анимацию.

Во-вторых, создадим анимацию, внутри которой будем хранить сам бокс, который будем перемещать. Там же инициализируем наш счётчик позиции и идентификатор setInterval. Так же внутри расположим функцию, которая будет менять позицию каждый кадр. Эта функция будет помещена в интервал.

Конечной точкой нашего блока будет сдвиг до конца блока (тут 600 бокс и 120 движущийся блок – 480 длина пути). Проверка будет выполняться, пока позиция не будет равна нужному нам числу. После мы очищаем интервал

Интересная особенность: даже если написать периодичнсть в интервале = 0, то всё равно на уровне кода она может иметь минимум в 4 миллисекунды.

005 (д) Сборщик мусора и утечки памяти

Если говорить просто, то JS – это высокоуровневый (все базовые команды прописаны за нас) интерпретируемый (его основа – это не компилятор, а интерпретатор) ЯП.

И вспомним, что в функции можно объявить переменную без let и const. Если залезть немного внутрь, то это будет то же самое, что и объявить переменную глобально через прототип window. Дело в том, что такая переменная неудаляема и сборщик мусора ничего с ней не сделает

Так же сборщик мусора не может удалить оставленные нами таймеры. Дело в том, что они хранят ссылку на конкретные объекты, которые в данный момент используются. Такие таймеры нужно останавливать и выключать

Так же стоит упомянуть и обработчики событий. Дело в том, что в старых браузерах они так же создавали утечку памяти. Если мы удалим объект с ивентом, но не удалим сам ивент, то у нас в памяти останутся оба данных объекта и будут занимать место. Однако современные браузеры могут чистить и оставленные ивенты.

В React и JQery очистка ивентов реализована автоматически. Однако в нативном JS является хорошим тоном стирать ивенты через removeEventListener().

Так же если мы удалим DOM-элемент с сайта, то при наличии другой ссылки в JS на этот элемент, он останется в памяти и сборщик не сработает

В этом примере у нас будет утечка памяти. Дело в том, что функция deleteElement удалит элемент из ДОМ-дерева, а переменная divBlock сохранит этот блок в памяти страницы – так делать не надо. Чтобы исправить ситуацию, нам нужно поместить переменную в такие условия, чтобы после создания она была удалена

Сейчас мы убрали лишнюю переменную из глобальной области видимости и создали оторванный от кода объект. При удалении этого объекта – он удалится полностью (и из ДОМ-дерева и из памяти)

Чтобы решать проблемы с памятью (если такое будет нужно), можно воспользоваться специальным инструментарием в DevTools

https://developer.chrome.com/docs/devtools/memory-inspector/

006 () WeakMap и WeakSet

Как мы знаем, сборщик мусора удаляет объекты, на которых уже нет ссылок в проекте. В данном примере ссылка на объект остаётся в массиве и поэтому он не стирается

Уже в этом примере объект не стирается, так как ссылку на объект хранит в себе карта

И дальше у нас идёт объект WeakMap. Это объект, у которого

  1. ключи – только объекты
  2. При отсутствии ссылки на объект извне – объект будет удалён сборщиком мусора

Если сейчас мы решим проверить значения через keys(), то мы получим ошибку, так как таких методов как keys, values и entries – не существует для такой карты. get, set, delete, has – единственные методы, которые у нас остаются от обычной карты. Поэтому проверим через наличие значения has()

Если мы выведем просто карту, то наш интерпретатор не будет понимать, что за объект находится в карте

Самый простой пример использования WeakMap – это отображение пользователей в онлайне в каком-нибудь чате. Если пользователь offline, то он получает значение null и скрывается из онлайна чата.

И уже дальше у нас идёт WeakSet. Если Set хранит в себе только уникальные значения массива, то WeakSet хранит эти значения только до тех пор, пока хоть какой-то объект ссылается на них в проекте. WeakSet поддерживает только add, has, delete.

Пример реализации прочитанных сообщений. WeakSet принимает в себя значения массива с сообщениями. Как прочитать сообщение можно один раз, так и добавить в прочитанные сообщения система одно сообщение может только один раз.

И тут у нас сразу видна такая структура WeakSet. В него мы добавили 3 разных объекта. Далее мы обращаемся к наличию третьего элемента в этом сете. Но так как мы из массива удалили первый элемент, то он съехал на одно значение назад и сам виксет теперь имеет только 2 значения вместо трёх, как это было до стирания первого элемента (при стирании объекта из массива все элементы по индексам сдвигаются на один элемент к началу)

007 Работа с датами

Дата задаётся обычно через конструктор Date()

Однако можно вписать и цифрами через запятую все нужные даты. Как можно увидеть, месяцы идут с 0, а время идёт по гринвичу. Поэтому при вводе 5 месяц 20 часов, мы получим – 6 месяц (так как он имеет индекс пять) и 17 часов (так как пояс +3 МСК)

Так же интересно знать, что все даты в JS выражаются в миллисекундах и их можно перевести как в дату, так и обратно в мс. Так же отсчёт времени идёт от 1970 года

Так же мы можем быстро посмотреть все нужные методы дат через get

Так же у нас есть возможность получить два разных часовых пояса

Через set мы можем установить нужное нам время. Проверять такой код в консоли не получится – это нужно делать сразу в браузере

Ну и не самая очевидная часть заключается в том, что можно так же вставить и остальные значения времени

Два варианта записи даты: первый обычный, второй через парсинг

Так же дата позволяет сделать таймер и высчитывать миллисекунды между выполнением операций.

Тут было подсчитано время выполнения операции

008 Создаем таймер обратного отсчета на сайте

Собственно, блок кода, в котором у нас и происходит всё действо

Первая константа хранит в себе конечное время, когда таймер прекратит свою работу. Внутри функции getTimeRemaining мы получаем всё время, дни, часы, минуты и секунды по-отдельности (получаем из разницы между нынешним временем и дедлайном) и возвращаем объект с данными значениями

Дальше в функции clockTime мы производим изменение времени. Время меняется через функцию updateClock, которая вызвается каждую секунду через setInterval. Внутри обновления часов мы получаем данные из нашей первой функции и подставляем значения полученного объекта в innerHTML наших блоков со временем.

Если время к моменту вызова часов уже уйдёт в минус (например, на дворе уже будет 1ое сентября), то интервал очистится.

Так же для вставляемых значений времени была применена отедельная функция, которая подставляет 0 к числам из одной цифры.

А вот и сама функция, которая переводит одиночные цифры в двуциферные(да-да)

И теперь мы имеем прекрасный счётчик на нашем сайте!

009 () Обработка прошедшей даты

Первый вариант починки даты, которая может уйти в минус – это при отрицательной дате не проводит расчёты самой даты, а просто выдать «0»

Так же можно при минусовом времени вместе с остановкой интервала просто выдать в текстовых блоках нули

Любой из этих вариантов будет работать нормально и очистит таймер

010 Параметры документа, окна и работа с ними

Есть три основных понятия среди глобальных объектов: document (сам документ с элементами), window (отображаемое окно, размер которого уменьшается, например, когда мы уменьшаем браузер) и screen (весь видимый монитор)

И в первую очередь хочется поговорить про получение размеров элемента через его свйоства, которые мы моежем получить через JS

Пример использования:

Тут стоит отметить, что на получаемые значения так же влияет и тип box-sizing (тот же border-box, который часто используется на страницах)

Так же мы можем показать полностью свёрнутое окно через свойство бокса, в котором хранится полная высота свёрнутого объекта

Так же мы можем отобразить свойство JS, которое хранит в себе положение по скроллу в пикселях

Данные методы позволяют отобразить Computed Styles конкретного объекта

И тут хочется немного прояснить про Computes Styles (вычисленные стили). Это стили, которые уже применены на объект. Они могут вычисляться автоматически как margin: 0 auto, который под копотом постоянно вычисляется браузером

И мы можем получить данные стили с помощью JS

Такой код и использование метрик относительно объектов позволяет, например сделать быстрый скролл по кнопке от начала страницы к середине (например)

Так же тут стоит отметить особенности обращения к самому документу, так как операции проходят не с самим document, а с documentElement

Уже данный метод позволяет проскроллить относительно текущего положения по нужным нам координатам

Второй метод будет уже работать относительно всей страницы

011 Создаем модальное окно

Важно! В HTML есть кастомные атрибуты, которые мы можем задавать самостоятельно. Это атрибуты, которые начинаются на «data-» - после дефиса мы можем прописывать свои значения

Блок кода модального окна (display: none – по умолчанию)

Это кнопка вызова модального окна

Это код для стоковой странички. Тут стоит отметить, что при вызове модельного окна мы скрываем оверфлоу Так же при использовании в качестве значения для стилей «“”» пустой строки – добавляется значение по умолчанию самим браузером

Если мы хотим сделать отображение или закрытие через тугл, то нужно будет убрать настройки дисплея из стилей и добавить класс хайд изначально

Так же будет удобно для пользователя, если мы позволим закрывать модальное окно через клик по пустому пространству

Так же в плохом коде можно встретить и такое объявление ивента. Дело в том, что ивент уже объявлен внутри обработчика события. Однако такая запись считается плохим тоном

Ну и добавим закрытие модального окна через Escape. Тут уже нужно проверять кнопку через событие «keydown» (кнопка нажата) и параметр внутри «e.code»

Чтобы определить нужную нам кнопку, можно воспользоваться уже заранее заготовленными таблицами кейкодов или нужными сайтами

Примечание: если бы мы добавляли и удаляли класс с объекта через add()-remove(), то нужно было бы дописать ещё и такое условие

012 Модификации модального окна

Сейчас перед нами стоит задача: проявить модальное окно через определённое время либо, когда пользователь прокрутит до конца страницы

Первая задача выполняется крайне просто через setTimeout и очистку этого таймаута внутри функции, которая его вызывает (чтобы окно не открывалось, если его уже открывал пользователь)

Вот код, который выполняет вторую поставленную задачу. Срабатывание тут устроено немного сложно.

Для решения некоторых багов нужно добавить -1 пиксель от высоты всего документа пользователя

Тут нужно кое-что отметить – нам нужно сделать срабатывание только однажды. И первое, что приходит в голову – это использовать настройку для ивент-листенера. Однако проблема заключается в том, что ивент скролла срабатыает сразу, как мы сделали первый скролл на странице

Уже такой код позволит нам вызвать ивент ровно один раз в самом конце страницы

013 (д) MutationObserver, ResizeObserver и contenteditable

Атрибут contenteditable="true"позволяет нам редактировать контент внутри определённого блока. Это интересный тег, который позволяет нам менять контент внутри блока

Такой атрибут используют чаще в CMS, когда нужно отредактировать контент на готовой странице. Сохранение данного контента уже выполняет логика бэкэнда

И примерно таким образом мы можем задать слежку за элементом через observer. Конкретно тут обсервер получает в себя колбэк-функцию, которая хранит в себе рекорды (логи изменений). Далее мы инициализируем слежку через observe(элемент, выполняемая функция)

Отключить слежку можно через observer.disconnect()

тут находятся все опции мутировщика

И теперь мы можем просмотреть изменения элемента.

  1. Observer срабатывает уже после изменений. Мы работаем с результатом изменений
  2. Это асинхронная операция. Поэтому она может выполниться чуть позже или чуть раньше
  3. При остановке Observer, он так же стирается и сборщиком мусора

Так же есть Perfomance, Resize и Intersection обсёрверы

014 Функции-конструкторы

Как можно увидеть – новые объекты можно создать через конструктор. Такой синтаксис через new означает, что дальше мы будем вызвать конструктор, который создаст экземпляр класса

И так же нужно сказать, что функции в JS – это объекты

И вот пример создания функции-конструктора. Внутри неё мы создаём внутренние переменные данной функции через «this» (обращение к самому себе) и присвоение к ним внешних значений. Такая функция создаёт объект

Так же мы можем создать внутреннюю функцию, которую можно будет вызывать из всех экземпляров данного конструктора. Так же в прототип можно записать и дополнительные значения (например, функцию, которая будет наследоваться всеми экземплярами)

015 Контекст вызова. This

Понимать контекст вызова с «this», стоит как присваивание переменной к определённому контексту (функции). То есть любая переменная, которую мы создаём – принадлежит всей области видимости. Если мы создаём переменную с «this», то она будет принадлежать только данной области

Так же тут стоит отметить, что при обычном вызове функции с this внутри, у нас будет выходить глобальная переменная Window. Если мы добавим “use strict”, то получим undefined

И тут представлен пример, когда мы используем «this» и избегаем его. Благодаря «замыканию функции» в обычной ситуации (первый скрин) у нас переменные ищутся сначала внутри самой функции, потом поиск заходит в функцию родителя.

На втором изображении уже используется вызов переменной через «this» и функция ищет переменную только внутри себя и дальше не выходит. Поэтому и возвращается undefined

Контекст вызова у методов объекта – это сам объект

Уже в данном примере контекст вызова (объект) теряется, так как внутри метода просто вызывается функция

This в конструкторах и классах – это новый экземпляр объекта

То есть при создании свойства в конструкторе через «this», это значение будет присваиваться только созданным экземплярам через данный конструктор

Ручная привязка this: call, apply, bind

Так же мы можем вызывать функцию и подвязать ей контекст вызова через методы call и apply (оба метода выполняют одно и то же, но по-разному передают аргументы в функцию)

Так же можно сделать много вариаций одной и той же функции, используя контекст вызова и функцию bind. bind принимает в себя значение «this» и возвращает в переменную новую функцию, где this заменён на данный аргумент метода

И когда мы используем конструкцию с function() в ивенте мы так же можем пользоваться и «this», так как он будет вызывать конкретно наш нажатый объект

И работать с этим объектом мы так же можем

Как помним из примера выше – функция внутри функции имеет свой контекст вызова и поэтому в объекте такая вложенная функция вернёт undefined. Но отличительной особенностью стрелочных функций является то, что у них нет своего контекста вызова. Контекст вызова они наследуют от родителя и поэтому код, приведённый ниже, будет работать

Ну и так как нет собственного контекста вызова у стрелочной функции, то берём пример чуть выше с function() и переделываем его на стрелочную функцию. Работать уже такой ивент не будет, так как контекст, опять же, берётся у родителя

И чтобы заменить «this» в таких функциях, используют таргет ивента

кнопка|400

016 Классы (ES6)

Классы появились в стандарте ECMA2015. Если нам нужно будет перевести код в старый стандарт, то для этого используются трансплиттеры (тот же babel)

Синтаксис классов немного отличается от синтаксиса функции. Внутри класса обязательно описывается конструктор. Конструктор представляет собой те начальные значения, которые мы задаём концепции экземпляра. Синтаксис достаточно схож по смыслу с функциональными конструкторами

Дальше мы пишем методы класса. Внутри них мы спокойно можем обращаться к тем начальным значениям, что мы задали для экземпляра

Button|400

И далее у нас идёт синтаксис наследования. Он представляет из себя передачу кода дочерним элементам. Реализуется наследование через extends.

Метод super вызывает код родителя. Помещать его нужно всегда до написания остального кода

018 Используем классы в реальной работе

Сама вёрстка. Она содержит в себе блок menu, внутри которого располагаются три menu__item, которые и являются нашими карточками

Первым делом мы создаём класс и конструктор, который будет в себя принимать уникальную для каждой карточки информацию. Так же чуть дальше пропишем метод, который будет переводить доллары (единица из БД) в рубли. Вызываем перевод прямо в конструкторе при создании

И дальше у нас идёт самый важный метод, который и выводит саму карточку на страницу. Первым делом мы инициализируем блок, в котором будет находиться карточка. Дальше мы в этот элемент помещаем саму карточку и забиваем её элементы нашими переменными.

В самом конце мы аппендим карточку на страницу через аппенд по родителю

Так же хочется упомянуть, что создать новый объект можно и без его объявления и присвоения в переменную. Конкретно тут, мы не собираемся над карточками проводить никаких операций, поэтому их можно просто создать через new. Ну и сразу вызвать метод render, который сконструирует блоки на сайте

Ну и дальше нам нужно создать три этих карты (вёрстку из html нужно будет удалить)

019 Rest оператор и параметры по умолчанию (ES6)

В JS есть оператор spread – “”, который деструктуризирует объект (массив). Однако так же есть и оператор rest, который собирает отдельные элементы в один массив – «…название»

И попытаемся вставить данный оператор в наш проект. Представим, что нужно дать возможность вставить сразу несколько классов на карточку и для этого можно воспользоваться рестом, который вернёт массив этих значений

Так же нам нужно внести изменения в рендер наших карточек. В условии нужно проверить, переданы ли вообще классы и присвоить эти классы (если нет или если их передали). В первом случае, мы проверяем массив, который нам возвращает рест. Если ничего в качестве классов не будет передано, то сработает первое условие (так как рест всегда возвращает массив, даже если значений 0). Если что-то будет передано, то сработает второе условие, которое через forEach передаст все нужные значения в качестве класса в наш див-элемент.

И сейчас у нас есть возможность не писать классы или написать сразу несколько

И примерно так это выглядит на странце

Продвинутый JS

001 Локальные сервера

Сервер

HTTP-запросы: post- (отправить данные на сервер – отправка введённых данных в форму) и get-запросы (получить из google ответ по запросу)

Самыми популярными запросами являются пост-запросы. И зачастую с ними и придётся работать в вебе, так как нам нужно обрабатывать получаемую от пользователя информацию.

Однако тут мы предстаём перед другой проблемой – маленькие локальные сервера (live-server, prepros, встроенные мелкие сервера в IDE) не могут обрабатывать запросы, которые мы на них отправляем.

Чтобы работать с запросами с сайта, нам нужно воспользоваться более полноценными локальными серверами. Например, можно скачать Open Server

002 JSON формат передачи данных, глубокое клонирование объектов

JSON – более современный формат обмена данными между сервером и клиентом. Он пришёл на смену XML, который был куда более тяжёловесным.

Мы имеем два основных метода. JSON.parse() переводит объект в JSON, JSON.stringify() возвращает из JSON нужный нам объект

Так же такой подход позволяет сделать глубокое копирование объектов (когда копируются объекты полностью любой вложенности), как тут показано на примере, когда мы меняем значения вложенных объектов

003 AJAX и общение с сервером

Технология AJAX (Async JS and XML) используется для динамической генерации контента на странице. Она позволяет интерактивно менять контент, что выглядит красиво и позволяет не перезагружать страницу для изменения контента. Это снижает количество потребляемого трафика (так как грузится только часть страницы, а не вся страница) и снижает нагрузку на сервер

Тут представлен код HTML и справа код – данные с сервера

Получаем инпуты

Два ивента для инпутов:

Три основных метода запросов на сервер

Open(метод(пост- или гет-запрос), ссылка на файл запроса, асинхронность (true/false – изначально стоит в true, так как выполняется запрос к серверу и работа страницы асинхронно и это нормально), логин, пароль)

get-запросы можно реализовать даже на простом сервере (тот же Live Server)

setRequestHeader – отправляет сам json-файл. В него мы вставляем заголовок типа и сам тип

send – в данном случае отправляет пустой запрос на получение данных. Однако, если мы будем использовать пост-запрос, то нужно будет в сенд передать отправляемые данные

Так же у нас есть определённые свойства, которые хранит наш реквест:

status - Содержит статус нашего запроса (404 и тд) statusText - текстовое описание ответа от сервера (OK(200), Not Found(404))
response - ответ от сервера (задаёт бэк-энд разработчик)
readyState - текущее состояние запроса (таблица)

Тут представлен код, который выполняет запрос на сервер, получает данные и обрабатывает их, чтобы вывести данные в инпут

Данный ивент срабатывает при выполнении запроса. Однако выполниться запрос может и с ошибкой, поэтому полностью условие убрать на проверку правильности – не получится. В остальном код работает ровно так же, только уже можно не проверять на полное выполнение запроса

И так выглядит итог

Полный код:

Первый вариант

Второй вариант

004 Реализация скрипта отправки данных на сервер

Сейчас нужно будет реализовать ПОСТ-запрос отправку данных на сервер и ответ пользователю

PHP файл будет возвращать данные, которые мы отправляем на сервер (принимает данные с клиента, превратит в строку и вернёт обратно) – это response от сервера

В классической работе браузера (без модификаций через JS) при отправке данных – они перезагружают страницу и показывают отправленные данные в url

Если инпут создан через тег button, то его тег type автоматически будет иметь submit

Самым первым делом мы получим все формы на нашей странице и заранее подготовим ответы пользователю по статусу отправки данных

И дальше нам нужно будет создать фунцию, которая будет производить отправку наших данных на удалённый сервер. Внутри функции будет располагаться ивент, который будет срабатывать на инпутах формы при сабмите (баттоны изначально имеют в себе такой вшитый атрибут). Обязательно в начале нужно сбросить обычное поведение объекта при срабатывании (чтобы страница не перезагружалась)

Так же отдельно хочется выделить данный функционал. Он позволяет не брать отдельно каждую форму и не перебирать отдельно значения. Данный конструктор позволяет собрать автоматически все данные с формы, которую в него пихнули

Однако обязательно, чтобы сработал FormData, нужно чтобы в инпутах внутри формы были прописаны атрибуты name=”имя_формы”

И дальше нам нужно создать ивент листенер внутри функции, который при полной загрузке запроса будет выводить в консоль сам отправленный запрос (для нас), показывать статус отправки для пользователя и очищать формы.

reset – сбросит форму remove – удаляет объект со страницы

И в конечном итоге нам нужно применить нашу созданную функцию на все формы (через функцию мы накидываем ивент-листенеры на формы и формируем из них данные)

И сразу при запуске мы встречаемся с непонятной ошибкой, которая нам говорит, что наши данные анонимны и не могут быть обработаны сервером

И тут уже кроется очень важная особенность работы с ПОСТ-запросами. Если мы используем XMLHttpRequest, ПОСТ-запрос и передаём данные через FormData, то указывать хедер нам нелья, так как вылезет ошибка

И так же теперь в консоли мы можем увидеть данные

Ну и так же в нетворке можно просмотреть и данные, которые были отправлены на сервер

И так же можно переписать немного код для работы с JSON-данными, а не отправлять через FormatData. Тут уже хедер нам нужно использовать

Так же нужно упомянуть, что PHP не умеет нативно работать с JSON

Вот php-код, который обработает JSON-данные с сервера

  • И код работает нормально и ровно так же, как и раньше

Полный код:

005 Красивое оповещение пользователя

На сегодня нам нужно сделать красивое оповещение пользователя о загрузке (если она будет долгой).

Первым делом – уберём код с нашего крестика для закрывания модального окна. Мы можем вызвать закрывание прямо внутри условия определения закрытия модального окна (обратиться к крестику через его персональный атрибут data-close)

Дальше мы должны превести наше модальное окно в генеративный вариант. То есть мы его должны возвращать с помощью JS. Сохраним оригинал в виде prevModal и скроем его. Дальше уже реализуем саму генерацию этого окна и будем сгенерированное окно вставлять в сам блок кода с модальным окном

И далее применяем нашу функцию

И теперь обе модалки какое-то небольшое время показывают окно диалога

Дальше добавляем наш загрузочный спиннер (это свг-анимация, которую можно добавить как картинку)

И такой код позволит вставить нашу СВГшку после блока форм снизу (чтобы не ломать вёрстку)

006 Promise (ES6)

Promise (обещание) – это блок кода, который будет выполняться асинхронно относительно другого кода. Промис позволяет избежать collback-hell ситуации, когда у нас огромное количество таймаутов и вложенных функций

Это код через setTimeout, который не гарантирует нам 100% на выполнение кода и может выдать ошибку

И коротко о том, что такое промисы. Они позволяют реализовать код, который обещают выполнить асинхронно относительно остального кода – они не останавливают выполнение основного потока кода и выполняются параллельно

Промисы создаются через конструктор. Внутрь промиса вкладывается функция, которая принимает в себя два аргумента – resolve и reject, которые тоже являются функциями. Обе вложенные функции отвечают за возврат значения в разных ситуациях.

resolve – возвращает значение, когда функция выполнилась успешно   reject – возвращает значение, когда функция выполнилась с ошибкой

Дальше идёт уже чейновая природа обработки промиса. Мы пишем имя промиса и then().(много then).catch().(много catch).finally(). Каждый чейн выполняет свою работу, которую мы для него пропишем. then срабатывает, когда Promise завершается резолвом и так же резолв передаёт своё значение в этот then. catch срабатывает, когда промис завершается реджектом и реджект так же передаёт вписанное в него значения в этот catch

И опять же про природу чейнов. Мы могли бы полностью прописывать промисы внутри thenов и создавать всё новые и новые промисы. Однако из thenов мы можем возвращать промисы (будет возвращаться значение внутри резолва или реджекта) и данные будут передаваться дальше по цепочке (выполнилось – передалось дальше и так бесконечно)

И основное преимущество, из-за которого мы используем промисы – это их чейновое выполнение. Конкретно тут выполняется сначала первый промис, потом резолв возвращает в then объект. Внутри первого чейна создаём новый промис, который у нас возвращается через return (возвращается то, что передаёт резолв при выполнении промиса). Дальше у нас идут ещё два чейна, которые выполняют действия последовательно друг за другом

И если у нас выполняется в конце промиса reject, то блок кода чейнится в catch (вместо then)

Чейн finally срабатывает при любом исходе промиса (reject/resolve) и всегда добавляется в конец

Тут пример небольшого промиса для следующих двух методов

И далее идёт так же важная команда, которая позволяет реализовать выполнение кода при выполнении сразу нескольких промисов - Promise.all. Например, мы отправили запрос на несколько разных серверов и нам обязательно нужно, чтобы выполнились сразу все нужные запросы

И так же есть метод, который выполняется сразу при выполнении первого промиса – Promise.race()

007 Fetch API

fetch – это конструкция с GET-запросом, которая внутри представляет из себя промис (то есть чейны работают так же как и в промисах)

Стандартно фетч возвращает промис, который выполняется в любом случае (кроме отсутствия интернета)

Чтобы отправить данные на сервер, нужно уже будет немного модифицировать фетч-запрос

Вторым аргументов в фетч передаётся объект с настройками, который заменяет сразу три метода стандартного реквеста. Мы прописываем тип запроса, бади с отправляемыми данными на сервер (например, отправим данные из формы) и тип заголовков (если отправляем JSON)

И сейчас на сайте с едой можно будет заменить все наши XMLHTTPRequests на короткий и понятный fetch, который в себе содержит все нужные нам реквесты, да ещё и исполняется асинхронно

И как итог мы заменили огромное количество кода с реквестами и данной проверкой

На данную короткую запись

Так же если мы допишем этот короткий участок кода, то мы сможем увидеть текст нашего запроса

Теперь так же можно удалить парсинг объекта и перенести его прямо в фетч

Ну и примерно так выглядит ответ от серера

Ну и так же нужно работать с особенностями фетча. Дело в том, что он не вернёт реджект, даже если у нас проблемы при связи с сервером. Единственное что, так это статус фетча поменяется с ОК на false.

Реджект сработает только при сбое сети / отсутствии интернета

008 Методы перебора массивов

И у нас есть основные методы массивов, которые мы можем использовать на постоянной основе

Метод forEach – просто выполняет какую-то операцию над каждым объектом в массиве. Сам массив он никак не изменяет filter – позволяет отфильтровать массив по определённому критерию map – перерабатывает массив нужным нам образом и записывает значения в новый массив. Можно записать значения в тот же самый массив, но это уже будет неправильно

reduce – самый необычный метод для перебора массива. Через него так же можно создать новый массив. Основная особенность данного метода заключается в том, что в качестве аргумента в него передаётся аккумулятор (переменная, которая сохраняет своё значение на каждую итерацию) и новый элемент массива.

В первом случае мы подсчитали сумму массива. Во втором сумму с начальным элементом для аккумулятора (задаётся через запятую – тут 3). В третьем методе подсчитали зарплату работников. В четвёртом вывели строку с именами сотрудников (тут показана особенность, что выведется строка без запятой сначала, так как метод выполняет сразу вторую итерацию)

И дальше у нас идут булевые методы, которые позволяют быстро проверить наличие определённого элемента в массиве.

every – проверяет, чтобы каждый элемент выполнял определённое условие (все работники с зп больше 100) some – проверяет наличие хотя бы одного элемента, который удовлетворяет условию (хотя бы один работник с зп больше 1000)

И далее у нас идёт пример: пришёл ответ от сервера и нам нужно отсортировать ответ так, чтобы остались только люди

Первым делом можно перевести объект в массив массивов (чтобы была возможность сортировки)

Дальше фильтруем массив и оставляем только персон

И уже в конце трансформируем массив так, чтобы в нём остались только люди (под нулевым индексом)

009 Подробно про npm и проект. JSON-server

Для использования npm в проекте, сначала нужно инициализировать этот проект

npm init
 
npm i json-server --save-dev

Чтобы не загружать на облако папку с модулями, можно её добавить в гитигнор

Так же если перед нами встанет такая ситуация, когда нам нужно будет установить нод-модули для проекта, у которого есть только json с зависимостями, то нужно прописать

npm i

Если мы напишем запрос именно к нашей базе данных, то мы будем получать все данные со страницы сразу, но в реквестах ничего не будет (так как там отображаются пост-запросы)

Чтобы активировать установленный json-server на ПК, нужно прописать

json-server ссылка_на_датабазу

И дальше вставить одну из ссылок на ресурсы (нам расширение сразу выдаёт ссылки на данные)

Ну и так же в расширении будет прописываться скорость выполнения запроса и сам запрос. Уже в браузере будет возвращаться не огромный объект, а конкретный массив

010 Получение данных с сервера. AsyncAwait (ES8)

В данном уроке нужно заменить отображение карточек на сайте с самостоятельного их создания через свои аргументы на создание по аргументам, полученным с сервера (а именно с нашей базы данных)

Ну и далее нашу датабазу нужно поместить в корень проекта и отдельно запустить json-server, чтобы работать с этой датабазой

И дальше нам нужно заменить фетч-запрос в нашем postData, чтобы мы могли запрашивать разыне данные по определённым параметрам.

Для этого сначала переименуем нашу основную функцию и сам пост данных забьём в отдельную функцию, внутри которой и будем вызывать фетч. Результат фетча (это промис) будем сразу переводить в JSON

И дальше мы встречаемся с проблемой. Promise – это асинхронный код, который выполняется вне основного потока функции. Поэтому на момент возвращения результата мы получим error, так как res будет равен undefined. Чтобы решить проблему, нужно превратить код в синхронный, чтобы return дождался выполнения промиса

И далее нам приходит на помощь конструкция async/await. Она работает примерно по такому принципу:

async говорит, что в функции есть асинхронные функции (промисы), а уже сам await говорит этой функции, что это и есть асинхронная функция и что её результата выполнения нужно дождаться, прежде чем выполнять код дальше. То есть выполнение кода остановится до получения любого результата этой функции

Так же await можно накинуть и на метод, который переводит промис в json (так как неизвестно насколько большой ответ от сервера придёт)

Примерно так выглядит итоговый запрос. Ссылка взята из json-server и перевод данных в текст убран (самый первый чейн)

И такой ответ от сервера мы получаем

И самая прикольная вещь тут – это наш первый постинг данных на сервер с записью ответа в датабазу

Ну и далее мы можем немного заменить такой формат записи данных из формдаты в объект

Мы можем это реализовать через метод entries, который переводит свойства объекта в массивы

И поэтому мы сейчас выполним такую комбинацию:

  1. Сначала formData переведём в массив массивов (можно записать и Object.entires(formData))
  2. Потом переведём через Object.fromEntires() этот массив массивов в обычный объект
  3. Потом через stringify переведём объект в json-формат данных
  4. Передадим переменную с джсоном в postData

Дальше переходим ко второй части, где мы должны заменить генерацию наших карточек из собственных запросов и генераций.

Тут нам нужны простые гет-запросы, поэтому оставляем только ссылку.

Дальше нам нужно решить проблему с тем, что fetch может вернуть error. catch нам недоступен, так как он вызывается только при отсутствии интернета. Делается это просто. У fetch есть свойство «ok» и нам нужно проверить, что если он отрицательный, то нам нужно будет выдать ошибку. В ошибке можно указать ещё и свойство статуса фетча

И дальше нам нужно выполнить наш запрос. При отправке запроса на сервер, мы получаем всю нашу дадабазу.

Затем в then обрабатываем наш запрос. Все данные (само меню) хранятся в массиве (массив объектов, где объект - менюшка), поэтому нужно будет перебрать полученную дату через forEach.

В переборе мы не передаём item, а сразу можем деструктуризировать полученный элемент на отдельные свойства, чтобы было проще (чтобы не обращаться постоянно item.img, item.altimg и так далее)

Так же можно таким способом реализовать рендер элементов на странице – не через наш созданный класс, а через функцию, которая принимает в себя массив карточек

011 Дополнительно Что такое библиотеки. Библиотека axios

Часть про библиотеки: вставляем все библиотеки над нашим скриптом, чтобы при выполнении логики основного файла, он смог ссылаться на эту библиотеку

А сейчас про библиотеку axios: https://github.com/axios/axios. Это библиотека, основанная на промисах и позволяющая быстро писать пост- и гет-запросы на сервер

Сразу нужно сказать, что если мы вставили axios ссылкой в HTML, то импортить его не нужно – это и будет ошибкой

Пишем данный коротенький запрос и получаем на выходе конечный объект, конвертация в нужный тип, расширенные свойства и проверка на ошибки – из коробки

Все полученные данные хранятся в data

И вот такой код заменит нам то, что мы делали в прошлом уроке. Тут мы получили сразу нужные данные и сразу же их и вывели

012 Создаем слайдер на сайте. Вариант 1 - простой

Простой вариант создания слайдера:

// Получаем массив слайдеров и стрелочки
const slides = document.querySelectorAll(".offer__slide"),
      prev = document.querySelector(".offer__slider-prev"),
      next = document.querySelector(".offer__slider-next");
// Получаем доступ к номерам слайдеров
const currentSlide = document.querySelector("#current"),
      totalSlides = document.querySelector("#total");
// Будем менять индекс выводимых слайдеров
let slideIndex = 1;
 
// Выводим первый слайд
showSlides(slideIndex);
 
// Будем менять номер слайдера по идентификатору
function showSlideIndex(slideIdentifier, slideDependency) {
    if (slides.length < 10) {
        slideIdentifier.textContent = `0${slideDependency}`;
    } else {
        slideIdentifier.textContent = slideDependency;
    }
}
 
// Выводим общее количество слайдеров
showSlideIndex(totalSlides, slides.length);
 
// Тут уже реализовано отображение слайдов
function showSlides(n) {
    // Проверяем границы значений слайдов
    if (n > slides.length) {  // После последнего слайда выводим первый
        slideIndex = 1;
    }
    if (n < 1) {  // Если тыкнем меньше первого слайда, то выведем последний слайд
        slideIndex = slides.length;
    }
 
    // Скрываем все слайды
    slides.forEach(item => item.style.display = 'none');
 
	// Отображаем слайдер по индексу
    slides[slideIndex - 1].style.display = 'block'; // так же можем поставить ""
 
	// Отображаем номер текущего слайда
    showSlideIndex(currentSlide, slideIndex);
}
 
// Тут мы будем менять отображаемый слайдер через вызов показа слайдов и изменение самого индекса
function changeSlides(n) {
    showSlides(slideIndex += n);
}
 
// Триггер для показа предыдущего слайда
prev.addEventListener("click", () => {
    changeSlides(-1)
});
 
// Триггер для следующего слайда
next.addEventListener("click", () => {
    changeSlides(1);
})

013 Создаем слайдер на сайте. Вариант 2 - более сложный

Опишем немного логику нашего слайдера. Самый внешний слайдер теперь будет не просто оболочной для наших слайдов - он будет просто окошком для просмотра слайдов. Ему мы зададим overflow:hidden. Дальше у нас идёт inner оболочка, которая будет в ширину равняться сразу всем слайдерам и будет занимать 400% от слайда на странице (400%, так как слайдов 4 штуки)

<div class="offer__slider-wrapper">  // Окошко
    <div class="offer__slider-inner">  // Добавили обёртку
        <div class="offer__slide">
            <img src="img/slider/pepper.jpg" alt="pepper">
        </div>
        <div class="offer__slide">
            <img src="img/slider/food-12.jpg" alt="food">
        </div>
        <div class="offer__slide">
            <img src="img/slider/olive-oil.jpg" alt="oil">
        </div>
        <div class="offer__slide">
            <img src="img/slider/paprika.jpg" alt="paprika">
        </div>
    </div>
</div>
const slides = document.querySelectorAll(".offer__slide"),
      prev = document.querySelector(".offer__slider-prev"),
      next = document.querySelector(".offer__slider-next"),
      total = document.querySelector("#total"),
      current = document.querySelector("#current"),
      slidesWrapper = document.querySelector(".offer__slider-wrapper"),
      slidesField = document.queryав
// Через регулярные выражения получаем ширину слайда
const width = +sourceWidth.match(/\d/g).reduce((acc, val) => {
    return acc += val; // собираем все значения массива в один
});
 
let slideIndex = 1;
let offset = 0; // Определяет длину прокрутки внутри slidesField
 
// Выводим общее количество слайдов
if (slides.length < 10) {
    total.textContent = `0${slides.length}`;
    current.textContent = `0${slideIndex}`;
} else {
    total.textContent = slides.length;
    current.textContent = slideIndex;
}
 
slidesField.style.width = 100 * slides.length + "%";// Зададим карусельному блоку ширину во все слайды
slidesField.style.display = "flex"; // Расположим слайды вдоль
slidesField.style.transition = "all .5s"; // Настроим анимацию для всех трансформаций внутри блока (тут - перемещение слайда)
 
slidesWrapper.style.overflow = "hidden"; // Скрывает все слайды за границами окошка
 
// Задаём всем слайдам одну ширину
slides.forEach(slide => {
    slide.style.width = width;
})
 
next.addEventListener("click", () => {
    // Перемещение слайда вперёд
    if (offset === width * (slides.length - 1)) {
        offset = 0;
    } else {
        offset += width;
    }
    slidesField.style.transform = `translateX(-${offset}px)`;
 
	// Выводим номер слайда
    if (slideIndex === slides.length) {
        slideIndex = 1;
    } else {
       slideIndex++;
    }
    if (slides.length < 10) {
        current.textContent = `0${slideIndex}`;
    } else {
        current.textContent = slideIndex;
    }
});
 
prev.addEventListener("click", () => {
    // Перемещение слайда назад
    if (offset === 0) {
        offset = width * (slides.length - 1);
    } else {
        offset -= width;
    }
    slidesField.style.transform = `translateX(-${offset}px)`;
 
	// Выводим номер слайда
    if (slideIndex === 1) {
        slideIndex = slides.length;
    } else {
        slideIndex--;
    }
    if (slides.length < 10) {
        current.textContent = `0${slideIndex}`;
    } else {
        current.textContent = slideIndex;
    }
});

014 Создаем навигацию для слайдов

Индикаторы слайдов сделаем через CSS и JS. Без дополнительных иконок.

.carousel-indicators {
   position: absolute;
   right: 0;
   bottom: 0;
   left: 0;
   z-index: 15;
   display: flex;
   justify-content: center;
   margin-right: 15%;
   margin-left: 15%;
   list-style: none;
}
 
.dot {
   box-sizing: content-box;
   flex: 0 1 auto;
   width: 30px;
   height: 6px;
   margin-right: 3px;
   margin-left: 3px;
   cursor: pointer;
   background-color: #fff;
   background-clip: padding-box;
   border-top: 10px solid transparent;
   border-bottom: 10px solid transparent;
   opacity: .5;
   transition: opacity .6s ease;
}

И вот сам код слайдера:

const slides = document.querySelectorAll(".offer__slide"),
      slider = document.querySelector(".offer__slider"), // Добавляем сам слайдер, чтобы относительно него спозиционировать точки
      prev = document.querySelector(".offer__slider-prev"),
      next = document.querySelector(".offer__slider-next"),
      total = document.querySelector("#total"),
      current = document.querySelector("#current"),
      slidesWrapper = document.querySelector(".offer__slider-wrapper"),
      slidesField = document.querySelector(".offer__slider-inner");
 
const sourceWidth = window.getComputedStyle(slidesWrapper).width;// Получаем ширину окошка прямо из посчитанных стилей
// Через регулярные выражения получаем ширину слайда
const width = +sourceWidth.match(/\d/g).reduce((acc, val) => {
    return acc += val; // собираем все значения массива в один
});
 
let slideIndex = 1;
let offset = 0; // Определяет длину прокрутки внутри slidesField
 
 
if (slides.length < 10) {
    total.textContent = `0${slides.length}`;
    current.textContent = `0${slideIndex}`;
} else {
    total.textContent = slides.length;
    current.textContent = slideIndex;
}
 
slidesField.style.width = 100 * slides.length + "%";// Зададим карусельному блоку ширину во все слайды
slidesField.style.display = "flex";
slidesField.style.transition = "all .5s";
 
slidesWrapper.style.overflow = "hidden";
 
// Задаём всем слайдам одну ширину
slides.forEach(slide => {
    slide.style.width = width;
})
 
// Делать точки будем полностью через JS
slider.style.position = "relative"; // Родитель должен иметь relative
const indicators = document.createElement("ol"),
      dots = []; // создаём истинный массив элементов
indicators.classList.add("carousel-indicators");
indicators.style.cssText = `
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 15;
    display: flex;
    justify-content: center;
    margin-right: 15%;
    margin-left: 15%;
    list-style: none;
`;
slider.append(indicators);
 
for (let i = 0; i < slides.length; i++) {
    const dot = document.createElement("li");
    dot.setAttribute("data-slide-to", i + 1); // устанавливаем (атрибут, значение атрибута)
    dot.style.cssText = `
        box-sizing: content-box;
        flex: 0 1 auto;
        width: 30px;
        height: 6px;
        margin-right: 3px;
        margin-left: 3px;
        cursor: pointer;
        background-color: #fff;
        background-clip: padding-box;
        border-top: 10px solid transparent;
        border-bottom: 10px solid transparent;
        opacity: .5;
        transition: opacity .6s ease;
    `;
    if (i === 0) {
        dot.style.opacity = 1;
    }
    indicators.append(dot); // добавляем на каждой итерации точку
    dots.push(dot); // и сразу пушим точки в массив
}
 
// Активируем точку определённого слайда
function activateDot() {
    dots.forEach(dot => dot.style.opacity = ".5");
    dots[slideIndex - 1].style.opacity = 1;
}
 
// Вынесем корректировку числа слайдеров вовне
function checkToZero() {
    if (slides.length < 10) {
        current.textContent = `0${slideIndex}`;
    } else {
        current.textContent = slideIndex;
    }
}
 
// Так же вынесем создание оффсета
function createOffset(offset) {
    slidesField.style.transform = `translateX(-${offset}px)`;
}
 
next.addEventListener("click", () => {
    if (offset === width * (slides.length - 1)) {
        offset = 0;
    } else {
        offset += width;
    }
    createOffset(offset);
 
    if (slideIndex === slides.length) {
        slideIndex = 1;
    } else {
       slideIndex++;
    }
    checkToZero();
    activateDot();
});
 
prev.addEventListener("click", () => {
    if (offset === 0) {
        offset = width * (slides.length - 1);
    } else {
        offset -= width;
    }
    createOffset(offset);
 
    if (slideIndex === 1) {
        slideIndex = slides.length;
    } else {
        slideIndex--;
    }
    checkToZero();
    activateDot();
});
 
// Функциональность точек
dots.forEach(dot => {
    dot.addEventListener("click", (e) => {
        // Получаем атрибут слайдера, на который мы тыкнули
        const slideTo = e.target.getAttribute("data-slide-to");
        slideIndex = slideTo; // Индекс = текущему слайдеру с таргета
        offset = width * (slideTo - 1); // считаем оффсет относительно тыкнутого слайдера
        createOffset(offset);
 
        if (slides.length < 10) {
            current.textContent = `0${slideIndex}`;
        } else {
            current.textContent = slideIndex;
        }
 
        activateDot();
    })
});

015 Как сохранить данные без БД. Работа с localStorage

localStorage- это свойство объекта Window, которое хранит в себе данные и сохранено в браузере пользователя. Оно позволяет сохранить данные даже после перезагрузки страницы

Пример использования:

  1. Запомнить время, на котором пользователь остановился
  2. Запомнить введённые данные в поля
  3. Запомнить настройки сайта (тёмная/светлая тема)

Однако нужно отметить, что нам доступно только 5 мб информации

И вот пример того, как мы можем закинуть данные в локальное хранилище данных через localStorage.setItem('ключ', значение). В браузере мы можем просмотреть настройки во вкладке Application

localStorage.setItem('number', 5);

И так же через getItem() мы можем получить нужную нам строку

localStorage.setItem('string', 'some long string in this life');
console.log(localStorage.getItem('string'));

А уже таким образом мы можем удалить ненужное нам значение

localStorage.removeItem('string');

Уже эта команда полностью очищает локальное хранилище

localStorage.clear();

Через этот код мы реализовали сохранение галочки на странице. Теперь при перезагрузке она остаётся в позиции checked

'use strict';
 
const checkbox = document.querySelector('#checkbox'),
      form = document.querySelector('form'),
      change = document.querySelector('#color');
     
// Проверим, есть ли наша запись при пезагрузке страницы
if (localStorage.getItem('isChecked')) { // если будет отсутствовать, то = null, а null === false
    checkbox.checked = true;
}
 
// Создадим новую запись в localStorage на клике галочки
checkbox.addEventListener('change', () => {
    localStorage.setItem('isChecked', true);
});

А тут уже показана реализация изменения цвета формы по листенеру и сохранение этих настроек даже после перезагрузки. Цвет можно убрать только нажав ещё раз по кнопке

// Проверка на наличие цвета
if (localStorage.getItem('bg') === 'changed') {
    form.style.backgroundColor = 'violet';
}
 
// Листенер на изменение цвета
change.addEventListener('click', () => {
    if (localStorage.getItem('bg') === 'changed') {
        localStorage.removeItem('bg');
        form.style.backgroundColor = '#fff';
    } else {
        localStorage.setItem('bg', 'changed');
        form.style.backgroundColor = 'violet';
    }
});

Ну и так же в localStorage мы можем закинуть объект (но только уже стрингифайнутый в JSON)

const person = {
    name: 'Alex',
    age: 18
}
 
const serializedPerson = JSON.stringify(person);
 
localStorage.setItem('alex', serializedPerson);

Ну и так же спокойно мы можем получить это значение с сайта

016 Регулярные выражения

Где используются? Для работы со строками: удаление, замена части слов, ограничить ввод определённых знаков, для поиска определённой буквы или сочетания букв в строке и так далее.

Регулярные выражения состоят из паттерна и флага. Записываются паттерны так: /паттерн/флаги.

Пример записи флагов выглядит так: всегда записываем сам паттерн и затем уже его флаги

new RegExp("pattern", "flags"); // редко используется
/pattern/flags; // используется часто

И тут мы в переменной answer через метод search() ищем pattern внутри ответа. Метод поиска вернёт нам индекс, где в слове находится паттерн. Тут 0 - A, 1 - n, ищем мы - n

При отсутствии нужного значения, получим “-1

Флаги:

  • i – пытаемся найти что-то вне зависимости от регистра
  • g – пытаемся найти сразу несколько вхождений
  • m – многострочный режим

И теперь мы можем попробовать вывести сразу несколько значений вне зависимости от регистра. Для этого мы должны будем поменять метод (search находит всегда только первое вхождение) на match(), который возвращает массив значений. А так же прописать нужные флаги (в любом порядке)

const answer = prompt("Имя?");
const pattern = /n/igm;
console.log(answer.match(pattern));

Если внутрь регулярного выражения поместить точку, то это означает, что мы берём все значения из переменной. Конкретно сейчас мы воспользуемся методом replace(), который заменит все символы, которые мы в нём укажем

const pass = prompt("Пароль");
console.log(pass.replace(/./g, "*"));

|600

Так же мы можем экранировать точку и выделять только её. Делаем это мы, потому что точка - это зарезервированный спецсимвол

const pass = prompt("Пароль");
 
console.log(pass.replace(/\./g, "*"));

Кроме точки у нас есть ещё много разных зарезервированных спецсимволов: |, \, /, ^, &

И вот пример прямого использования строкового метода. Тут мы можем непосредственно и быстро изменять нужный нам текст.

| 600

Дальше пойдут методы, которые непосредственно относятся к паттернам. Все прошлые относились к строкам. Метод test() вернёт boolean значение. Отвечает за поиск паттерна в слове

| 600

Так же перед нами может встать задача, которая будет заключаться в поиске значений определённого типа. Например, поиск чисел или слов. Для этого используются классы:

\d // - digits - числа
\w // - words - слова
\s // - spaces - пробелы

И вот пример поиска чисел:

Так же мы можем вписать заборчик из того паттерна, который нам нужно найти. Тут мы уже ищем слово, которое совпадает под наш паттерн

Так же у нас имеются обратные классы, которые ищут НЕ_тип

\D // Ищет НЕ цифры
\W // НЕ буквы
\S // НЕ пробелы

Пример поиска НЕ цифр:

Дальше уже перейдём в наш проект

Напомню, что мы получаем свойство ширины из компутированных стилей в браузере. Тут мы получаем строку ‘560px

const sourceWidth = window.getComputedStyle(slidesWrapper).width;

Это первый вариант получения чисел из полученного массива. Тут мы складываем все полученные числа через reduce

const width = +sourceWidth.match(/\d/g).reduce((acc, val) => {
    return acc += val;
});

А вот второй вариант. Тут мы все НЕ числа заменяем на пустую строку и переводим полученный результат в число

const width = +sourceWidth.replace(/\D/g, '');

017 Создаем калькулятор на сайте, часть 1

Берём наш HTML-код:

// Кнопки женщины и мужчины
<div class="calculating__choose" id="gender">
    <div id="female" class="calculating__choose-item calculating__choose-item_active">Женщина</div>
    <div id="male" class="calculating__choose-item">Мужчина</div>
</div>
 
// Кнопки активности жизнедеятельности
// Через data-ratio мы задаём коэффициент для формулы в JS
<div class="calculating__subtitle">
    Выберите вашу физическая активность
</div>
<div class="calculating__choose calculating__choose_big">
    <div data-ratio="1.2" id="low" class="calculating__choose-item">Низкая активность </div>
    <div data-ratio="1.375" id="small"  class="calculating__choose-item calculating__choose-item_active">Невысокая активность</div>
    <div data-ratio="1.55" id="medium" class="calculating__choose-item">Умеренная активность</div>
    <div data-ratio="1.725" id="high" class="calculating__choose-item">Высокая активность</div>
</div>

И сама реализация нашего калькулятора:

const result = document.querySelector(".calculating__result span");
// Задали значения по умолчанию, чтобы всё работало без клика по кнопкам
let sex = "female",
    weight, height, age,
    ratio = 1.375;
 
// Калькулятор результата ккал в день
function calcTotal() {
    if (!sex || !height || !weight || !age || !ratio) {
        result.textContent = "----";
        return; // досрочно прервёт функцию
    }
 
    if (sex === "female") {
        result.textContent = Math.round((447.6 + (9.2 * weight) + (3.1 * height) - (4.3 * age)) * ratio);
    } else {
        result.textContent = Math.round((88.36 + (13.4 * weight) + (4.8 * height) - (5.7 * age)) * ratio);
    }}
 
// Активирует эффект нажатой кнопки
function getStaticInformation(parentSelector, activeClass) {
    const elements = document.querySelectorAll(`${parentSelector} div`);
 
    elements.forEach(elem => {
        elem.addEventListener("click", (e) => {
            if (e.target.getAttribute("data-ratio")) {
                ratio = +e.target.getAttribute("data-ratio"); // вытаскиваем значение активности из атрибута
            } else {
                sex = e.target.getAttribute("id");
            }
 
            elements.forEach(elem => {
                elem.classList.remove(activeClass);
            });
 
            e.target.classList.add(activeClass);
 
            calcTotal();
        });
    });
}
 
// Каждый раз при вводе данные будут записываться в нужный идентификатор
function getDynamicInformation(selector) {
    const input = document.querySelector(selector);
 
    input.addEventListener("input", () => {
        switch(input.getAttribute("id")) {
            case "height":
height = +input.value;
                break;
            case "weight":
                weight = +input.value;
                break;
            case "age":
                age = +input.value;
                break;
        }
 
        calcTotal();
    });
}
 
calcTotal();
getStaticInformation("#gender", "calculating__choose-item_active");
getStaticInformation(".calculating__choose_big", "calculating__choose-item_active");
getDynamicInformation("#height");
getDynamicInformation("#weight");
getDynamicInformation("#age");

018 Создаем калькулятор на сайте, часть 2

Сначала сделаем проверку на вводимые значения в наши поля

При обращении к localStorage, мы сохраняем выбор пользователя в локальное хранилище, чтобы при перезагрузке сохранить эти значения

И немного перепишем задание наших переменных по умолчанию. Конкретно тут мы будем хранить наши значения в локальном хранилище и каждый раз при стирании их из локального хранилища (очистка кэша сайтов), у нас будут загружаться эти значения

И вот сама функция, которая будет реализовывать у нас сохранение положения активных классов при перезагрузке страницы

019 Геттеры и сеттеры (свойства объектов)

Свойства объектов делятся на две группы: свойства данные и свойства акцессоры. Свойства акцессоры так называются именно потому, что при вызове не ставятся круглые скобки – подразумевается, что мы обращаемся к свойству-методу

Реализуются свойства акцессоры через функции, которые начинаются с get и set. Такой функционал может напоминать шарп. Данные функции позволяют задать функционал отображения и задания свойства в объекте. Если написать гет, то через него мы получаем свойство. При использовании сета, мы задаём новое значение свойства. Если сет не написать, то и задать через эту функцию не получится.

Данные методы позволяют реализовать интерфейс задания и получения данных, а так же являются частью инкапсуляции (через них и получится инкапсулировать данные в объекте)

020 Инкапсуляция

Вот пример функции-конструктора, который создаёт нам персонажа. Тут мы легко можем поменять переменные внутри пользователя

Однако если мы создадим переменную, то она станет недоступна для просмотра извне, но останется вполне себе изменяемой

Дальше у нас идут собственные геттеры и сеттеры. Это методы, которые реализуют наш вывод данной переменной во внешнюю среду

Так же мы можем переписать наш код на классы. Однако тут встаёт проблема, которая запрещает нам использовать простые переменные в конструкторе (let) и теперь опять наши переменные можно просто увидеть и изменить извне без геттеров и сеттеров

По соглашению о наименовании переменных, если переменная начинается с нижнего подчёркивания (например, _age), то эту переменную нельзя изменять

И вот через сеттер age мы можем добавить нужное нам значение возраста

Однако у нас до сих пор есть возможность менять переменную, которую мы можем получить через геттеры и сеттеры. Есть возможность инкапсулировать значения полностью, но это уже экспериментальная технология

Так же можно инкапсулировать нормально значения в TypeScript

Через решётку создаётся приватное поле класса. Оно создаётся вне конструктора, чтобы можно было обратиться к нему везде.

Так же он выполняет нужную нам функцию – не выходит за пределы класса. Его можно только просмотреть через метод (при обычном вызове будет undefined). Поменять извне – не получится

И вот реализация геттера и сеттера для приватного поля класса

021 Прием модуль, как и зачем его использовать

Проблема: иногда, при использовании сторонних библиотек, мы можем сталкиваться с проблемой, что мы создаём переменную, которая уже присутствует в библиотеке. Такие конфликты происходят в основе своей из-за того, что мы почти постоянно находимся в глобальной области видимости

Моделируем ситуацию:

<script defer src="lib.js"></script>
<script defer src="script.js"></script>

Как итог, мы получаем вот такую вот ошибку:

Один из способов решения проблемы: использовать IIFE функцию. Она создаёт локальную область видимости и исключает соударения с глобальной

(function() {  
    const a = 22;
}());

И тут нужно показать пример, который создаст для нас отдельный модуль, через который можно будет вызывать нужные для нас функции

const user = (function() {  
    const private = function () {
        console.log('I\'m a private');
    }
   
    return {
        sayHello: private
    };
}());
 
user.sayHello(); // I'm a private

022 Webpack. Собираем наш проект

Все большие проекты на JS разбиваются на различные блоки кода и эти блоки кода убирают в отдельные файлы. Дробление проекта на отдельные модули позволяет нам нормально поддерживать проект и делить логику на составные части. Однако мы встречаемся с той проблемой, что браузеры не понимают деление проекта на модули и поэтому за помощью нам нужно обращаться к сборщикам модулей, которые создадут один файл из множества маленьких. И уже этот единственный файл браузер сможет обработать и смочь с нам работать

Webpack - это сборщик модулей. Он позволяет нам собрать проект, состоящий из большого количества отдельных файлов с экспортами и импортами.

Gulp - это планировщик задач. Он выполняет определённые задачи при наших определённых действиях. Например, он может компилировать препроцессоры при сохранении кода

Вот пример импорта и экспорта с использованием CommonJS:

// lib.js - export
 
"use strict";
 
function myModule() {
    this.run = function() {
        console.log('run');
    };
 
    this.go = function() {
        console.log('go');
    };
}
 
module.exports = myModule; // exports
// script.js - import
 
const myModule = require('./lib'); // import
 
const myModuleInstance = new myModule();
myModuleInstance.run(); // run

Установка Webpack:

mkdir webpack-demo
cd webpack-demo
npm init
npm install webpack webpack-cli --save-dev
 
// Либо
npm install // установит все зависимости из package.json

Для запуска паковщика модулей:

npx webpack

Дальше нужно перейти к настройке конфигурации вебпака:

  • mode - девелопментом пользуемся, когда нужно просто работать над сайтом, а ставим продакшн, когда выпускаем сайт на продакшен (там будут модули для оптимизации кода)
  • entry - указываем точку входа в программу
  • output - настраивает выходной файл
  • watch - следит за файлами и пересобирает проект при каждом изменении
  • devtool - определяет логику сохранения карты расположения кода (лучше чекнуть документацию)
  • module - устанавливает дополнительные модули (например, babel)

'use strict';
 
let path = require('path');
 
module.exports = {
  mode: 'development', // or production
  entry: './js/script.js', // входная точка
  output: {
    filename: 'bundle.js', // название выходного файла
    path: __dirname + '/js' // путь до него
  },
  watch: true,
  devtool: "source-map",
  module: {}
};

И вот, что нам даёт карта. Мы можем видеть оригинальный несжатый код

И как с этим всем работать?

Нужно весь код, который мы переносим в другой модуль, обернуть в функцию и эту функцию экспортнуть. В принимаемом файле нужно через require получить и вызывать эту функцию. Далее просто запускаем npx webpack с настройками выше и получаем готовый результат

function calculator() {
    const result = document.querySelector(".calculating__result span");
    // ...
}
 
module.exports = calculator;

Ну и в самом HTML подключаем выходной файл

<script src="js/bundle.js"></script>

При запуске Webpack, он начинает следить за проектом и работает на постоянной основе, поэтому для работы с терминалом, нам нужно запустить второй

Так же мы можем через Ctrl+C отключить работу сборщика модулей

023 ES6 Modules

Импорт ES6 - это уже более современный способ импорта данных в файлы. Он обеспечивает более понятный синтаксис импорта, который представляет из себя конструкцию: export для экспорта элемента и import from для принятия экспорта из другого файла. Через as мы можем задать другое имя для импортируемого объекта (например, если имена очень длинные) Если мы используем export default, то конкретно этот объект нам не нужно будет заключать в { } при импорте Ещё одна важная вещь - *модули ES6 так же как и CommonJS нужно собирать сборщиком модулей Webpack*

// lib.js - export
 
"use strict";
 
export const ex = 4;
 
const a = 1;
const b = 2;
const c = 3;
 
export { a, b, c};
 
export function go() {
    console.log('go');
}
 
export default class User {
    constructor(name) { console.log(name); }
}
// script.js - import
 
import User, { a as first, b, c, ex } from './lib'; // импорт
 
new User('Andrew'); // Использование
 
console.log(first);

Через запись * as all мы импортируем всё под именем all. И теперь мы получили объект all, хранящий все экспорты из нужного нам файла

import * as all from './lib';
 
console.log(all.a); // 1

Интересная особенность дефолтного экспорта заключается в том, что под капотом он выглядит при импорте так:

import {default as User} from './lib'; // ок - реализован так
import User from './lib'; // ок - выглядит так

И теперь мы можем подключить эти модули к браузеру. Можно воспользоваться вебпаком и сделать всё удобно, но так же мы можем обойтись и подключением, которое может сделать браузер. Для нормальной работы нам нужно:

  • './lib.js' указать импорты с правильным наименованием браузера
  • В HTML самым последним скриптом указать тот, который собирает все импорты
  • Указать атрибут type="module"
import User, { a as first, b, c, d} from './lib.js';
<script type="module" src="js/lib.js"></script>
<script type="module" src="js/script.js"></script>

024 Собираем наш проект и фиксим баги

В первую очередь, нам нужно дефолтно экспортировать наши обёртки и правильно их импортировать (в глобальной области видимости)

Дальше мы встречаемся с такой прописной истиной: все модули должны быть обособлены друг от друга. На нынешний момент времени нам нужно решить проблему с функцией, которая запускается в разных модулях и решить вопрос с переменными

Первым делом вынесем функции, которые нужны в других модулях и экспортнём их. Дальше для всех функций создадим вызываемость с аргументами

export function closeModal(modalSelector) {
    const modal = document.querySelector(modalSelector);
    modal.classList.toggle("hide");
    document.documentElement.style.overflow = "";
}
 
export function openModal(modalSelector) {
    const modal = document.querySelector(modalSelector);
    modal.classList.toggle("hide");
    document.documentElement.style.overflow = "hidden";
    clearTimeout(modalTimer);
}
 
function modal(triggerSelector, modalSelector) {
    const modal = document.querySelector(modalSelector),
    modalBtns = document.querySelectorAll(triggerSelector);
    //...
}
 
export default modal;
export {openModal, closeModal};

Импорт функций

//forms.js
import {openModal, closeModal} from './modal';

Мы встретимся с такой проблемой, что нам нужно добавить аргументы в функцию, которую не вызываем в данный момент времени (которая без ())

modalBtns.forEach(btn => {btn.addEventListener("click", openModal);}); // openModal не имеет ()

Для этого функцию с аргументами оборачиваем в колбэк

modalBtns.forEach(btn => {btn.addEventListener("click", () => openModal(modalSelector));});

И тут дальше хочется отметить, что не просто так учили мы методы строк. Мы можем модифицировать переменные и, если наш аргумент селектора немного не подходит, сделать это:

if (target && target.classList.contains('tabheader__item')) {

tabsSelector = ".tabheader__item"

tabs(".tabheader__item", ".tabcontent", ".tabheader__items", "tabheader__item_active");

Слайсим переменную и пользуемся ей

if (target && target.classList.contains(tabsSelector.slice(1))) {

Сделаем подобие деструктурированной передачи информации для слайдеров как в slickslider

И тут мы реализовали передачу аргументов в функцию через деструктуризацию, которая происходит через {}. Это даёт нам возможность не передавать какие-либо аргументы (если они не нужны) или передавать их в хаотичном порядке

// Реализация в slider.js
function slider({container, slide, nextArrow, prevArrow, totalCounter, currentCounter, wrapper, field}) {
 
    const slides = document.querySelectorAll(slide),
        slider = document.querySelector(container), // Добавляем сам слайдер, чтобы относительно него спозиционировать точки
        prev = document.querySelector(prevArrow),
        next = document.querySelector(nextArrow),
        total = document.querySelector(totalCounter),
        current = document.querySelector(currentCounter),
        slidesWrapper = document.querySelector(wrapper),
        slidesField = document.querySelector(field);
// Вызов из script.js
slider({
    container: ".offer__slider",
    totalCounter: "#total",
    currentCounter: "#current",
    prevArrow: ".offer__slider-prev",
    nextArrow: ".offer__slider-next",
    wrapper: ".offer__slider-wrapper",
    field: ".offer__slider-inner",
    slide: ".offer__slide"
});

Модульный подход к разработке сайта предоставляет нам возможность

  • переиспользовать код
  • более эффективно его модифицировать за счёт того, что теперь это не простыня, а отдельные блоки кода

025 Формируем портфолио на GitHub

Гитхаб - это не только площадка для постинга репозиториев, но и отличная площадка для отображения своих скилов программиста. Тут мы можем в открытый доступ выкладывать наши приложения для того, чтобы работодатель мог увидеть наши скилы. В первую очередь внимание нужно обратить на оформление нашего гита. Нужно фото - желательно с улыбкой, имя и достаточное количество проектов (даже самых малых) + желательно много новых коммитов, которые покажут, что мы активно работаем

026 Ошибки. Как избежать “поломки” своего кода

Для обработки ошибок у нас существует стандартная конструкция try-catch. Внутрь него вкладываем код, который подозреваем на ошибку и в блоке catch пишем обработку нашей ошибки. Так же этот блок принимает в себя параметр ошибки. Из самой ошибки мы можем получить имя, сообщение и стек ошибки. Ну и блок finally, который необязателен, но позволяет выполнить логику, которая должна обязательно произойти

try { // проверяемый блок кода
    console.log('work');
    throw new Error('Some error');
} catch(e) { // Если ошибка
    console.log(e.name + ' - error name');
    console.log(e.message + ' - error message');
    console.log(e.stack + ' - error stack');
} finally { // блок кода, который выполняется в любом случае
    // Завершаем какую-то операцию
}
 
console.log('that\'s works!');

Для чего, например, может использоваться такой подход в повседневной работе? У нас есть функция, которая подключена сразу к двум документам. Эта функция из одного из них берёт информацию и обрабатывает её. Если на первой странице всё будет нормально, то на второй выйдет ошибка и код дальше работать не будет.

Уже с таким кодом ничего не будет падать и мы спокойно сможем работать одним скрипт-файлом на нескольких страницах

027 (д) Создание своих ошибок

Представим, что нам нужно добавить на страницу несколько блоков с определёнными элементами по тегу и атрибуту В результате выполнения этого кода у нас будет один элемент без id

'use strict';
 
// Содержит будущие элементы
const data = [
    {
        id: 'box',
        tag: 'div'
    },
    {
        id: '',
        tag: 'nav'
    },
    {
        id: 'circle',
        tag: 'span'
    }
]
 
// Выводит эти элементы на страницу
data.forEach(blockObj => {
	// Создаст элемент с определённым тегом
    const block = document.createElement(blockObj.tag);
    // Добавит ему определённый атрибут
    block.setAttribute('id', blockObj.id);
    // Добавит элемент на страницу
    document.body.append(block);
})

Однако если мы воспользуемся оператором throw, то мы сможем предотвратить ошибку. Этот оператор выдаёт в консоль какое-то сообщение (строку 'string' или созданную нами ошибку new Error()) и останавливает выполнение дальнейшего кода По итогу мы получим только один элемент

data.forEach((blockObj, i) => {
    const block = document.createElement(blockObj.tag);
 
    if (!blockObj.id) throw new Error(`Под индексом ${i} нет данных об id`); // !
 
    block.setAttribute('id', blockObj.id);
    document.body.append(block);
})

Так же есть SyntaxError, TypeError, ReferenceError и другие ошибки, которые можно выделить под определённые предполагаемые ситуации

const err = new ReferenceError('Ref Err');
console.log(err.name, err.message, err.stack);

И вот пример самих сущностей, которые мы получаем при ошибке

try {
    data.forEach((blockObj, i) => {
        const block = document.createElement(blockObj.tag);
        if (!blockObj.id) throw new SyntaxError(`Под индексом ${i} нет данных об id`);
        block.setAttribute('id', blockObj.id);
        document.body.append(block);
    })
} catch (e) {
    console.error(e.name);
    console.log(e.message);
    console.log(e.stack);
}

Так же мы можем дописать логику для непредвиденных ошибок, которые выходят за рамки того, что можно предугадать. Для этого через условие можно прописать в блоке catch, какую ошибку мы ожидаем получить, а какая ошибка уже говорит о критических недоработках кода

'use strict';
 
const data = [
    {
        id: 'box',
        tag: 'div'
    },
    {
        id: 'had', // нормально
        tag: 'nav'
    },
    {
        id: 'circle',
        tag: '' // другая ошибка
    }
]
 
try {
    data.forEach((blockObj, i) => {
        const block = document.createElement(blockObj.tag);
        if (!blockObj.id) throw new SyntaxError(`Под индексом ${i} нет данных об id`);
        block.setAttribute('id', blockObj.id);
        document.body.append(block);
    })
} catch (e) {
    if (e.name === 'SyntaxError') { // если моя ошибка
        console.error(e.message); // То вывести сообщение ошибки
    } else throw e; // если ошибка не моя (непредвиденная), то пропустить ошибку
}

028 Как превратить код ES6+ в старый формат ES5. Babel, Core.js и полифиллы

Трансплиттер - это инструмент, который переводит код нового формата в более старый формат, который, например, понятен старым браузерам Полифилл - это участки старого кода, которые эмулируют поведение современных стандартов кода

Использовать в своей работе мы будем babel. Его уже в свою очередь нужно будет подключить к сборщику модулей

// Установка babel
npm install --save-dev @babel/core @babel/cli @babel/preset-env
 
// для установки всех полифилов в проект (даже неиспользуемых)
npm install --save @babel/polyfill
 
// для работы конфига
npm install --save-dev babel-loader
 
// Библиотека, которая оставляет только нужные полифиллы в проекте
npm install --save-dev corejs

Это настройка для package.json, которая скажет babel для каких браузерв нужно подстраиваться

Это настройки для webpack.config.js, которые нужны для babel

'use strict';
 
let path = require('path');
 
module.exports = {
  mode: 'production',
  entry: './js/script.js',
  output: {
    filename: 'bundle.js',
    path: __dirname + '/js'
  },
  watch: true,
 
  devtool: "source-map",
 
  // Настройка модулей в webpack
  module: {
  // Правила для модулей
    rules: [
      {
        test: /\.m?js$/,
        // Исключаем эти файлы
        exclude: /(node_modules|bower_components)/,
        // работаем с этими модулями
        use: {
	      // Модуль установили чуть выше
          loader: 'babel-loader',
          // Настройки для этого лоадера
          options: {
	        // Пресеты есть на сайте babel во вкладке Presets
            presets: [['@babel/preset-env', {
                // Вывод полной информации при дебаге
                debug: true,
                // Версия библиотеки для полифилов (corejs)
                corejs: 3,
                // Выбирает только используемые полифиллы
                useBuiltIns: "usage"
            }]]
          }
        }
      }
    ]
  }
};

Дальше для активации Babel нам нужно запустить вебпак через npx webpack. После компиляции файл может весить до двух раз больше :(

Так же нужно упомянуть, что иногда некоторые полифиллы могут не сработать и из-за этого может полететь функционал на странице. Для исправления ситуации, нужно зайти в гугл и ввести: <функция> polyfill js Например, на том же npm можно найти полифилл для промиса

// Установка полифилла
npm install es6-promise
// Активация полифилла в проекте
require('es6-promise').polyfill();

Так же есть и другой вариант импорта полифиллов. Так же если мы пропишем просто import 'имя_пакета', то он импортнётся из папки модулей

npm i nodelist-foreach-polyfill

Так же такой вариант импорта работает и для других отдельных полноценных плагинов, которые мы можем просто установить через npm и так импортнуть в проект

И лучше плагины подключать именно так, так как это всё скомпилируется в один JS файл без кучи других отдельных с разными зависимостями

import 'slick-slider';

029 Современные библиотеки и фрэймворки

Фреймворки и библиотеки: Angular React Vue

Библиотека - это просто готовые участки кода, которые можно как использовать для быстрого написания какой-то логики, так и не использовать Библиотека зачастую направлена на решение одной задачи

Фреймворк - это уже определённая структура, которая диктует правила, как нужно писать код программисту и заставляет его их придерживаться Фреймворки позволяют нам создать полноценное приложение прямо из того функционала, который в нём присутствует

Фреймворки позволяют нам создавать SPA (Single Page Applications), которые, в свою очередь, представляют из себя приложения в браузере. Эти приложения позволяют работать с сайтом как с программой, которая располагается на удалённом компьютере. При этом при переходе в другие вкладки внутри приложения - страница не перезагружается.

Что нужно знать, прежде чем изучать фреймворки:

  • Angular
    • TypeScript
    • node.js
    • Webpack
    • MVC pattern
    • Сам фреймворк (документация)
  • React
    • node.js
    • JSX (препроцессор)
    • Babel (компилятор JSX)
    • Webpack
    • Сам фреймворк (документация)
  • Vue
    • node.js
    • Webpack
    • Сам фреймворк (документация)

Так же стоит упомянуть о jQuery. Это библиотека, которая используется в большом количестве старых сайтов для создания интерактива. Её просто стоит знать, чтобы понимать, с чем может прийтись работать.

030 Библиотека Jquery

Самая простая установка:

// устанавливаем библиотеку для сайта
npm i jquery --save
 
// будем компилировать через вебпак, так как будем использовать импорты
npx webpack
import 'jquery';

Использование в компилируемом файле:

import 'jquery';
import $ from 'jquery'; // Просто чтобы не видеть ошибку в консоли в браузере
 
// $() - это функция получения элемента со страницы
const btn = $('#btn');
 
console.log(btn);

Все функции JQuery можно найти здесь.

  • JQuery UI - это пользовательские модификации
  • Ajax - это технология запросов, которая была прототипом функции fetch, которую добавили уже после

И вот пример быстрой реализации некоторых анимаций на JQuery:

import 'jquery';
import $ from 'jquery';
 
// ready() - проверяет, что все нужные компоненты со страницы загружены
$(document).ready(function() {
    // Выбираем первый элемент из списка list-item'ов
    // hover() - срабатывание при наведении    $('.list-item:first').hover(function() {
        // $(this) - обращение к ЭТОМУ элементу
        $(this).toggleClass('active');
    });
    // Обращаемся к третьей кнопке и назначем ей eventListener
    $('.list-item:eq(2)').on('click', function() {
        // Обращаемся ко всем чётным элементам (0-1-2-3 - 1, 3)
        // fadeToggle() - производит скрытие и показ элементов
        $('.image:even').fadeToggle('slow');
    })
    // Обращаемся к пятой кнопке и назначем ей eventListener
    $('.list-item:eq(4)').on('click', function() {
        // Обращаемся ко всем нечётным элементам (0-1-2-3 - 0, 2)
        $('.image:odd').animate({
            opacity: 'toggle',
            height: 'toggle'
        }, 2000);
    })
});

Стоит сказать, что JQuery не стоит использовать в современных проектах.

  • Лет 10-12 назад он добавлял функционал на сайт, который делать через JS было очень тяжело и долго. Либо добавлял такие функции, которые сделать было просто невозможно из нативного JS.
  • Используем JQuery только для поддержки старых проектов - но не для написания новых!

031 Функции-генераторы

Функция-генератор в разные моменты времени выдаёт разные результаты. Первым делом нам нужно присвоить функцию в новую переменную и через эту переменную вызвать методы, которые контролируют поведение функции-генератора. Одним из методов, который вызывают следующее значение является next(). Так как этот метод возвращает объект, то мы сразу можем обратиться к нужному нам свойству.

function* generator() {
    yield 'S'; // Первый вызов
    yield 'b'; // Второй вызов
    yield 'c'; // Третий вызов
    yield 'd'; // Четвёртый вызов
}
 
const str = generator();
 
console.log(str.next());
console.log(str.next().value); // сразу получаем значение
console.log(str.next());
console.log(str.next());
console.log(str.next());
{ value: 'S', done: false }
b
{ value: 'c', done: false }
{ value: 'd', done: false }
{ value: undefined, done: true }

И вот пример функции-генератора, которая будет при каждом вызове возвращать новое значение

function* count(n) {
    for (let i = 0; i < n; i++) {
        yield i;        
    }
}
 
const counter = count(7);
 
console.log(counter.next().value); // 0
console.log(counter.next().value); // 1
console.log(counter.next().value); // 2

Так же конструкция for-of позволяет нам максимальное количество раз перебрать конструкцию функции-генератора

function* count(n) {
    for (let i = 0; i < n; i++) {
        yield i;        
    }
}
 
for (const k of count(7)) {
    console.log(k);
}

032 JS анимации, requestAnimationFrame

Главная проблема CSS-анимаций заключается в том, что построить сложную анимацию крайне сложно. Так же для настройки плавности анимации используются сугубо кривые Безье

p {
  animation-duration: 3s;
  animation-name: slidein;
}
 
@keyframes slidein {
  from {
    margin-left: 100%;
    width: 300%;
  }
 
  to {
    margin-left: 0%;
    width: 100%;
  }
}

Главная проблема анимаций на JS через тот же setTimeout заключается в том, что они не реагируют на частоту кадров пользователя на компьютере. Обычно она составляет 60 кадров в секунду, но ПК пользователя может быть нагружен и не способен выдавать нужное количество кадров в секунду

Поэтому был создан API: requestAnimationFrame. Он подстраивается под количество кадров на компьютере пользователя и позволяет реализовать адекватную анимацию. Так же он оптимизирует нашу анимацию (запускает анимацию и производит отрисовку в браузере одновременно, а не последовательно, как в обычных функциях).

Стандартный вариант

Вот так выглядит стоковая анимация через setInterval

const btn = document.querySelector(".animation-button"),
    box = document.querySelector(".box-rider");
 
function myAnimation() {
    const box = document.querySelector(".box-rider");
    let pos = 0;
 
    const id = setInterval(frame, 5);
    function frame() {
        if (pos === 480) {
            clearInterval(id);
        } else {
            pos++;
            box.style.top = `${pos}px`;
            box.style.left = `${pos}px`;
        }    }
}
btn.addEventListener('click', myAnimation)
Предпочтительный вариант

И вот так выглядит код с использованием requestAnimationFrame. Тут мы рекурсивно вызываем функцию по заданному условию, что позволяет нам сократить код и не использовать таймауты. Так же эта анимация и более оптимизирована под браузеры.

const btn = document.querySelector(".animation-button"),
    box = document.querySelector(".box-rider");
 
let pos = 0;
function myAnimationFrame() {
    pos++;
    box.style.top = `${pos}px`;
    box.style.left = `${pos}px`;
 
	// Тут идёт перевызов функции до тех пор, пока не будет выполнено условие
    if (pos < 480) {
        requestAnimationFrame(myAnimationFrame);
    }
}
 
// Вызов функции с аргументами в листенерах нужно осуществлять через стрелочные функции
btn.addEventListener('click', () => requestAnimationFrame(myAnimationFrame));

Этот код позволит остановить анимацию по нашему усмотрению

let id = requestAnimationFrame(myAnimationFrame);
cancelAnimationFrame(id);

033 Web Animations API

Web Animations API - это API, который связывает анимации JS и CSS. То есть в него мы передаём сначала ключевые кадры как в CSS, а уже потом передаём опции, которые влияют на эту анимацию

animate(keyframes, options)
document.getElementById("tunnel").animate([
  // keyframes
  { transform: 'translateY(0px)' },
  { transform: 'translateY(-300px)' }
], {
  // timing options
  duration: 1000,
  iterations: Infinity
});

Этим кодом мы заанимировали бесконечное движение телефона по ограниченным координатам

const btnPhone = document.querySelector('#iphone'),
      btnMacbook = document.querySelector('#macbook'),
      images = document.querySelectorAll('img');
 
const phoneAnimation = images[0].animate([
    // keyframes
    { transform: 'translateY(0)' },
    { transform: 'translateY(-100px)' },
    { transform: 'translateY(100px)' },
    { transform: 'translateY(0)' }
], {
    // timing options
    duration: 3000,
    iterations: Infinity
});

Анимация имеет 4 состояния (PlayState):

  • idle
  • running
  • paused
  • finished

Тут уже представлена полноценная реализация этой анимации со стейтами паузы, а так же с несколькими изменяемыми свойствами. Это более удобный вариант нежели чем создание такой же анимации через CSS

const btnPhone = document.querySelector('#iphone'),
      btnMacbook = document.querySelector('#macbook'),
      images = document.querySelectorAll('img');
 
// переменная, которая будет хранить анимацию
let phoneAnimation;
// Регулируем анимацию по кнопке
btnPhone.addEventListener('click', () => {
    // Запускаем анимацию при нажатии на кнопку
    if (!phoneAnimation) {
        phoneAnimation = images[0].animate([
            // keyframes
            { transform: 'translateY(0) rotate(0)',
                filter: 'opacity(100%)'},
            { transform: 'translateY(-100px) rotate(180deg)',
                filter: 'opacity(50%)'},
            { transform: 'translateY(100px) rotate(270deg)',
                filter: 'opacity(90%)'},
            { transform: 'translateY(0) rotate(360deg)',
                filter: 'opacity(100%)'}
        ], {
            // timing options
            duration: 3000,
            iterations: Infinity
        });
        // Если анимация находится в состоянии паузы, то
    } else if (phoneAnimation.playState === 'paused') {
        // ... нужно опять запустить анимацию
        phoneAnimation.play();
        // Если анимация есть и она не стоит на паузе, то мы её должны поставить на паузу
    } else {
        phoneAnimation.pause();
    }
})

034 Event loop, подробная работа асинхронных и синхронных операций

Ниже представлено сочетание синхронных и асинхронных функций:

console.log(1);
 
setTimeout(() => {
    console.log('timeout_1');
}, 2000)
 
setTimeout(() => {
    console.log('timeout_2');
}, 4000)
 
console.log(2);
 
// 1
// 2
// timeout_1
// timeout_2

Во втором случае команды оставим всё те же, но сеттаймауты сделаем по одинаковому времени. Вывод будет идентичен первому варианту, так как первый сеттаймаут в коде запустился чуть раньше второго

console.log(1);
 
setTimeout(() => {
    console.log('timeout_1');
}, 4000)
 
setTimeout(() => {
    console.log('timeout_2');
}, 4000)
 
console.log(2);
 
// 1
// 2
// timeout_1
// timeout_2

Call Stack - это операции, которые выполняются прямо сейчас на данный момент Web Apis - это хранилище в браузере для хранения промежуточных данных Callback Queue - это очередь задач. Все операции не могут выполняться параллельно, они встают в очередь друг за другом, чтобы нормально выполниться.

Что тут происходит? Все наши таймауты, ивенты на кнопках попадают в Web Apis и ждут своего выполнения (таймауты - конца таймера, кнопки - срабатывания). Дальше абсолютно все задачи попадают в порядке очереди в Callback Queue (очередь из синхронных задач по порядку и попадающие в них асинхронные функции). После того, как задача подошла к выходу из очереди, она попадает в Call Stack, где и выполняется. После выполнения новая задача попадает в стек из очереди.

Все события (клики, коллбэки, таймауты, промисы), которые мы вызовем, становятся в очередь и не могут выполниться одновременно, так как JS - это однопоточный язык.

Но если мы запустим функцию и внутри неё уже будут производиться итерации по циклу, то они будут выполняться сразу в Call Stack. Самый важный из этого вывод: если внутри цикла выполняется какая-то тяжёлая задача, то она будет тормозить всю очередь на странице - то есть кроме этого цикла ничего выполняться на странице не будет Например, бесконечный цикл может полностью убить сайт от чего придётся перезапускать вкладку с этим сайтом

Пример

Такой цикл с перебором и записью стопорит сайт полностью на 10+ секунд и не даёт тыкать кнопки или выполнять какую-либо анимацию

let k;
 
function count() {
    for (let i = 0; i < 1e9; i++) { // 1 000 000 000
        k++;
    }
    alert('Браузер выполнил обработку');
}
 
count();

  1. setTimeout проходит всегда полный цикл асинхронных операций, поэтому он обязательно попадает сначала в Web Apis, что замедлит его выполнение. В таком примере всегда сначала выполняется синхронная операция, если таковая имеется сразу после асинхронной операцией.
  2. Минимальная длительность задержки = 4 мс (даже если мы напишем 0). Сделано это для совместимости с разными браузерами.
setTimeout(() => {
    console.log(1);
}, 0) // 4мс
 
console.log(2);
 
// 2
// 1

Очень важно понимать работу Event Loop для правильной работы с промисами, сервером и правильного построения архитектуры приложения, так как из-за непонимания этого подхода у нас может очень легко крашнуться сайт.

035 Макро и микрозадачи

Микро и макрозадачи:

  • Все задачи, которые попадают в Callback Queue, являются макрозадачами
  • Уже then, catch, finally и await относятся к микрозадачам

Конкретно в примере ниже у нас выполняется сначала макрозадача console.log('code'), после которой выполняются микрозадачи у промисов, которые сначала попали в Web Apis, а уже только потом выполняется макроздача setTimeout После выполнения какой-то макрозадачи, у нас обязательно выполняются сразу все микрозадачи, которые скопились в очереди (queue). Promise имеет свои микрозадачи, поэтому он имеет приоритет выполнения выше, чем setTimeout, который представляет из себя просто макрозадачу Выполняются сначала все микрозадачи ровно потому, что им важно то окружение на странице, с которым они поступили в очередь

setTimeout(() => console.log('timeout'))
 
Promise.resolve().then(() => console.log('promise'))
Promise.resolve().then(() => console.log('promise_2'))
 
console.log('code');
 
// code
// promise
// promise_2
// timeout

Так же с помощью функции queueMicrotask() мы можем создать собственную микрозадачу, которая выполнится между макрозадачами

setTimeout(() => console.log('timeout'));
 
Promise.resolve().then(() => console.log('promise'));
 
// Сгенерированная нами микрозадача
queueMicrotask(() => console.log('microtask'));
 
Promise.resolve().then(() => console.log('promise_2'));
 
console.log('code');
 
// code
// promise
 
// microtask
 
// promise_2
// timeout

036 Работаем с готовым кодом

На всех проектах мы используем JS, чтобы решать задачи. Иногда нам нужно решать абсолютно типовые задачи, которые писать самостоятельно нет никакого смысла.

Есть библиотека для автоматической анимации и работы со свайпами

Слайдеры: С использованием JQuery (займёт много места и замедлит сайт):

Работа с Tiny Slider

Устанавливается слайдер как через импорты в HTML, так и через npm

npm install tiny-slider

По возможности, подключаем плагин во внутрь скрипта, так как подключать всё в HTML и городить кашу - это некрасиво

import { tns } from "./node_modules/tiny-slider/src/tiny-slider"

Библиотека React. Базовый уровень

React

003 Фундаментальные принципы Реакта

Все фундаментальные принципы реакта описаны на его главном экране
  1. Реакт – имеет декларативный (что?) подход, то есть в нём мы описываем, какой результат нам нужен Императивный (как?) полностью описывает как дойти до определённого результата – описаны конкретные шаги
  2. Реакт основан на компонентах (элементы которые могут повторяться, но иметь разное содержимое)
  3. На реакте построено множество других библиотек, которыми можно писать те же мобильные приложения или приложения для VR
Отличительные особенности React
  1. Использует препроцессор JSX, который позволяет писать JS и вёрстку в одном месте
  2. В нём есть механизмы сравнения, которые позволяют отследить только те участки веб-приложения, которые изменились и изменить только их, а не всё приложение полностью (reconciliation algorithm)
  3. Виртуальное дерево. Реакт работает сначала с виртуальным деревом, которое он создаёт для себя. Потом уже изменения переносятся на основное дерево.
  4. Оптимизация. Она вытекает из прошлого пункта и обеспечивается более компактным формированием информации в объектах виртуального ДОМ Реакта

Пример обычного компонента в ДОМ-дереве: огромное количество свойств и строк

И пример ДОМ-элемента в реакте. Он имеет только небольшое количество свойств, что позволяет снять нагрузку на память

004 Create React App - создаем свое приложение

Чтобы создать реакт-проект, можно воспользоваться утилитой create-react-app, которая загрузит сразу шаблон

npx create-react-app first-react-proj

Это два основных файла в src. Всё остальное - это стили и тесты.

Это index.html, в котором и будет рендериться наша страница

И компиляцией jsx в нативный js занимается Babel

Официальная документация Babel

005 Работаем с JSX-препроцессором, ограничения в нем

При установке реакт-проекта, мы начинаем работать с index.js

import React from 'react';  // импорт реакта
import ReactDOM from 'react-dom/client';// импорт его виртульного ДОМ
// Так же мы можем напрямую подключать в него стили
import './index.css';
// Этот импорт отвечает за то, что находится изначально в рендере
import App from './App';
// Измеряет производительность нашего приложения
import reportWebVitals from './reportWebVitals';
 
// Получаем место, с которым будем работать
const root = ReactDOM.createRoot(document.getElementById('root'));
// Это метод рендера информации на страницу
root.render(
	// Сюда изначально помещён блок из App.js
	<React.StrictMode>
		<App />
	</React.StrictMode>
);

И вот так вот выглядит препроцессор JSX в написании: мы пишем HTML прямо внутри JS и выводим его через рендер. Рендер должен быть только один!

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
 
const elem = <h2>Hello, world!</h2>;  // !
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  elem  // !
);

А это старый вариант записи, который работает под капотом у препроцессора JSX

const elem = React.createElement('h2', {className: 'greetengs'}, 'Hello, world!');

После выполнения метода выше, нам возвращается вот такой вот объект:

const elem = {
	type: 'h2',
	props: {
		className: 'greetings',
		children: 'Hello, World!'
	}
};

Так же нужно сказать, что в каждый файл, где используется препроцессор JSX, не нужно импортировать React - достаточно иметь импорт в основном файле

И нужно описать несколько правил написания такого многострочного HTML внутри JSX:

  1. Все элементы нужно обернуть внутрь одного родителя <div><div/>
  2. Теги можем записать как самозакрывающиеся - <button />, так и полные - <button></button>
const elem = (
    <div>
        <h2>Hello, world!</h2>
        <div>some text</div>
        <input type='text' />
 
        <button>Click</button>
        <button />
    </div>
);

Если не обернём элементы внутрь одного тега, то получим такую ошибку

Ну и так же JSX позволяет вставлять интерполяцию { }, в которую можно вставлять переменные и выполнять функции

const text = 'Hello, world!';
const texth3 = <h3>Goodbye, world!</h3>;
const elem = (
    <div>
        <h2>Текст: {text}</h2>  // 'Hello, world!'
        {texth3}  // 'Goodbye, world!'
        <div>some text</div>
        <p>Посчитаем? 8+8 = {8+8}</p>
        <input type='text' />
 
        <button>Click</button>
        <button />
    </div>
);
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  elem
);

Единственное исключение - в эту интерполяцию нельзя вкладывать объекты. На выходе мы получим ошибку, так как тут идёт конвертация объекта в строку - Object object. Делается конвертация для обеспечения безопасности сайта (чтобы не было вмешательств со стороны)

const elem = (
    <div>
        <h2>Текст: {new Date()}</h2>  // !
        <input type='text' />
        <button>Click</button>
        <button />
    </div>
);

Все атрибуты тегов записываются в camelCase и имеется два атрибута, которые имеют отличное написание от того, что есть в оригинальном HTML - это className = class и htmlFor = for

const elem = (
    <div>
        <h2 className="text">Текст: {['234']}</h2>
        <label htmlFor=""></label>
        <input type='text' />
    </div>
);

006 Элементы и компоненты

Компонент - это блок пользовательского интерфейса, который может иметь своё поведение. Это самостоятельная структура, которую мы можем переиспользовать в различных местах приложения. В первую очередь, компонент представляет из себя функцию, которая возвращает JSX блок кода.

Элемент - это структурная часть компонента.

<h2 className="text">Текст: {['234']}</h2>

И тут мы столкнёмся с другой особенностью этой библиотеки - мы не можем изменить уже отрендеренный элемент на странице. Для этого нужно поменять его состояние.

Компонент записывается в виде функции и используется при рендере в виде тега. И вот пример использования компонентов в React. Компоненты в блоке рендера на странице записываются внутри:

<Компонент/> или <Компонент> Вложение </Компонент>

Имя компонента всегда обязательно должно начинаться с заглавной буквы, а иначе babel воспримет и скомпилирует его как элемент!

// App.js - хранит компоненты
const Header = () => {
    return <h2>Hello, neighbour!</h2>
}
 
function App() {
    return (
        <div className="App">
            <Header/> {/*Ничего в себя не вкладывает*/}
            <Header>
                {/* Может дать в себя что-нибудь вложить*/}
            </Header>
        </div>
    );
}
 
export default App;

Тут мы вызваем рендер основного компонента App из корневого файла index.js.

Вложить в компонент можно любой элемент и так же можно запустить функцию

import logo from './logo.svg';
import './App.css';
 
const Header = () => {
    return <h2>Hello, neighbour!</h2>
}
 
const Field = () => {
    return <input type="text" placeholder="Type here"/>
}
 
const Btn = () => {
    const text = 'Log in';
    const textFunc = () => {
        return 'Log in';
    }
    const p = <p>man</p>;
    return <button>{text} {textFunc()} {p}</button>;
}
 
function App() {
    return (
        <div className="App">
            <Header/>
            <Field/>
            <Btn/>
        </div>
    );
}
 
export default App;

Так же можно использовать любые выражения внутри {}. Конкретно тут, если пользователь залогинен, то он может выйти. Если не залогинен, то может войти. Кнопка вход/выход.

const Btn = () => {
    const logged = true;
    const text = 'Log in';
 
    return <button>{logged ? 'Log out' : text}</button>;
}

Так же в атрибуты мы можем помещать переменные. Например, стили мы можем записать отдельно в виде объекта и передавать их внутрь компонента через переменную.

const Field = () => {
    const placeholder = "Type here";
    const styledField = {
        width: "300px",
        height: "50px",
    }
    return <input
			    type="text"
			    placeholder={placeholder}
			    style={styledField}
		    />
}

Так же мы можем записать компоненты в виде классов.

Первый вариант:

import React from 'react';
 
class Field extends React.Component { }

Более короткий вариант через деструктуризацию:

import { Component } from 'react';
 
class Field extends Component { }

В каждом классе компонента должен быть обязательный метод render(), который будет отрисовывать интерфейс.

class Field extends Component {
    render() {
        const placeholder = "Type here";
        const styledField = {
            width: "300px",
            height: "50px",
        }
        return <input
			    type="text"
			    placeholder={placeholder}
			    style={styledField}
		    />
    }
}

Если мы используем функциональные компоненты, то они просто должны иметь директиву return, которая вернёт JSX.Element

export const Field = () => {
	const placeholder = "Type here";
	const styledField = {
		width: "300px",
		height: "50px",
	}
 
	return (
		<input
			type="text"
			placeholder={placeholder}
			style={styledField}
		/>
    );
}

007 Строгий режим

Строгий режим внутри реакта подразумевает под собой проверку на актуальные конструкции, которые мы можем использовать. Обычно он используется для перевода проекта на новую версию реакта. Если какой-то компонент или подход не будет соответствовать нынешней версии реакта или будет нерекоммендуемым к использованию, то реакт нас об этом предупредит. Строгий режим работает только на dev-проекте. На выпущенном в продакшн стрикт уже не сработает, так как подразумевается, что мы всё отработали.

import React, {StrictMode} from 'react'; // !
import ReactDOM from 'react-dom/client';
import './index.css';
import App, {Btn, Field, Header} from './App';
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <StrictMode> {/* ! */}
      <App/>
  </StrictMode>
);

Так же этот режим можно использовать в отдельных компонентах, чтобы проверять только их

function App() {
    return (
        <div className="App">
            <StrictMode>  {/* ! */}
                <Header/>
            </StrictMode>
            <Field/>
            <Btn/>
        </div>
    );
}

009 Создание нового проекта

CRUD — акроним, обозначающий четыре базовые функции, используемые при работе с базами данных: создание (create), чтение (read), модификация (update), удаление (delete). Это стандартная классификация функций по манипуляции данными. В SQL этим функциям, операциям соответствуют операторы Insert (создание записей), Select (чтение записей), Update (редактирование записей), Delete (удаление записей).

Конкретно будет реализовываться проект, который позволит добавлять, удалять, изменять и читать сотрудников и как-то с этими данными взаимодействовать

И в самом начале нам нужно поделить макет на отдельные составные части - компоненты, которые будут представлять собой интерфейс приложения

Первым делом, нужно добавить определённые библиотеки в HTML для стилизации приложения: bootstrap и font-awesome

<!-- Head -->
 
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.1/css/bootstrap.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"/>
 
<title>React App</title>

Для начала определимся со структурой. В папке src храним все исходники проекта. Внутри этой папки нужно хранить два основных файла index js и css. Там же создадим папку components, которая будет хранить все реакт-файлы. Все компоненты делим по папкам, которые именуем в порядке cebab-case. Основной папкой тут будет App и его JS-файл

Сразу нужно сказать, что без разницы, будем мы писать JS или JSX расширение файла - вебпак всё соберёт

Основной файл рендера:

index.js

import React, {StrictMode} from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
 
import App from './components/app/app';
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <StrictMode>
        <App/>
    </StrictMode>
);

Основной файл для компонентов, который отправляется на рендер:

app.js

// Импорты компонентов
import AppInfo from '../app-info/app-info';
import SearchPanel from '../search-panel/search-panel';
import AppFilter from '../app-filter/app-filter';
import EmployeesList from '../employees-list/employees-list';
import EmployeesAddForm from '../employees-add-form/employees-add-form';
// Импорты стилей
import './app.css';
 
// Сборная рендер-функция, которая отображает все компоненты
function App() {
    return (
        <div className="app">
            <AppInfo/>
 
            <div className="search-panel">
                <SearchPanel/>
                <AppFilter/>
            </div>
 
            <EmployeesList/>
 
            <EmployeesAddForm/>
        </div>
    );
}
 
// Экспорт рендер-функции
export default App;

Пример компонента, который хранит в себе другой компонент:

employees-list.js

import EmployeesListItem from '../employees-list-item/employees-list-item';  // импорт другого компонента
import './employees-list.css';  // импорт стилей
 
const EmployeesList = () => {
    return (
        <ul className="app-list list-group">
            <EmployeesListItem/>
            <EmployeesListItem/>
            <EmployeesListItem/>
        </ul>
    );
}
 
// Экспорт компонента
export default EmployeesList;

Стили компонента:

employees-list.css

.app-list {
    margin-top: 30px;
    background-color: #3D5A80;
    border-radius: 4px;
    box-shadow: 15px 15px 15px rgba(0, 0, 0, .15);
    color: #fff;
}

Пример полноценного компонента со стилями:

employees-list-item.js

import './employees-list-item.css';
 
const EmployeesListItem = () => {
    return (
        <li className="list-group-item d-flex justify-content-between">
            <span className="list-group-item-label">
                John Smith
            </span>
            <input type="text" className="list-group-item-input" defaultValue="1000$"/>
            <div className="d-flex justify-content-center align-items-center">
                <button type="button" className="btn-cookie btn-sm">
                    <i className="fas fa-cookie"></i>
                </button>
                <button type="button" className="btn-trash btn-sm">
                    <i className="fas fa-trash"></i>
                </button>
                <i className="fas fa-star"></i>
            </div>
        </li>
    );
}
 
export default EmployeesListItem;

defaultValue - это атрибут инпута, который задаёт начальное значение по умолчанию

010 Свойства компонентов

Свойства компонентов (properties - пропсы) - это объект, который передаётся в компонент из вызова компонента на рендер. Сами значения пропса хранятся в атрибутах компонента. Значения атрибутов неизменяемые - они только на чтение. Кроме как отобразить на странице и прочесть значение - больше ничего не получится. Пропсы используются в компоненте. А менять пропсы в компонентах - нельзя.

app.js

import './App.css';
 
function WhoAmI(props) {  // !
    return (
        <div>
            <h1>My name is {props.name}, surname - {props.surname}</h1>
            <a href={props.link}>My profile</a>
        </div>
    );
}
 
function App() {
    return (
        <div className="App">
            <WhoAmI  // !
                name="John"
                surname="Smith"
                link="google.com"
            />
            <WhoAmI  // !
                name="Angela"
                surname="Coose"
                link="facebook.com"
            />
        </div>
    );
}
 
export default App;

Однако чаще используется деструктуризация пропса

function WhoAmI({name, surname, link}) {
    return (
        <div>
            <h1>My name is {name}, surname - {surname}</h1>
            <a href={link}>My profile</a>
        </div>
    );
}

Ну и так же подобный вариант передачи значения - в виде объекта. Мы можем положить в атрибут объект с определёнными свойствами и эти свойства вызвать в функции

function WhoAmI({name, surname, link}) {
    return (
        <div>
					        // !
            <h1>My name is {name.firstName}, surname - {surname}</h1>
            <a href={link}>My profile</a>
        </div>
    );
}
 
function App() {
    return (
        <div className="App">
            <WhoAmI
                name={{firstName: 'John'}}  // !
                surname="Smith"
                link="google.com"
            />
            <WhoAmI
                name={{firstName: 'Angela'}}
                surname="Coose"
                link="facebook.com"
            />
        </div>
    );
}

Однако нужно быть с этим способом аккуратнее, так как если не указать свойство объекта, то мы получим ошибку

// name === Object - Error
<h1>My name is {name}, surname - {surname}</h1>
//...
<WhoAmI name={{firstName: 'John'}} />

Так же при вызове компонента мы можем вызвать функцию и такой функционал у нас тоже отработает

function WhoAmI({name, surname, link}) {
    return (
        <div>
						     // !
            <h1>My name is {name()}, surname - {surname}</h1>
            <a href={link}>My profile</a>
        </div>
    );
}
 
function App() {
    return (
        <div className="App">
					        // !
            <WhoAmI name={() => { return 'John' }} surname="Smith" link="google.com" />
            <WhoAmI name={() => { return 'Angela' }} surname="Coose" link="facebook.com" />
        </div>
    );
}

011 Практика свойств на проекте

Нам нужно вывести разных пользователей

Первый вариант:

const EmployeesList = () => {
    return (
        <ul className="app-list list-group">
            <EmployeesListItem name="Johnathan" salary={800} />
            <EmployeesListItem name="Cloose" salary={1800} />
            <EmployeesListItem name="Angela" salary={300} />
        </ul>
    );
}
const EmployeesListItem = ({name, salary}) => {
    return (
        <li className="list-group-item d-flex justify-content-between">
            <span className="list-group-item-label">
                {name}
            </span>
            <input type="text" className="list-group-item-input" defaultValue={`${salary}$`}/>
            // ...

Второй вариант: Тут мы уже передаём массив объектов в компонент в качестве пропса и внутри компонента срабатывает логика генерации массива дочерних компонентов, которые представляю из себя вёрстку

function App() {
 
	// Сохраняем наших пользователей в конкретную базу
    const data = [
        {name: "Johnathan", salary: 800},
        {name: "Cloose", salary: 1800},
        {name: "Angela", salary: 300},
    ]
 
    return (
        <div className="app">
            <AppInfo/>
 
            <div className="search-panel">
                <SearchPanel/>
                <AppFilter/>
            </div>
 
			// Передаём базу пользователей в виде пропса
            <EmployeesList data={data}/>
 
            <EmployeesAddForm/>
        </div>
    );
}
import EmployeesListItem from '../employees-list-item/employees-list-item';
import './employees-list.css';
 
const EmployeesList = ({data}) => {
    // Генерируем вёрстку через перебор элементов массива
    const elements = data.map((item) => {
        return (
          <EmployeesListItem name={item.name} salary={item.salary}/>
        );
    });
 
	// Помещаем сюда сгенерированную вёрстку
    return (
        <ul className="app-list list-group">
            {elements}
        </ul>
    );
}
 
export default EmployeesList;

Модифицируем через spread-оператор: Запись через {...item} равна name={item.name} salary={item.salary}

const EmployeesList = ({data}) => {
    const elements = data.map((item) => {
        return (
            <EmployeesListItem {...item} />
        );
    });
    //...

Если нам нужно модифицировать класс: Тут уже нужно реализовать реагирование элемента на повышение зарплаты

function App() {
    const data = [
										    // !
        {name: "Johnathan", salary: 800, increase: false},
        {name: "Cloose", salary: 1800, increase: true},
        {name: "Angela", salary: 300, increase: false},
    ]
const EmployeesListItem = ({name, salary, increase}) => {
 
    let classNames = `list-group-item d-flex justify-content-between`;  // Стоковый класс
 
    if (increase) {  // если повышение = true
        classNames += ' increase';  // то добавляем класс стилей
    }
 
    return (
        <li className={classNames}> // Сюда передаём переменную
        //...

012 Работа со списками и алгоритм согласования

Современные веб-приложения представляют из себя динамически-меняющиеся документы, которые представляются в вебе. То есть мы можем динамически менять элементы на странице в зависимости от действий пользователя.

При изменении структуры документа в реакте, начинает работать алгоритм реконцеляции или Reconciliation. Он представляет из себя полное перерендеривание элемента на странице, что сильно упрощает работу с изменением элементов и их обновлением. Этот алгоритм работает рекурсивно и обновляет как сам элемент, так и все вложенные внутри него. Реакт сохраняет прошлую версию дерева и новую версию дерева (всё находится внутри Virtual DOM). Производит сравнение внутри себя каждого элемента друг с другом и если находит отличия, то производит перерисовку элемента в реальном ДОМ-дереве. То есть перерисовка происходит ровно у тех элементов, которые изменились - остальные остаются нетронутыми.

Первый вариант:

return (
    <ul className="app-list list-group">
        {elements}
    </ul>
);

Пример изменения, при котором произойдёт перерендер:

return (
        <div className="app-list list-group">
            {elements}
        </div>
    );
}

Однако из такого подхода вытекает следующий минус: Если мы можем в конец этого же массива добавить новый элемент и реакт его просто дорисует, то если добавить новую строчку в начало массива, то реакт перерисует всё, так как порядок нумерации элементов в его дереве был сбит

Стоковый массив работников:

const data = [
   { name: "Johnathan", salary: 800, increase: false },
   { name: "Cloose", salary: 1800, increase: true },
   { name: "Angela", salary: 300, increase: false },
];

Без полного перерендера:

const data = [
   { name: "Johnathan", salary: 800, increase: false },
   { name: "Cloose", salary: 1800, increase: true },
   { name: "Angela", salary: 300, increase: false },
   // новый тут
   { name: "Valentine", salary: 500, increase: false },
];

С полным перерендером:

const data = [
   // новый тут
   { name: "Valentine", salary: 500, increase: false },
   { name: "Johnathan", salary: 800, increase: false },
   { name: "Cloose", salary: 1800, increase: true },
   { name: "Angela", salary: 300, increase: false },
];

И чтобы реакт не перерендеривал одинаковые элементы постоянно, нужно добавить в качестве атрибута тегу уникальный key

// employers-list-item.js
//...
const EmployeesList = ({data}) => {
 
	//--- Этот блок нужно модифицировать
    const elements = data.map((item) => {
        return (
            <EmployeesListItem {...item} />
        );
    });
    //---
 
    return (
        <ul className="app-list list-group">
            {elements}
        </ul>
    );
}
//...

Добавляем в объект деструктуризацию и передаём не весь item, а все его свойства по отдельности и id (который поместим в атрибут key):

const elements = data.map((item) => {
    const {id, ...itemProps} = item;
    return (
        <EmployeesListItem key={id} {...itemProps} />
    );
});

Либо подойдёт такой вариант:

const elements = data.map((item, i) => {
    const {...itemProps} = item;
    return (
        <EmployeesListItem key={i} {...itemProps} />
    );
});

И теперь в девтулзе мы можем увидеть атрибут key, который будет помечать для реакта повторяющийся компонент и не давать ему его перерендеривать, если тот не изменён

Важный момент: мы должны понимать, что порядок элементов у нас не поменяется, а не иначе смысла в этих атрибутах практического не будет

Особенности быстрой работы реакта:

  • Реакт обновляет только те элементы интерфейса, которые действительно изменились
  • В этом ему помогает алгоритм согласования, который сравнивает старую и новую копию ДОМ-дерева
  • При работе со списком одинаковых сущностей лучше использовать атрибут key, чтобы реакт не перерендеривал страницу и работал ещё быстрее

013 Состояния компонентов

Перепишем немного приложение из первых уроков под классы с использованием компонентов:

// Импортим сам компонент
import { Component } from 'react';
import './App.css';
 
// Экстендим класс от компонента
class WhoAmI extends Component {
 
	// Так же конструктор можно тут удалить, если в нём кроме super() ничего не вызывается
    constructor(props) {
        super(props);
    }
 
    render() {
        const {name, surname, link} = this.props;
        return (
            <div>
                <h1>My name is {name}, surname - {surname}</h1>
                <a href={link}>My profile</a>
            </div>
        );
    };
}
 
function App() {
    return (
        <div className="App">
            <WhoAmI name='John' surname="Smith" link="google.com" />
            <WhoAmI name='Angela' surname="Coose" link="facebook.com" />
        </div>
    );
}
 
export default App;

И конкретно в React мы можем прописать переменную, которая будет хранить в себе значения определённых состояний. Состояния - это динамически изменяемые объекты на странице. Мы не можем их изменять напрямую, но можем попробовать поменять особым образом

class WhoAmI extends Component {
    constructor(props) {
        super(props);
        this.state = {
            years: 27,
        }
    }
 
    render() {
        const {name, surname, link} = this.props;
        return (
            <div>
                <h1>My name is {name}, surname - {surname}, age - {this.state.years}</h1>
                <a href={link}>My profile</a>
            </div>
        );
    };
}

И в этом примере в метод рендера мы закинули кнопку, которая будет вызвать метод для смены состояния и написали сам метод для смены состояния.

Для смены состояний обязательно нужно использовать стрелочные функции, чтобы наследовать контекст.

Сам объект мы не мутируем - нужно передавать внутри setState() новый объект с нужным значением.

setState() - при запуске активирует перерисовку всего компонента с новым состоянием

class WhoAmI extends Component {
    constructor(props) {
        super(props);
        this.state = {
            years: 27,
        }
    }
 
	// -- Метод смены состояний
    // Обязательно нужно тут использовать стрелочную функцию
    nextYear = () => {
        console.log('+++');
        // Правильная смена состояния
        this.setState({
            // Делать через инкремент ("++") не стоит, так как это мутирует состояние объекта
            years: this.state.years + 1,
        });
    }
 
    render() {
        const {name, surname, link} = this.props;
        return (
            <div>
	            {// А тут добавили кнопку, которая вызовет метод}
                <button onClick={this.nextYear}>+++</button>
                <h1>My name is {name}, surname - {surname}, age - {this.state.years}</h1>
                <a href={link}>My profile</a>
            </div>
        );
    };
}

Особенности работы хуков:

  • Функция setState асинхронна, поэтому когда мы её очень быстро выполняем, она может не успеть поменять состояние объекта и мы можем пропустить изменения.
  • В React есть механизмы для объединения сразу нескольких изменений состояний в одно изменение состояния.
  • Чтобы избежать проблем с асинхронным изменением состояния, нужно использовать для изменения объекта колбэк-функцию

Тут уже нужно отметить несколько моментов.

  1. Сейчас в метод setState мы передаём коллбэк-функцию, которая заставит Реакт сначала выполнить текущее изменение состояния, а уже только потом изменять новое состояние.
  2. Сам объект {}, который передаётся через коллбэк оборачивается в скобки, чтобы не вызвать внутри return, поэтому получается такая конструкция: state => ({})
  3. setState меняет состояние только тех свойств объекта state, которые мы в него передали. То есть свойство text (которое мы отображаем в качестве текста кнопки) меняться не будет и перезагружаться на странице тоже, что снижает потребление ресурсов
constructor(props) {
    super(props);
    this.state = {
        years: 27,
        text: '+++',
    }
}
 
nextYear = () => {
    console.log('+++');
    this.setState((state) => ({
        years: state.years + 1,
    }));
}
 
render() {
    const {name, surname, link} = this.props;
    return (
        <div>
            <button onClick={this.nextYear}>{this.state.text}</button>
            <h1>My name is {name}, surname - {surname}, age - {this.state.years}</h1>
            <a href={link}>My profile</a>
        </div>
    );
};

Итог:

  • У компонента может быть своё внутреннее состояние, которое динамически меняется
  • Состояние может быть как у классовых, так и у функциональных компонентов
  • Состояние напрямую менять нельзя - только через команду setState
  • setState и какое-либо изменение состояния - это асинхронная операция и если мы хотим сохранить точность и последовательность данных, нужно передавать коллбек-функцию
  • В команде setState мы можем менять только те свойства объекта, которые нам нужны - остальные останутся без изменений

014 Самостоятельное задание на работу с состояниями

Задание:

  1. Начальное значение счетчика должно передаваться через props
  2. INC и DEC увеличивают и уменьшают счетчик соответственно на 1. Без ограничений, но можете добавить границу в -50/50. По достижению границы ничего не происходит
  3. RND изменяет счетчик в случайное значение от -50 до 50. Конструкцию можете прогуглить за 20 секунд :) Не зависит от предыдущего состояния
  4. RESET сбрасывает счетчик в 0 или в начальное значение из пропсов. Выберите один из вариантов

Начальный код:

<div id="app">
</div>
* {
  box-sizing: border-box;
}
html, body {
  height: 100%;
}
 
body {
  background: rgb(131,58,180);
background: linear-gradient(90deg, rgba(131,58,180,1) 0%, rgba(253,29,29,1) 50%, rgba(252,176,69,1) 100%);
}
 
.app {
  width: 350px;
  height: 250px;
  background-color: #fff;
  margin: 50px auto 0 auto;
  padding: 40px;
  border-radius: 5px;
  box-shadow: 5px 5px 10px rgba(0,0,0, .2);
}
 
.counter {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  box-shadow: 5px 5px 10px rgba(0,0,0, .2);
  background-color: #e6e6e6;
  text-align: center;
  line-height: 100px;
  font-size: 34px;
  margin: 0 auto;
}
 
.controls {
  display: flex;
  justify-content: space-around;
  margin-top: 40px;
}
 
.controls button {
  padding: 7px 12px;
  cursor: pointer;
  background-color: #6B7A8F;
  color: white;
}
class App extends React.Component {
  constructor(props) {
    super(props);
  }
 
  // Используйте только стрелочную форму методов
  // Почему? Подробный ответ будет в следующем уроке
 
  render() {
    return (
      <div class="app">
        <div class="counter">10</div>
        <div class="controls">
          <button>INC</button>
          <button>DEC</button>
          <button>RND</button>
          <button>RESET</button>
        </div>
      </div>
    )
  }
}
 
ReactDOM.render(<App counter={0}/>, document.getElementById('app'));
 

Результат:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 10,
    }
  }
 
  // Используйте только стрелочную форму методов
  // Почему? Подробный ответ будет в следующем уроке
 
  increment = () => {
    this.setState(state => ({
      counter: state.counter + 1,
    }));
  }
 
  decrement = () => {
    this.setState(state => ({
      counter: state.counter - 1,
    }));
  }
 
  randomize = () => {
    this.setState(state => ({
      counter: Math.floor(Math.random() * (50 - (-50)) + (-50)),
    }));
  }
 
  reset = () => {
    this.setState(state => ({
      counter: 10,
    }));
  }
 
  render() {
    return (
      <div class="app">
        <div class="counter">{this.state.counter}</div>
        <div class="controls">
          <button onClick = {this.increment}>INC</button>
          <button onClick = {this.decrement}>DEC</button>
          <button onClick = {this.randomize}>RND</button>
          <button onClick = {this.reset}>RESET</button>
        </div>
      </div>
    )
  }
}
 
ReactDOM.render(<App counter={0}/>, document.getElementById('app'));

All is works!@!!

015 События в React и вспоминаем this

Сейчас нужно поговорить о том, почему мы используем коллбэк-функции вместо обычных, когда нам нужно работать в реакте.

Причина крайне проста: методы, которые мы вызываем в другом методе, при использовании обычных функций, не сохраняют контекст вызова. Коллбэк-функция nextYear сохраняет контекст вызова и обращается к классу, в котором находится метод render. Обычная функция не сохраняет контекст и this внутри неё будет = undefined

Решить проблему мы можем не только коллбэк-функцией, но и другими способами:

Первый способ: привязка контекста через bind

constructor(props) {
    super(props);
    this.state = {
        years: 27,
        text: '+++',
        position: '',
    }
    // Функция привязки контекста вызова
    this.nextYear = this.nextYear.bind(this);
}
 
nextYear() {
    this.setState((state) => ({
        years: state.years + 1,
    }));
}

Второй способ: непосредственное использование классов с вложенными в них коллбэк-функциями

nextYear = () => {
    console.log('+++');
    this.setState((state) => ({
        years: state.years + 1,
    }));
}

Третий способ: использование анонимной стрелочной функции. Тут мы вызываем функцию nextYear внутри стрелочной функции, которая позволяет сохранить контекст вызова. Проблемным этот способ может быть, если мы соберёмся передавать такой вызов в качестве пропсов в другие вложенные компоненты

<button onClick={() => this.nextYear()}>{text}</button>

Если нам нужно передать в функцию значения, то мы должны воспользоваться анонимной стрелочной функцией (e) => this.commitInputChanges(e, 'red'), которая и позволит передать значения.

Если нам нужно задать триггер функции для тега input стоит пользоваться атрибутом onChange

// Функция, которая принимает в себя значения
commitInputChanges = (e, color) => {
    console.log(color);
    this.setState(state => ({
        position: e.target.value,
    }));
}
 
render() {
    const {name, surname, link} = this.props;
    const {text, years, position} = this.state;
    return (
        <div>
            <button onClick={() => this.nextYear()}>{text}</button>
            <h1>My name is {name}, surname - {surname}, age - {years} <br/> profession - {position}</h1>
            <a href={link}>My profile</a>
 
			//Форма с данными
 
            <form>
                <label htmlFor={`profession${name}`}>Введите вашу профессию</label>
                <input type="text" id={`profession${name}`} onChange={(e) => this.commitInputChanges(e, 'red')}/>
            </form>
        </div>
    );
};

Так же нужно отметить, что event передаётся в функцию по умолчанию и нам не нужно передавать его отдельно, поэтому для вызова функции для нас доступна такая запись: this.commitInputChanges

// Это метод
commitInputChanges = (e) => {
    this.setState(state => ({
        position: e.target.value,
    }));
}
 
// Это в рендере
<input type="text" id={`profession${name}`} onChange={this.commitInputChanges}/>

016 Практика состояний на проекте

Конкретно сейчас нам нужно реализовать добавление звёздочек на работников, которым назначена премия

class EmployeesListItem extends Component {
    constructor(props) {
        super(props);
        this.state = {
            increase: false,
            liked: false
        }
    }
 
    onIncrease = () => {
        // Получаем state.increase
        this.setState(({increase}) => ({
            // Возвращает свойство противоположное тому, что было до этого
            increase: !increase,
        }))
    }
 
	// Так же добавляем булеан проверку состояния и для установки нового класса
    setLike = () => {
        this.setState(({liked}) => ({
            liked: !liked,
        }))
    }
 
    render() {
        const {name, salary} = this.props;
 
        // получаем деструктурированные значения увеличения и лайка из стейта
        const {increase, liked} = this.state;
 
		// стандартные стили для объекта списка сотрудника
        let classNames = `list-group-item d-flex justify-content-between`;
 
		// функция добавления класса премии сотруднику
        if (increase) {
            classNames += ' increase';
        }
 
		// Тут добавляем класс лайка на пользователя
		if (liked) {
            classNames += ' like';
        }
 
        return (
            <li className={classNames}>
            <span
                className="list-group-item-label"
                onClick={this.setLike}>
                {name}
            </span>
                <input type="text" className="list-group-item-input" defaultValue={`${salary}$`}/>
                <div className="d-flex justify-content-center align-items-center">
                    <button type="button"
                            className="btn-cookie btn-sm"
                            onClick={this.onIncrease}>
                        <i className="fas fa-cookie"></i>
                    </button>
 
                    <button type="button" className="btn-trash btn-sm">
                        <i className="fas fa-trash"></i>
                    </button>
                    <i className="fas fa-star"></i>
                </div>
            </li>
        );
    }
}
 
export default EmployeesListItem;

Тут полетели стили, но всё работает именно так, как должно было

017 Работа с формами, управляемые и неуправляемые компоненты

Конкретно тут мы сделали запись значения из инпута в стейт кмопонента, тем самым сделав его управляемым.

import { Component } from 'react';
import './emploees-add-form.css';
 
class EmployeesAddForm extends Component {
    constructor(props) {
        super(props);
        this.state = {
            name: '',
            salary: ''
        }
    }
 
	// Этот метод изменяет состояние и записывает его в state
    onValueChange = (e) => {
        this.setState({
            [e.target.name]: e.target.value,
        })
    }
 
    render() {
        const {name, salary} = this.state;
 
        return (
            <div className="app-add-form">
                <h3>Добавьте нового сотрудника</h3>
                <form className="add-form d-flex">
                    <input type="text"
                           className="form-control new-post-label"
                           placeholder="Имя"
                           //--
                           name="name"  // Имя элемента
                           value={name}  // Значение элемента
                           //--
                           onChange={this.onValueChange}
                    />
                    <input type="number"
                           className="form-control new-post-label"
                           placeholder="ЗП $"
                           //--
                           name="salary"  // Имя элемента
                           value={salary} // Значение элемента
                           //--
                           onChange={this.onValueChange}
                    />
                    <button className="btn btn-outline-light" type="submit">Добавить</button>
                </form>
            </div>
        );
    }
}
 
export default EmployeesAddForm;

Если метод связан с определёнными действиями пользователя, то наименование стоит начинать с on (например onValueChange - см. пример выше в коде)

Конкретно, если в атрибуте стоит value={определённое_значение}, то мы всегда будем иметь актуальный стейт. То есть значение value формы инпута будет контролироваться реактом. Сам такой элемент будет называться управляемым компонентом/элементом.

Главным преймуществом такого подхода заключается в том, что стейт синхронизирован с интерфейсом, что позволяет интерфейсу мгновенно реагировать на все изменения

<input type="text"
	   className="form-control new-post-label"
	   placeholder="Имя"
	   name="name"
	   value={name}
	   onChange={this.onValueChange}
/>

У неуправляемых компонентов/элементов значения полей хранятся прямо в DOM-дереве Неуправляемые компоненты менее предпочтительны, так как их функционал куда беднее, чем у управляемых. Единственный компонент, который всегда будет неуправляемым - это инпут typefile, когда пользователь должен загрузить в него какой-то файл.

018 Иммутабельность состояния и собственные события

Что такое иммутабельность?

Иммутабельность - это свойство объекта, при котором его нельзя изменить - как он был создан, таким и остаётся.

  • Такой подход сохраняется и в React, потому как обычно все наши объекты неизменны. Чтобы поменять отображение объекта на странице, нужно создать его копию и изменить уже её.
  • Такой подход очень сильно облегчает работу программиста и он обеспечивает работу reconciliation-алгоритма
  • Однако немного падает производительность за счёт того, что нужно сохранять прошлое состояние объекта

деструктуризация Тут нужно сказать, что при таком способе записи {...old} все последующие свойства, которые будут после деструктуризации, будут перезатираться. Запись в блоке кода говорит, что свойство increase будет иметь обратное значение тому, что передали через old

// формируем массив из значения по индексу
const old = data[index];
// формируем объект из массива
const newItem = {...old, increase: !old.increase};

findIndex Так же метод findIndex позволяет нам найти одно определённое значение из массива по нужному нам параметру. Этот метод принимает коллбэк-функцию с условием внутри

onToggleIncrease = (id) => {
   this.setState(({data}) => {
      // Ищем значение массива по id
      const index = data.findIndex(elem => elem.id === id);
   })
}

Нам нужно реализовать метод для удаления сотрудников из списка сотрудников. Чтобы удалять сотрудников, нам нужно удалять их физически: из данных (например, в БД).

В первую очередь нам нужно создать сам метод для удаления:

function App() {
   const data = [
      { name: 'Johnathan', salary: 800, increase: false, id: 1 },
      { name: "Cloose", salary: 1800, increase: true, id: 2 },
      { name: "Angela", salary: 300, increase: false, id: 3 },
   ];
 
   return (
      <div className="app">
         <AppInfo />
 
         <div className="search-panel">
            <SearchPanel />
            <AppFilter />
         </div>
 
         <EmployeesList
            data={data}
            // Сам метод удаления будет представлять из себя функцию, которая будет возвращать нам id пользователя в консоль (определяем элемент для удаления)
            onDelete={id => console.log(id)}
         />
         <EmployeesAddForm />
      </div>
   );
}

У самого работника нам нужно поместить срабатывание метода onDeleted, который будет вызывать его при клике

render() {
	// Добавляем сюда метод onDeleted
    const {name, salary, onDeleted} = this.props;
    const {increase, liked} = this.state;
 
    let classNames = `list-group-item d-flex justify-content-between`;
 
    if (increase) {
        classNames += ' increase';
    }
    if (liked) {
        classNames += ' like';
    }
    return (
        <li className={classNames}>
        <span
            className="list-group-item-label"
            onClick={this.setLike}>
            {name}
        </span>
            <input type="text" className="list-group-item-input" defaultValue={`${salary}$`}/>
            <div className="d-flex justify-content-center align-items-center">
                <button type="button"
                        className="btn-cookie btn-sm"
                        onClick={this.onIncrease}>
                    <i className="fas fa-cookie"></i>
                </button>
 
                <button type="button"
                        className="btn-trash btn-sm"
                        // И вызваем onDeleted при клике на этот значок
                        onClick={onDeleted}
                >
                <i className="fas fa-trash"></i>
                </button>
                <i className="fas fa-star"></i>
            </div>
        </li>
    );
}

И в самом списке работников мы завершаем функционал идентификации определённого элемента списка

const EmployeesList = ({data, onDelete}) => {
    const elements = data.map((item) => {
        const {id, ...itemProps} = item;
        return (
            <EmployeesListItem
                key={id} {...itemProps}
                // Сюда в Delete мы уже спокойно передаём id нашего элемента, чтобы обращаться к нему
                onDeleted={() => onDelete(id)}
            />
        );
    });
 
    return (
        <ul className="app-list list-group">
            {elements}
        </ul>
    );
}

И теперь при клике на корзинку, мы можем увидеть индекс работника в списке

Дальше нам нужно реализовать непосредственно работу с данными. Самый правильный способ работы с данными - это менять их через стейт.

class App extends Component {
   constructor(props) {
      super(props);
      this.state = {
         data: [
            {name: 'Johnathan', salary: 800, increase: false, id: 1},
            {name: "Cloose", salary: 1800, increase: true, id: 2},
            {name: "Angela", salary: 300, increase: false, id: 3},
         ],
      }   };
 
   deleteItem = (id) => {
      this.setState(({data}) => {
         const index = data.findIndex((elem) => {
            return elem.id === id;
         });
 
         console.log(index);
 
         return index;
      });
   }
 
   render() {
      return (
         <div className="app">
            <AppInfo />
 
            <div className="search-panel">
               <SearchPanel />
               <AppFilter />
            </div>
 
            <EmployeesList
               data={this.state.data}
               onDelete={this.deleteItem}
            />
            <EmployeesAddForm />
         </div>
      );
   }
}

Далее нам нужно реализовать само удаление элемента со страницы. Сразу нужно повториться: менять напрямую стейт нельзя

Это пример неправильной реализации, так как тут меняется стейт напрямую

deleteItem = (id) => {
   this.setState(({data}) => {
      const index = data.findIndex(elem => elem.id === id);
 
	  // Напрямую изменять стейт вот так - нельзя!
      data.splice(index, 1);
 
      return {
         data: data
      };
   });
}

Уже подход представленный ниже является верным, так как тут не модифицируется наш стейт, а создаётся новый без удаляемого элемента

deleteItem = (id) => {
   this.setState(({data}) => {
      const index = data.findIndex(elem => elem.id === id);
 
      // Получаем все элементы от начала массива до того, который получать нам не нужно
      const before = data.slice(0, index);
      // Получаем все элементы массива после искомого
      const after = data.slice(index + 1);
      // Объединяем оба массива в один (теперь тут нет удалённого элемента)
      const newArr = [...before, ...after];
 
      return {
         data: newArr
      };
   });
}

Либо можно воспользоваться более коротким вариантом: Мы сформируем новый массив, в котором будут все элементы, идентификатор которых не равен тому, что передали аргументом в функцию

deleteItem = (id) => {
   this.setState(({data}) => {
      return {
         data: data.filter(elem => elem.id !== id)
      };
   });
}

Практика. Добавление нового сотрудника.

App.js

constructor(props) {
   super(props);
   this.state = {
      data: [
	    {name: 'Johnathan', salary: 800, increase: false, liked: true, id: 1},
         {name: "Cloose", salary: 1800, increase: true, liked: false, id: 2},
         {name: "Angela", salary: 300, increase: false, liked: false, id: 3},
      ],
      // Добавляем новое свойство в стейт, у которого будет последнее максимальное значение
      maxId: 4
   }
};
 
// Это метод добавления нового пользователя
addItem = (name, salary) => {
   // Создаём объект со значениями нового пользователя
   const newItem = {
      name: name,
      salary: salary,
      increase: false,
      id: this.maxId++
   }
   // Тут мы меняем стейт и в новый массив добавляем нового пользователя
   this.setState(({data}) => {
      const newArr = [...data, newItem];
      return {
         data: newArr
      }
   });
}
 
render() {
   return (
      <div className="app">
         <AppInfo />
 
         <div className="search-panel">
            <SearchPanel />
            <AppFilter />
         </div>
 
         <EmployeesList
            // Это и есть пропсы
            data={this.state.data}
            onDelete={this.deleteItem}
            // А это те методы, которые передадутся в качестве props
            onToggleIncrease={this.onToggleIncrease}
            onToggleRise={this.onToggleRise}
         />
 
         <EmployeesAddForm
	         // Тут уже в качестве пропсов передаём метод из App.js в EmployeesAddForm
            onAdd={this.addItem}
         />
      </div>
   );

employees-add-form.js

class EmployeesAddForm extends Component {
    constructor(props) {
        super(props);
        this.state = {
            name: '',
            salary: ''
        }
    }
 
    onValueChange = (e) => {
        this.setState({
            [e.target.name]: e.target.value,
        })
    }
 
	// ! Это сам метод подтверждения добавления нового пользователя
    onSubmit = (e) => {
	    // Всегда сбрасываем стандартное поведение браузера при ивентах
        e.preventDefault();
 
		// если длина имени меньше 3 или нет зарплаты, то отменяем функцию
        if (this.state.name.length < 3 || !this.state.salary) return;
 
		//
        this.props.onAdd(this.state.name, this.state.salary);
 
		// устанавливаем состояние
        this.setState({
            name: '',
            salary: ''
        })
    }
 
    render() {
        const {name, salary} = this.state;
 
        return (
            <div className="app-add-form">
                <h3>Добавьте нового сотрудника</h3>
                <form className="add-form d-flex"
	// Запускаем срабатывание метода при нажатии на кнопку, которая имеет тип "submit"
                      onSubmit = {this.onSubmit}
                >
                    <input type="text"
                           className="form-control new-post-label"
                           placeholder="Имя"
                           name="name"
                           value={name}
                           onChange={this.onValueChange}
                    />
                    <input type="number"
                           className="form-control new-post-label"
                           placeholder="ЗП $"
                           name="salary"
                           value={salary}
                           onChange={this.onValueChange}
                    />
                    <button
                        className="btn btn-outline-light"
                    // добавляем кнопке тип "submit", чтобы реагировала форма при нажатии
                        type="submit"
                    >Добавить</button>
                </form>
            </div>
        );
    }
}

Для чуть большего ознакомления с иммутабельными объектами, стоит ознакомиться с этой статьёй

019 Практика. Подъём состояния

Подъём состояния (statelifting) - это поднятие внутреннего состояния одного компонента выше по иерархии

  • В нынешнем варианте, компонент App.js является источником истины, так как данные находятся в нём
  • Так же данные могут храниться в отдельной сущности или располагаться внутри компонентов

Покажем пример проброса методов вниз:

Создаём методы onToggleIncrease и onToggleRise, которые в качестве пропсов передадим внутрь EmployeesList, откуда мы и сможем получить к ним доступ

App.js

class App extends Component {
   constructor(props) {
      super(props);
      this.state = {
         data: [
            {name: 'Johnathan', salary: 800, increase: false, id: 1},
            {name: "Cloose", salary: 1800, increase: true, id: 2},
            {name: "Angela", salary: 300, increase: false, id: 3},
         ],
      }   };
 
   // Первый метод, который хотим передать вниз
   onToggleIncrease = (id) => {
      console.log(`Increase this ${id}`);
   }
 
   // Второй метод, который хотим передать вниз
   onToggleRise = (id) => {
      console.log(`Rise this ${id}`);
   }
 
   deleteItem = (id) => {
      this.setState(({data}) => {
         return { data: data.filter(elem => elem.id !== id) };
      });
   }
 
   render() {
      return (
         <div className="app">
            <AppInfo />
 
            <div className="search-panel">
               <SearchPanel />
               <AppFilter />
            </div>
 
            <EmployeesList
               // Это и есть пропсы
               data={this.state.data}
               onDelete={this.deleteItem}
               // А это те методы, которые передадутся в качестве props
               onToggleIncrease={this.onToggleIncrease}
               onToggleRise={this.onToggleRise}
            />
            <EmployeesAddForm />
         </div>
      );
   }
}

EmployeesList

// Передаём в параметры функции пропсы, которые мы передали в тег <EmployeesList> в App.js
const EmployeesList = ({data, onDelete, onToggleIncrease, onToggleRise}) => {
    const elements = data.map((item) => {
        const {id, ...itemProps} = item;
        return (
            <EmployeesListItem
        // Тут мы передали data, который вложили через data={this.state.data} в App.js
                key={id} {...itemProps}
                // Тут мы запускаем метод для удаления, который вложили в App.js (onDelete={this.deleteItem})
                onDeleted={() => onDelete(id)}
                // Тут запускаем методы, которые вложили так же в App.js
                onToggleIncrease={() => onToggleIncrease(id)}
                onToggleRise={() => onToggleRise(id)}
            />
        );
    });
 
    return (
        <ul className="app-list list-group">
            {elements}
        </ul>
    );
}

employees-list-item - этот компонент переведём в функцию и закинем все данные пропсов из нашего компонента выше сюда в этот компонент

const EmployeesListItem = (props) => {
    // Тут получаем пропсы из EmployeesList, которые являются атрибутами тега
    const {name, salary, onDeleted, onToggleIncrease, onToggleRise, increase, liked} = props;
 
    let classNames = `list-group-item d-flex justify-content-between`;
 
    if (increase) {
        classNames += ' increase';
    }
 
    if (liked) {
        classNames += ' like';
    }
 
    return (
        <li className={classNames}>
        <span
            className="list-group-item-label"
            // Заменяем метод setLike методом, который передали из App.js
            onClick={onToggleRise}>
            {name}
        </span>
            <input type="text" className="list-group-item-input" defaultValue={`${salary}$`}/>
            <div className="d-flex justify-content-center align-items-center">
                <button type="button"
                        className="btn-cookie btn-sm"
                        // Сюда вставляем метод, который вложили в EmployeesList
                        onClick={onToggleIncrease}>
                    <i className="fas fa-cookie"></i>
                </button>
 
                <button type="button"
                        className="btn-trash btn-sm"
                        onClick={onDeleted}
                >
                    <i className="fas fa-trash"></i>
                </button>
                <i className="fas fa-star"></i>
            </div>
        </li>
    );
}

Теперь была восстановлена работа печеньки на повышение З/П Это более сложный и объёмный, но зато более понятный способ создания нового массива

App.js

// Первый метод, который хотим передать вниз
onToggleIncrease = (id) => {
   this.setState(({data}) => {
      // Ищем значение массива по id
      const index = data.findIndex(elem => elem.id === id);
 
      // формируем массив из значения по индексу
      const old = data[index];
      // формируем объект из массива
      const newItem = {...old, increase: !old.increase};
      // Тут уже формируем новый массив с вложенным новым объектом
      const newArr = [...data.slice(0, index), newItem, ...data.slice(index+1)];
 
      return {
         data: newArr
      }
   })
}

А вот второй более простой вариант:

App.js

onToggleIncrease = (id) => {
   this.setState(({data}) => ({
      data: data.map(item => {
         // Если id айтема равен id искомого объекта, то
         if (item.id === id) {
            // ... мы возвращаем новый объект со свойствами, которые было до и инвертированный increase
            return {...item, increase: !item.increase}
         }
         return item;
      })
   }))
}

tips Эти обе записи идентичны и вернут один и тот же результат

// 1
const incresed = this.state.data.filter(item => item.increase === true).length;
 
//2
const incresed = this.state.data.filter(item => item.increase).length;

Дальше нам нужно реализовать блок подсчёта сотрудников. Для этого нам нужно подсчитать количество записей в массиве. Количество сотрудников с премией можно подсчитать через фильтр

App.js

//...
render() {
   // Считаем количество сотрудников
   const employees = this.state.data.length;
   // Количество сотрудников, идущих на повышение
   const incresed = this.state.data.filter(item => item.increase === true).length;
 
   return (
      <div className="app">
         <AppInfo
	         // Передаём props в AppInfo
			employees={employees}
            incresed={incresed}
         />
	{/* CODE ...*/}

Это уже сам компонент с общей информацией по сотрудникам (шапка)

app-info.js

// Принимаем props переданные в App.js
const AppInfo = ({employees, increased}) => {
    return (
        <div className="app-info">
            <h1> Учёт сотрудников в компании</h1>
            // Вставляем значения
            <h2>Общее число сотрудников: {employees}</h2>
            <h2>Премию получат: {increased}</h2>
        </div>
    );
}

Второй метод onToggleRise работает почти идентично тому, что был чуть выше. Нам стоит заняться оптимизацией этих методов, так как внутри них отличается только 2 слова.

App.js

// Первый метод, который хотим передать вниз
onToggleIncrease = (id) => {
   this.setState(({data}) => ({
      data: data.map(item => {
         // Если id айтема равен id искомого объекта, то
         if (item.id === id) {
            // ... мы возвращаем новый объект со свойствами, которые было до и инвертированный increase
            return {...item, increase: !item.increase}
         }
         return item;
      })
   }))
}
 
// Второй метод, который хотим передать вниз
onToggleRise = (id) => {
   this.setState(({data}) => ({
      data: data.map(item => {
         if (item.id === id) {
            return {...item, liked: !item.liked}
         }
         return item;
      })
   }))
}

Объединяем метод в один, используя подстановку через props и передаём в EmployeesList этот метод

App.js

onToggleProp = (id, prop) => {
   this.setState(({data}) => ({
      data: data.map(item => {
         // Если id айтема равен id искомого объекта, то
         if (item.id === id) {
            // ... мы возвращаем новый объект со свойствами, которые было до и инвертированный increase
            return {...item, [prop]: !item[prop]}
         }
         return item;
      })
   }))
}
 
// Внутри render
<EmployeesList
   // Это и есть пропсы
   data={this.state.data}
   onDelete={this.deleteItem}
   // это объединённый метод переключения нашего списка
   onToggleProp={this.onToggleProp}
/>

Дальше в списке сотрудников нам нужно дата-атрибуты, которые в реакте записываются через data-toggle

employees-list-item

const EmployeesListItem = (props) => {
    // Меняем на onToggleProp
    const {name, salary, onDeleted, onToggleProp, increase, liked} = props;
 
    let classNames = `list-group-item d-flex justify-content-between`;
 
    if (increase) {
        classNames += ' increase';
    }
 
    if (liked) {
        classNames += ' like';
    }
 
    return (
        <li className={classNames}>
        <span
            className="list-group-item-label"
            // Меняем метод в в onClick
            onClick={onToggleProp}
            // Этот атрибут должен будет попасть вторым аргументом в onToggleProp
            data-toggle="liked"
        >
	        {name}
        </span>
            <input type="text" className="list-group-item-input" defaultValue={`${salary}$`}/>
            <div className="d-flex justify-content-center align-items-center">
                <button type="button"
                        className="btn-cookie btn-sm"
                        // Меняем методв в onClick
                        onClick={onToggleProp}
                        // И этот атрибут должен будет попасть вторым аргументом в onToggleProp
                        data-toggle="increase"
                >
 
			{/* CODE ...*/}

Тут уже нам нужно передать как сам метод, так и атрибут. Атрибут можно передать через ивент, передав в него от текущего таргета атрибут с именем 'data-toggle'

employees-list-item.js

//...
const EmployeesList = ({data, onDelete, onToggleProp}) => {
    const elements = data.map((item) => {
        const {id, ...itemProps} = item;
        return (
            <EmployeesListItem
                key={id} {...itemProps}
                onDeleted={() => onDelete(id)}
                // Тут
                onToggleProp={(e) => onToggleProp(id, e.currentTarget.getAttribute('data-toggle'))}
            />
        );
    });
//...

020 React-фрагменты

Очень часто при работе с вёрсткой нам может понадобиться удалить лишний div, в который по правилам JSX мы должны обернуть все элементы возвращаемые из return. Помешать может лишний див, когда нам нужно использовать компонент при вёрстке как flex-box компонентов, так и grid

Для исправления этой ситуации используют React-фрагменты

Первый способ создания и использования фрагмента подразумевает под собой его импорт и использование в качестве тега:

// 1 - импортируем Fragment
import { Component, Fragment } from 'react';
 
//2 - оборачиваем во Fragment
render() {
    const {name, surname, link} = this.props;
    const {text, years, position} = this.state;
    return (
	    // И оборачиваем не в <div>, а во <Fragment>
        <Fragment>
            <button onClick={() => this.nextYear()}>{text}</button>
            <h1>My name is {name}, surname - {surname}, age - {years} <br/> profession - {position}</h1>
            <a href={link}>My profile</a>
            <form>
                <label htmlFor={`profession${name}`}>Введите вашу профессию</label>
                <input type="text" id={`profession${name}`} onChange={(e) => this.commitInputChanges(e, 'red')}/>
            </form>
        </Fragment>
    );
};

И теперь можно увидеть, что у нас нет того лишнего дива

Второй способ подразумевает под собой просто использование пустых скобок без импортов фрагмента

render() {
    const {name, surname, link} = this.props;
    const {text, years, position} = this.state;
    return (
	    // Пустые скобки
        <>
            <button onClick={() => this.nextYear()}>{text}</button>
            <h1>My name is {name}, surname - {surname}, age - {years} <br/> profession - {position}</h1>
            <a href={link}>My profile</a>
            <form>
                <label htmlFor={`profession${name}`}>Введите вашу профессию</label>
                <input type="text" id={`profession${name}`} onChange={(e) => this.commitInputChanges(e, 'red')}/>
            </form>
        </>
    );
};

Первый способ используется обычно только тогда, когда нам нужно в обёртку передавать какие-либо атрибуты - например, индекс элемента списка

021 Практика. Реализуем поиск и фильтры

Реализация поиска

app.js

class App extends Component {
   constructor(props) {
      super(props);
      this.state = {
         data: [
            {name: 'Johnathan', salary: 800, increase: false, liked: true, id: 1},
            {name: "Cloose", salary: 1800, increase: true, liked: false, id: 2},
            {name: "Angela", salary: 300, increase: false, liked: false, id: 3},
         ],
         maxId: 4,
         term: '', // Строчка, по которой будет происходить поиск сотрудника
      }
   };
 
   onToggleProp = (id, prop) => {
      this.setState(({data}) => ({
         data: data.map(item => {
            if (item.id === id) {
               return {...item, [prop]: !item[prop]}
            }
            return item;
         })
      }))
   }
 
   deleteItem = (id) => {
      this.setState(({data}) => {
         return { data: data.filter(elem => elem.id !== id) };
      });
   }
 
   addItem = (name, salary) => {
      const newItem = {
         name: name,
         salary: salary,
         increase: false,
         liked: false,
         id: this.maxId++
      }
      this.setState(({data}) => {
         const newArr = [...data, newItem];
         return {
            data: newArr
         }
      });
   }
 
   searchEmployee = (items, term) => {
      // Если ничего не введено в поиск, то показываем все объекты
      if (term.length === 0) return items;
 
      return items.filter(item => {
         // indexOf() - поиск подстроки
         // Возвращаем только те элементы, где присутствует term
         return item.name.indexOf(term) > -1;
      });
   }
 
   onUpdateSearch = (term) => {
      // сокращённая замена записи {term: term}
      this.setState({term});
   }
 
   render() {
 
      // Данные для реализации фильтрации на странице
      const {term, data} = this.state;
      // Сразу передаём только те данные, которые подходят по поиску
      const visibleData = this.searchEmployee(data, term);
 
      // Считаем количество сотрудников
      const employees = this.state.data.length;
      // Количество сотрудников, идущих на повышение
      const increased = this.state.data.filter(item => item.increase === true).length;
 
      return (
         <div className="app">
            <AppInfo
               employees={employees}
               increased={increased}
            />
 
            <div className="search-panel">
               <SearchPanel onUpdateSearch={this.onUpdateSearch}/>
               <AppFilter />
            </div>
 
            <EmployeesList
               // Сюда передаём отфильтрованные данные
               data={visibleData}
               onDelete={this.deleteItem}
               onToggleProp={this.onToggleProp}
            />
            <EmployeesAddForm
               onAdd={this.addItem}
            />
         </div>
      );
   }
}

search-panel.js

import {Component} from "react";
import './search-panel.css';
 
class SearchPanel extends Component {
    constructor(props) {
        super(props);
        this.state = {
            term: ''
        }
    }
 
    // Установка локального состояния term
    onUpdateSearch = (e) => {
        const term = e.target.value;
        this.setState({term});
        this.props.onUpdateSearch(term);
    }
 
    render() {
        return (
            <input type="text"
                   className="form-control search-input"
                   placeholder="Найти сотрудника"
                   // передаём значение и при изменении вызываем функцию
                   value={this.state.term}
                   onChange={this.onUpdateSearch}
            />
        );
    }
}
 
export default SearchPanel;

Реализация фильтров на странице

App.js

import {Component} from "react";
 
import AppInfo from "../app-info/app-info";
import SearchPanel from "../search-panel/search-panel";
import AppFilter from "../app-filter/app-filter";
import EmployeesList from "../employees-list/employees-list";
import EmployeesAddForm from "../employees-add-form/employees-add-form";
 
import "./app.css";
 
class App extends Component {
   constructor(props) {
      super(props);
      this.state = {
         data: [
            {name: 'Johnathan', salary: 800, increase: false, liked: true, id: 1},
            {name: "Cloose", salary: 1800, increase: true, liked: false, id: 2},
            {name: "Angela", salary: 300, increase: false, liked: false, id: 3},
         ],
         maxId: 4,
         term: '', // Строчка, по которой будет происходить поиск сотрудника
         filter: 'all' // Сюда запишем выбранный фильтр
      }
   };
 
   // Объединённый метод, который хотим передать вниз
   onToggleProp = (id, prop) => {
      this.setState(({data}) => ({
         data: data.map(item => {
            // Если id айтема равен id искомого объекта, то
            if (item.id === id) {
               // ... мы возвращаем новый объект со свойствами, которые было до и инвертированный increase
               return {...item, [prop]: !item[prop]}
            }
            return item;
         })
      }))
   }
 
   deleteItem = (id) => {
      this.setState(({data}) => {
         return { data: data.filter(elem => elem.id !== id) };
      });
   }
 
   addItem = (name, salary) => {
      const newItem = {
         name: name,
         salary: salary,
         increase: false,
         liked: false,
         id: this.maxId++
      }
      this.setState(({data}) => {
         const newArr = [...data, newItem];
         return {
            data: newArr
         }
      });
   }
 
   searchEmployee = (items, term) => {
      // Если ничего не введено в поиск, то показываем все объекты
      if (term.length === 0) return items;
 
      return items.filter(item => {
         // indexOf() - поиск подстроки
         // Возвращаем только те элементы, где присутствует term
         return item.name.indexOf(term) > -1;
      });
   }
 
   // Метод, который будет регистрировать изменения стейта данных
   onUpdateSearch = (term) => {
      // {term: term}
      this.setState({term});
   }
 
   // Выбор фильтра
   filterPost = (items, filter) => {
      switch (filter) {
         case 'onIncrease':
            return items.filter(item => item.liked);
         case 'moreSalary':
            return items.filter(item => item.salary > 1000);
         default:
            return items;
	    }
	}
 
   onFilterSelect = (filter) => {
      // {filter: filter}
      this.setState({filter});
   }
 
   render() {
      // Данные для реализации фильтрации на странице
      const {term, data, filter} = this.state;
      // Сразу передаём только те данные, которые подходят по поиску
      const visibleData = this.filterPost(this.searchEmployee(data, term), filter);
 
      // Считаем количество сотрудников
      const employees = this.state.data.length;
      // Количество сотрудников, идущих на повышение
      const increased = this.state.data.filter(item => item.increase === true).length;
 
      return (
         <div className="app">
            <AppInfo
               employees={employees}
               increased={increased}
            />
 
            <div className="search-panel">
               <SearchPanel onUpdateSearch={this.onUpdateSearch}/>
               {/*Чтобы сделать анимацию кнопки, нужно передать фильтр*/}
               <AppFilter
                  onFilterSelect={this.onFilterSelect}
                  filter={filter}
               />
            </div>
 
            <EmployeesList
               // Это и есть пропсы
               // Сюда передаём отфильтрованные данные
               data={visibleData}
               onDelete={this.deleteItem}
               // А это те методы, которые передадутся в качестве props
               onToggleProp={this.onToggleProp}
            />
            <EmployeesAddForm
               onAdd={this.addItem}
            />
         </div>
      );
   }
}
 
export default App;

app-filter.js

import {Component} from "react";
import './app-filter.css';
 
class AppFilter extends Component {
    render() {
        // Выносим данные о кнопках в отдельный элемент
        const buttonsData = [
            {name: 'all', label: 'Все сотрудники'},
            {name: 'onIncrease', label: 'На повышение'},
            {name: 'moreSalary', label: 'ЗП больше 1000$'},
        ];
 
        const buttons = buttonsData.map(({name, label}) => {
            // ! Класс активности будем назначать только той кнопке, которая отвечает за нужный фильтр
            const active = this.props.filter === name;
            const clazz = active ? 'btn-light' : 'btn-outline-light';
 
            // Сохраняем кнопки в массив
            return (
                <button className={`btn ${clazz}`}
                        type="button"
                        key={name}
                        // Вкладываем функцию, которую передали сюда в качестве пропсов
                        onClick={() => this.props.onFilterSelect(name)}
                >
                    {label}
                </button>
            );
        })
 
        return (
            <div className="btn-group">
                {/*И отображаем массив с кнопками*/}
                {buttons}
            </div>
        );
    }
}
 
export default AppFilter;

022 Семантика и доступность контента (ARIA)

Accessible Rich Internet Applications (ARIA) определяет способ сделать веб контент и веб приложения (особенно те, которые разработаны с помощью Ajax и JavaScript) более доступными для людей с ограниченными возможностями.

Мы должны строить правильную семантическую структуру документа на HTML, чтобы её могли адекватно прочитать скринридеры или, чтобы по ним можно осуществлять навигацию без мышки.

ARIA представляет из себя набор тегов и атрибутов, которые упрощают взаимодействие ограниченных людей с нашим приложением: https://prgssr.ru/development/ispolzovanie-aria-v-html5.html

Так же очень важным является атрибут tabIndex, который позволяет навесить таб на те элементы, до которых по умолчанию табом мы не можем добраться

Так же есть видео-доклад по правильной организации интерфейса для людей. Ну и видео по правильной семантике.

023 Стили в React. Inline Styles

Инлайн-стили в React представляют из себя один большой объект, который мы вкладываем в атрибут style. Их особенности:

  • Они, в первую очередь, записываются в один объект
  • Единицы измерения в них прописываются автоматически (только px)
  • Все наименования так же идут в camelCase
  • Вебкиты начинаются с разных регистров букв

Предпочтительнее менять для элементов уже готовые CSS-классы, нежели чем постоянно писать инлайн-стили

024 Стили в React. CSS и SASSSCSS

В первую очередь нам нужно научить вебпак компилировать SCSS, установив плагин

npm install sass
 
// или
 
npm i -D sass-loader node-sass

Чтобы подключить sass-файл со стилями, нам нужно его непосредственно подключить к компоненту реакта

// И вместо этого импорта
import './emploees-add-form.css';
// Используем этот
import './emploees-add-form.scss';

Ну и нужно отметить, что если нам нужно импортировать в scss те же переменные из другого scss, то нам придётся импорты делать в каждый файл, а не только в основной

025 Стили в React. Динамические классы и стили

Динамическими классами и стилями называются те классы и стили, которые мы добавили через условие (например, тернарный оператор)

const buttonsData = [
	// Добавим условие colored, по которому будем проверять цветность
    {name: 'all', label: 'Все сотрудники', colored: false},
    {name: 'onIncrease', label: 'На повышение', colored: true},
    {name: 'moreSalary', label: 'ЗП больше 1000$', colored: false},
];
 
const buttons = buttonsData.map(({name, label, colored}) => {
    const active = this.props.filter === name;
    const clazz = active ? 'btn-light' : 'btn-outline-light';
 
    // Сохраняем кнопки в массив
    return (
        <button className={`btn ${clazz}`}
                type="button"
                key={name}
                onClick={() => this.props.onFilterSelect(name)}
 
				// Так выглядят динамические стили
                style={colored ? {color: 'red'} : null}
        >
            {label}
        </button>
    );
})

|500

Желательно такие условия выносить в отдельную переменную, чтобы не портить код

const buttons = buttonsData.map(({name, label, colored}) => {
    const active = this.props.filter === name;
    const clazz = active ? 'btn-light' : 'btn-outline-light';
 
	// Тут мы будем хранить условие, которое передадим в рендер объекта
	const styled = colored ? {color: 'red'} : null;
 
    // Сохраняем кнопки в массив
    return (
        <button className={`btn ${clazz}`}
                type="button"
                key={name}
                onClick={() => this.props.onFilterSelect(name)}
 
				// Результат условия будет находиться тут
                style={styled}
        >
            {label}
        </button>
    );
})

026 Стили в React. Styled Components

Styled Components в React позволяют писать CSS код прямо внутри реакт-компонентов

Устанавливаются через:

npm install --save styled-components

Дальше для реализации компонента с логикой и его стилями используются ==тэгированные шаблонные строки== Конкретно тут мы сделаем компонент Button, который будет представлять из себя ссылку a с определёнными стилями внутри кавычек

const Button = styled.a`
  /* This renders the buttons above... Edit me! */
  display: inline-block;
  border-radius: 3px;
  padding: 0.5rem 0;
  margin: 0.5rem 1rem;
  width: 11rem;
  background: transparent;
  color: white;
  border: 2px solid white;
 
  /* The GitHub button is a primary button
   * edit this to target it specifically! */
  ${props => props.primary && css`
    background: white;
    color: black;
  `}
`

И вот пример использования стилевых компонентов в React: Так же эти компоненты можно экспортировать в другие компоненты

// импорт стилевых компонентов
import styled from 'styled-components';
 
// Создание стилевых компонентов
export const Wrapper = styled.div`
    width: 600px;
    margin: 80px auto 0 auto;
`;
 
function App() {
    return (
	    // Использование стилевых компонентов
        <Wrapper>
            <WhoAmI name='John' surname="Smith" link="google.com" />
            <WhoAmI name='Angela' surname="Coose" link="facebook.com" />
        </Wrapper>
    );
}

Так же компоненты стилей можно наследовать и переопределять для других элементов:

App.js

export const Button = styled.button`
  display: block;
  padding: 5px 15px;
  background-color: gold;
  border: 1px solid rgba(0, 0, 0, 0.2);
  box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2);
`;

index.js

import App, {Btn, Field, Header, Button} from './App';
import styled from 'styled-components';
 
// В () указываем элемент, который мы переписываем
const BigButton = styled(Button)`
  margin: 0 auto;
  width: 245px;
  height: 50px;
`;
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <StrictMode>
        <App/>
        {/* Непосредственное использование переопределённой кнопки */}
        <BigButton>Some Text</BigButton>
    </StrictMode>
);

Так же мы можем заменить выводимый тег. То есть если мы создавали компонент стилей div, то мы можем его поменять на a или на любой другой тег через as="тег"

index.js

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <StrictMode>
        <App/>
        {/* меняем тег 'div' на 'a' */}
        <BigButton as="a">Some Text</BigButton>
    </StrictMode>
);

Так же синтаксис поддерживает вкладывание обращений к элементам

App.js

const EmpItem = styled.div`
    padding: 20px;
    margin-bottom: 15px;
    border-radius: 5px;
    box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2);
    a {
        display: block;
        margin: 10px 0px 10px 0px;
        color: black;
    }
    input {
        display: block;
        margin-top: 10px;
    }
`;

Так же мы можем использовать выражения внутри наших CSS-свойств (те же тернарные операторы, которые будут подставлять нужное значение свойства в зависимости от значения атрибута, которое передаётся через props)

App.js

//...
const EmpItem = styled.div`
    padding: 20px;
    margin-bottom: 15px;
    border-radius: 5px;
    box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2);
    a {
        display: block;
        margin: 10px 0px 10px 0px;
 
        /*тут мы использем условие, опираясь на значение пропса*/
        color: ${props => props.active ? 'orange' : 'black'};
    }
    input {
        display: block;
        margin-top: 10px;
    }
`;
 
//...
render() {
    const {name, surname, link} = this.props;
    const {text, years, position} = this.state;
    return (
 
	    // В качестве пропса передаём атрибут активности (по умолчанию true)
        <EmpItem active>
            <Button onClick={() => this.nextYear()}>{text}</Button>
            <Header>My name is {name}, surname - {surname}, age - {years} <br/> profession - {position}</Header>
            <a href={link}>My profile</a>
            <form>
                <label htmlFor={`profession${name}`}>Введите вашу профессию</label>
                <input type="text" id={`profession${name}`} onChange={(e) => this.commitInputChanges(e, 'red')}/>
            </form>
        </EmpItem>
    );
};

Несколько важных фактов:

  • Вендорные префиксы ставятся автоматически - их не требуется писать
  • Отношения пишутся и работают как в обычном CSS
  • Псевдоселекторы и псевдоэлементы работают точно так же
  • Так же можно создавать тут же CSS-анимации

Преймущества:

  • Инкапсулирование стилей - они нигде друг с другом не пересекаются и нет необходимости писать лишние классы
  • Так же отпадает необходимость пользоваться БЭМом
  • Возможность использования пропсов и условий
  • Вендорные префиксы ставятся автоматически

Минусы:

  • К такому синтаксису нужно привыкнуть
  • Очень легко запутаться в тегах, если написано очень много стилизованных компонентов
  • Названия стилей внутри devtools превращены в кашу (так как идёт динамическая генерация имён классов)
  • CSS и JS вместе до конца - их не получится отдельно закэшировать

027 Стили в React. Готовые библиотеки компонентов со стилями

Для React существует отдельный и подогнанный под него ==Bootstrap==

npm install react-bootstrap bootstrap

Дальше в index.js (самый основной JS-файл) нужно запихнуть файл стилей

import 'bootstrap/dist/css/bootstrap.min.css';

И далее в побочных файлах только остаётся импортировать определённые компоненты бутстрапа (из документации)

import {Container, Row, Col} from 'react-bootstrap';
 
const BootstrapTest = () => {
    return (
        <Container>
            <Row>
                <Col>1 of 2</Col>
                <Col>2 of 2</Col>
            </Row>
        </Container>
    );
}
 
export default BootstrapTest;

Использование в основном файле:

import React, {StrictMode} from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App, {Btn, Field, Header} from './App';
import styled from 'styled-components';
 
// Импорты бутстрапа и примера
import 'bootstrap/dist/css/bootstrap.min.css';
import BootstrapTest from './BootstrapTest';
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <StrictMode>
      <App/>
      <BootstrapTest/>  {/* использование */}
  </StrictMode>
);

028 Поля классов и static

static поля классов:

  • Вызываются просто через имя класса без инициализации
  • Можно получить и вывести их значения
  • Позволяют задать стейт как поле класса (без инициализации через конструктор и ключевого слова static)
class EmployeesAddForm extends Component {
 
    state = {
        name: '',
        salary: ''
    }
 
    static onLog = () => {
        console.log('Hey!');
    }
 
	static logged = 'logged';
}
 
EmployeesAddForm.onLog();
console.log(EmployeesAddForm.logged);
 
// Hey
// logged

029 Заключение модуля

Попробовать сверстать на React Cofee Shop по макету (уже скопирован в драфты):

https://www.figma.com/file/Iu4Lul87WvzdM5CXFwE4qtZ6/Coffee-shop?node-id=0%3A1

Библиотека React. Средний фундаментальный уровень

API React

001 Что такое API и как работают реальные приложения

API (Application Programming Interface) - это набор готовых функций и свойств, которые можно использовать. Либо это можно назвать договором - мы обязуемся использовать правильный синтаксис и использовать допустимые запросы для того, чтобы получить нужные данные или что-то выполнить, а АПИ обязуется выполнить то, что мы ему отправили.

Вот пример открытого API, который готов нам выдать информацию с сервера, если мы только правильно введём fetch-запрос с нужной ссылкой - контракт - я правильно запрашиваю, а ты выдаёшь

Конкретно в мире веб-разработки API говорит нам, как мы будем реализовывать общение фронт-енд части приложения с сервером

Так же ещё примеры АПИ-сервисов:

Так же есть отдельная страница на гитхабе, где есть огромный список всех публичных API

002 Новый проект и работа с сервером

Будет разрабатываться реальный сайт на основе данных, полученных с сайта по вселенной Marvel. Отсюда нам нужно только произвести настройку доступности отправляемых запросов (ставим ото всюду *.) и сам ключ для совершения запросов abfdaba95091affea928543eb9253ded

Чтобы не писать путь до изображения, можно просто вставить его как объект - вебпак поймёт

Далее нужно реализовать сервис, который будет отвечать за взаимодействие фронта с бэком

|500

И тут мы описываем класс, который будет содержать методы, получающие данные с сервера

src > services > marvel.service.js

// это класс для взаимодействия фронта с апи марвела
class MarvelService {
   api = 'abfdaba95091affea928543eb9253ded';
 
   // данная функция будет фетчить данные с сервера и возвращать их
   getResource = async (url) => {
      let res = await fetch(url);
 
      if (!res.ok) {
         throw new Error(`Couldn't fetch ${url}, status: ${res.status}`);
      }
 
      return await res.json();
   };
 
   // эта функция будет получать всех персонажей по переданной ссылке
   getAllCharacters = async () => {
      return await this.getResource(`https://gateway.marvel.com:443/v1/public/characters?limit=9&offset=36&apikey=${this.api}`);
   };
 
   // эта функция будет получать одного персонажа по id
   getCharacter = async (id) => {
      return await this.getResource(`https://gateway.marvel.com:443/v1/public/characters/${id}?apikey=${this.api}`);
   };
}
 
export default MarvelService;

Получим для примера ответ от сервера в виде объекта:

src > index.js

// создаём инстанс класса
const marvelService = new MarvelService();
 
// получаем персонажей с сервера и выводим их в консоль
marvelService.getAllCharacters().then(res => console.log(res));
 
// получаем одного персонажа с сервера
marvelService.getCharacter(1009567).then(res => console.log(res));
 
const root = ReactDOM.createRoot(document.getElementById('root'));
 
root.render(
   <React.StrictMode>
      <App />
   </React.StrictMode>
);

Первым нам вышел персонаж по id, а вторым ответом мы получили ограниченный массив персонажей (9 штук)

Далее нам нужно доработать метод - вынесем повторяющиеся данные в отдельные поля класса

src > services > marvel.service.js

// это класс для взаимодействия фронта с апи марвела
class MarvelService {
   _apiBase = 'https://gateway.marvel.com:443/v1/public/';
   _apiKey = 'apikey=abfdaba95091affea928543eb9253ded';
 
   // данная функция будет фетчить данные с сервера и возвращать их
   getResource = async (url) => {
      let res = await fetch(url);
 
      if (!res.ok) {
         throw new Error(`Couldn't fetch ${url}, status: ${res.status}`);
      }
 
      return await res.json();
   };
 
   // эта функция будет получать всех персонажей по переданной ссылке
   getAllCharacters = async () => {
      return await this.getResource(`${this._apiBase}characters?limit=9&offset=36&${this._apiKey}`);
   };
 
   // эта функция будет получать одного персонажа по id
   getCharacter = async (id) => {
      return await this.getResource(`${this._apiBase}characters/${id}?${this._apiKey}`);
   };
}
 
export default MarvelService;

И далее попробуем вывести массив имён из того ответа, который приходит нам с сервера:

src > index.js

marvelService.getAllCharacters().then(res => res.data.results.forEach(item => console.log(item.name)));

003 Трансформация данных и компонент со случайным персонажем

Самое первое, что нужно реализовать - это получение данных по персонажу с сервера:

  • Метод _transformCharacter() принимает в себя данные с сервера и возвращает данные отформатированные под тот формат, что нужно использовать в приложении. Такой подход заменит работу с интерфейсом.
  • Методы getAllCharacters() и getCharacter() модифицируем таким образом, чтобы они как и раньше принимали и сохраняли в себе результат запроса и возвращали во внешние модули только отформатированный запрос через _transformCharacter()

src > services > marvel.service.js

class MarvelService {
   _apiBase = 'https://gateway.marvel.com:443/v1/public/';
   _apiKey = 'apikey=abfdaba95091affea928543eb9253ded';
 
   getResource = async (url) => {
      let res = await fetch(url);
 
      if (!res.ok) {
         throw new Error(`Couldn't fetch ${url}, status: ${res.status}`);
      }
 
      return await res.json();
   };
 
   getAllCharacters = async () => {
		const res = await this.getResource(
			`${this._apiBase}characters?limit=9&offset=36&${this._apiKey}`,
		);
 
		// тут мы возвращаем массив персонажей, где каждый отдельный элемент массива проходится через трансформацию
		return res.data.results.map(this._transformCharacter);
	};
 
   // эта функция будет получать одного персонажа по id
   getCharacter = async (id) => {
      const res = await this.getResource(`${this._apiBase}characters/${id}?${this._apiKey}`);
      return this._transformCharacter(res.data.results[0]);
   };
 
   // тут мы будем возвращать только ограниченное количество данных при запросе на сервер
   _transformCharacter = (char) => {
      return {
         name: char.name,
         description: char.description,
         thumbnail: char.thumbnail.path + '.' + char.thumbnail.extension,
         homepage: char.urls[0].url,
         wiki: char.urls[1].url,
      };
   };
}
 
export default MarvelService;

Далее переходим к отображению данных на фронте:

  • создаём классовый компонент
  • создадим его состояние, которое будет хранить в себе объект персонажа
  • далее инициализируем сервис по работе с сервером
  • далее создадим функцию onCharLoaded, которая будет менять состояние персонажа компонента
  • в функции updateChar мы генерируем id персонажа и дёргаем сервер, получая информацию о персонаже (уже заранее отформатированную сервисом) и далее устанавливаем состояние в компоненте
  • метод sliceDescription обрежет строку, так как описание полученное с сервера, может выходить за пределы карточки

src > components > randomChar > RandomChar.js

import './randomChar.scss';
import mjolnir from '../../resources/img/mjolnir.png';
import { Component } from 'react';
import MarvelService from '../../services/marvel.service';
 
class RandomChar extends Component {
   constructor(props) {
      super(props);
      // вызываем функцию обновления при создании компонента
      this.updateChar();
   }
 
   // состояние данных персонажа
   state = {
      char: {},
   };
 
   // создаём инстанс сервиса для общения с сервером
   marvelService = new MarvelService();
 
   // этот метод будет просто менять состояние персонажа
   onCharLoaded = (char) => {
      this.setState({ char });
   };
 
   // метод, который будет общаться с сервером, получать данные и записывать их в стейт
   updateChar = () => {
      // получаем рандомный id персонажа из определённого диапазона
      // рандом * (маисмум - минимум) + минимум
      const id = Math.floor(Math.random() * (1011400 - 1011000) + 1011000);
 
      // вызываем получение персонажа по id с нашего сервиса и меняем состояние компонента
      // здесь мы можем вставить просто res, так как мы трансформировали данные в сервисе через _transformCharacter
      this.marvelService.getCharacter(id).then(this.onCharLoaded);
   };
 
  // эта функция обрежет строку, если та длинее 150 символов
   sliceDescription = (description) => {
      if (description.length > 150) {
         return description.slice(0, 150) + '...';
      }
      return description;
   };
 
   render() {
	   // деструктурируем значения из состояния, чтобы внести их в компонент
      const {
         char: { name, description, thumbnail, homepage, wiki },
      } = this.state;
 
      return (
         <div className='randomchar'>
            <div className='randomchar__block'>
               <img src={thumbnail} alt='Random character' className='randomchar__img' />
               <div className='randomchar__info'>
                  <p className='randomchar__name'>{name}</p>
                  <p className='randomchar__descr'>
                     {description
                        ? this.sliceDescription(description)
                        : 'Данных про данного персонажа нет'}
                  </p>
                  <div className='randomchar__btns'>
                     <a href={homepage} className='button button__main'>
                        <div className='inner'>homepage</div>
                     </a>
                     <a href={wiki} className='button button__secondary'>
                        <div className='inner'>Wiki</div>
                     </a>
                  </div>
               </div>
            </div>
            <div className='randomchar__static'>
               <p className='randomchar__title'>
                  Random character for today!
                  <br />
                  Do you want to get to know him better?
               </p>
               <p className='randomchar__title'>Or choose another one</p>
               <button className='button button__main'>
                  <div className='inner'>try it</div>
               </button>
               <img src={mjolnir} alt='mjolnir' className='randomchar__decoration' />
            </div>
         </div>
      );
   }
}
 
export default RandomChar;

Так же хочется отметить про несколько видов записей функций: если мы просто вставим в функцию вызов другой функции, то вложенная функция вызовется с переданным дефолтным аргументом родительской функции (первой)

// длинная запись
this.marvelService.getCharacter(id).then((res) => {
   this.onCharLoaded(res);
});
 
// короткая запись
this.marvelService.getCharacter(id).then(this.onCharLoaded);

И тут нужно сразу сказать, что такой подход, который был описан выше - неправильный. Работать со стейтом, подписываться на события и создавать сервисы при конструировании компонента является плохой практикой

Итог: мы имеем при перезагрузке страницы рандомного персонажа

Оформление работы с сервером:

  • Если наше приложение взаимодействует с сервером, то сетевую логику нужно отделять от реализации фронтенда
  • В остальных компонентах нужно использовать только результаты работы данного сервиса

004 Хороший тон приложения (спиннер, ошибки…)

Далее на сайт нужно добавить спиннер, который будет показывать пользователю, что информация загружается - это его успокоит и упростит взаимодействие с сайтом

Можно просто скачать любой спиннер на выбор

А можно создать свой спиннер

Первым делом создаём компонент со спиннером, который будем использовать на странице

src > components > Spinner > Spinner.js

const Spinner = () => {
   return (
      <svg         xmlns='http://www.w3.org/2000/svg'
         style={{ margin: '0 auto', background: 'none', display: 'block' }}
         width='200px'
         height='200px'
         viewBox='0 0 100 100'
         preserveAspectRatio='xMidYMid'
      >
         <g transform='translate(80,50)'>
            <g transform='rotate(0)'>
               <circle cx='0' cy='0' r='7' fill='#1c4595' fillOpacity='1'>
                  <animateTransform
                     attributeName='transform'
                     type='scale'
                     begin='-0.875s'
                     values='1.5 1.5;1 1'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                  ></animateTransform>
                  <animate                     attributeName='fillOpacity'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                     values='1;0'
                     begin='-0.875s'
                  ></animate>
               </circle>
            </g>
         </g>
         <g transform='translate(71.21320343559643,71.21320343559643)'>
            <g transform='rotate(45)'>
               <circle cx='0' cy='0' r='7' fill='#1c4595' fillOpacity='0.875'>
                  <animateTransform
                     attributeName='transform'
                     type='scale'
                     begin='-0.75s'
                     values='1.5 1.5;1 1'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                  ></animateTransform>
                  <animate                     attributeName='fillOpacity'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                     values='1;0'
                     begin='-0.75s'
                  ></animate>
               </circle>
            </g>
         </g>
         <g transform='translate(50,80)'>
            <g transform='rotate(90)'>
               <circle cx='0' cy='0' r='7' fill='#1c4595' fillOpacity='0.75'>
                  <animateTransform
                     attributeName='transform'
                     type='scale'
                     begin='-0.625s'
                     values='1.5 1.5;1 1'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                  ></animateTransform>
                  <animate                     attributeName='fillOpacity'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                     values='1;0'
                     begin='-0.625s'
                  ></animate>
               </circle>
            </g>
         </g>
         <g transform='translate(28.786796564403577,71.21320343559643)'>
            <g transform='rotate(135)'>
               <circle cx='0' cy='0' r='7' fill='#1c4595' fillOpacity='0.625'>
                  <animateTransform
                     attributeName='transform'
                     type='scale'
                     begin='-0.5s'
                     values='1.5 1.5;1 1'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                  ></animateTransform>
                  <animate                     attributeName='fillOpacity'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                     values='1;0'
                     begin='-0.5s'
                  ></animate>
               </circle>
            </g>
         </g>
         <g transform='translate(20,50.00000000000001)'>
            <g transform='rotate(180)'>
               <circle cx='0' cy='0' r='7' fill='#1c4595' fillOpacity='0.5'>
                  <animateTransform
                     attributeName='transform'
                     type='scale'
                     begin='-0.375s'
                     values='1.5 1.5;1 1'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                  ></animateTransform>
                  <animate                     attributeName='fillOpacity'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                     values='1;0'
                     begin='-0.375s'
                  ></animate>
               </circle>
            </g>
         </g>
         <g transform='translate(28.78679656440357,28.786796564403577)'>
            <g transform='rotate(225)'>
               <circle cx='0' cy='0' r='7' fill='#1c4595' fillOpacity='0.375'>
                  <animateTransform
                     attributeName='transform'
                     type='scale'
                     begin='-0.25s'
                     values='1.5 1.5;1 1'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                  ></animateTransform>
                  <animate                     attributeName='fillOpacity'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                     values='1;0'
                     begin='-0.25s'
                  ></animate>
               </circle>
            </g>
         </g>
         <g transform='translate(49.99999999999999,20)'>
            <g transform='rotate(270)'>
               <circle cx='0' cy='0' r='7' fill='#1c4595' fillOpacity='0.25'>
                  <animateTransform
                     attributeName='transform'
                     type='scale'
                     begin='-0.125s'
                     values='1.5 1.5;1 1'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                  ></animateTransform>
                  <animate                     attributeName='fillOpacity'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                     values='1;0'
                     begin='-0.125s'
                  ></animate>
               </circle>
            </g>
         </g>
         <g transform='translate(71.21320343559643,28.78679656440357)'>
            <g transform='rotate(315)'>
               <circle cx='0' cy='0' r='7' fill='#1c4595' fillOpacity='0.125'>
                  <animateTransform
                     attributeName='transform'
                     type='scale'
                     begin='0s'
                     values='1.5 1.5;1 1'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                  ></animateTransform>
                  <animate                     attributeName='fillOpacity'
                     keyTimes='0;1'
                     dur='1s'
                     repeatCount='indefinite'
                     values='1;0'
                     begin='0s'
                  ></animate>
               </circle>
            </g>
         </g>
      </svg>
   );
};
 
export default Spinner;

Так будет выглядеть компонент ошибки:

src > components > ErrorMessage > ErrorMessage.js

import React from 'react';
import ErrorIcon from './error.gif';
 
const ErrorMessage = () => {
   return (
      <img         style={{
            display: 'block',
            width: '250px',
            height: '250px',
            margin: '0 auto',
            objectFit: 'contain',
         }}
         src={ErrorIcon}
         alt='Ошибка'
      />
   );
};
 
export default ErrorMessage;

Так же можно отметить, что мы можем положить нужное изображение в паблик папку и получить нужное нам изображение через переменную окружения

И тут мы реализуем два новых состояния и метод onError, которые будут отвечать за отображение ошибки и спиннера

src > components > randomChar > RandomChar.js

import './randomChar.scss';
import mjolnir from '../../resources/img/mjolnir.png';
import { Component } from 'react';
import MarvelService from '../../services/marvel.service';
import Spinner from '../Spinner/Spinner';
import ErrorMessage from '../ErrorMessage/ErrorMessage';
 
class RandomChar extends Component {
   constructor(props) {
      super(props);
      this.updateChar();
   }
 
   state = {
      char: {},
      loading: true, // состояние загрузки компонента
      error: false, // состояние ошибки
   };
 
   marvelService = new MarvelService();
 
   onCharLoaded = (char) => {
      // так же тут дополняем информацию по загрузке
      this.setState({ char, loading: false });
   };
 
   // этот метод будет срабатывать при ошибке
   onError = () => {
      this.setState({ loading: false, error: true });
   };
 
   updateChar = () => {
      const id = Math.floor(Math.random() * (1011400 - 1011000) + 1011000);
      // const id = 10;
 
      // так же дополняем обработку ответа от сервера реагированием на ошибку      this.marvelService.getCharacter(id).then(this.onCharLoaded).catch(this.onError);
   };
 
   render() {
      const { char, loading, error } = this.state;
 
      const showError = error ? <ErrorMessage /> : null;
      const showLoading = loading ? <Spinner /> : null;
      const showContent = !(error || loading) ? <View char={char} /> : null;
 
      return (
         <div className='randomchar'>
            {/* показываем на странице то, что подошло под условия */}
            {showContent}
            {showLoading}
            {showError}
            <div className='randomchar__static'>
               <p className='randomchar__title'>
                  Random character for today!
                  <br />
                  Do you want to get to know him better?
               </p>
               <p className='randomchar__title'>Or choose another one</p>
               <button className='button button__main'>
                  <div className='inner'>try it</div>
               </button>
               <img src={mjolnir} alt='mjolnir' className='randomchar__decoration' />
            </div>
         </div>
      );
   }
}
 
// динамическую информацию нашего компонента вынесем в отдельный компонент
const View = ({ char }) => {
   const { name, description, thumbnail, homepage, wiki } = char;
 
   const sliceDescription = (description) => {
      if (description.length > 150) {
         return description.slice(0, 150) + '...';
      }
      return description;
   };
 
   return (
      <div className='randomchar__block'>
         <img src={thumbnail} alt='Random character' className='randomchar__img' />
         <div className='randomchar__info'>
            <p className='randomchar__name'>{name}</p>
            <p className='randomchar__descr'>
               {description
                  ? sliceDescription(description)
                  : 'Данных про данного персонажа нет'}
            </p>
            <div className='randomchar__btns'>
               <a href={homepage} className='button button__main'>
                  <div className='inner'>homepage</div>
               </a>
               <a href={wiki} className='button button__secondary'>
                  <div className='inner'>Wiki</div>
               </a>
            </div>
         </div>
      </div>
   );
};
 
export default RandomChar;

Итог: мы имеем отображение загрузки и ошибки

005 Жизненный цикл компонентов

Сейчас воспроизведём одну проблему в работе компонентов:

  • переведём компонент App в классовый и внутри него создадим возможность удалять компонент со страницы, показывая null вместо него

  • Далее в самом компоненте будем внутри конструктора по интервалу вызвать запрос на получение данных updateChar

  • И несколько раз скроем и покажем компонент

В итоге мы получим ситуацию, когда запрос на получение данных со страницы уходит на просто за раз по два раза (что уже плохо), но так же и компоненты, которые мы насоздавали - не исчезают и продолжают отправлять запросы на сервер, что приводит к отправке более чем двух запросов в секунду. Такой подход создаёт сильную угрозу утечки данных

И тут нам нужно перейти к жизненному циклу компонента, чтобы понять, каким образом у нас происходят данные ошибки

Жизненный цикл компонента делится на 3 этапа (если не включать состояние ошибки) и предполагает под собой 3 хука этих состояний:

  • componentDidMount - компонент появляется на странице
  • componentDidUpdate - компонент обновляется
  • componentWillUnmount - компонент уходит со страницы

И данную структуру нужно знать:

  • Первым этапом у нас идёт монтирование
    • вызывается конструктор
    • после конструктора идёт render компонента
    • потом обновляется DOM-дерево
    • и в конце вызывается componentDidMount
  • Вторым этапом у нас идёт обновление компонента
    • обновление компонента вызвают:
      • изменение пропсов
      • установление нового состояния через setState()
      • насильное обновление через forceUpdate()
    • И мы опять попадаем в метод render, который монтирует наш компонент
    • Дальше обновляется дерево
    • И срабатывает хук componentDidUpdate
  • Третьим этапом мы просто размонтируем компонент и стираем со страницы
    • тут вызывается хук componentWillUnmount

Расставим на всех контрольных точках приложения логи определённого этапа монтирования компонента

Сейчас закомментируем вызов функции обновления персонажа на странице, чтобы у нас нормально работал компонент.

class RandomChar extends Component {
   constructor(props) {
      super(props);
      // this.updateChar();
      console.log('constructor');
   }
 
   componentDidMount() {
      console.log('mount');
   }
 
   componentWillUnmount() {
      console.log('unmount');
   }
 
   state = {
      char: {},
      loading: true,
      error: false,
   };
 
   marvelService = new MarvelService();
 
   onCharLoaded = (char) => {
      console.log('update');
      this.setState({ char, loading: false });
   };
 
   onError = () => {
      this.setState({ loading: false, error: true });
   };
 
   updateChar = () => {
      const id = Math.floor(Math.random() * (1011400 - 1011000) + 1011000);
 
      this.marvelService.getCharacter(id).then(this.onCharLoaded).catch(this.onError);
   };
 
   render() {
      console.log('render');
 
      const { char, loading, error } = this.state;
 
      const showError = error ? <ErrorMessage /> : null;
      const showLoading = loading ? <Spinner /> : null;
      const showContent = !(error || loading) ? <View char={char} /> : null;
 
      return (
         <div className='randomchar'>
            {showContent}
            {showLoading}
            {showError}
            <div className='randomchar__static'>
               <p className='randomchar__title'>
                  Random character for today!
                  <br />
                  Do you want to get to know him better?
               </p>
               <p className='randomchar__title'>Or choose another one</p>
               <button className='button button__main'>
                  <div className='inner'>try it</div>
               </button>
               <img src={mjolnir} alt='mjolnir' className='randomchar__decoration' />
            </div>
         </div>
      );
   }
}

И тут можно увидеть, что компонент рендерится ровно один раз

Но если мы вернём в конструктор данную запись, то можно увидеть, что компонент обновился два раза, что и привело к двойному срабатыванию функции отправки запроса на сервер.

Это происходит потому, что мы обновляем состояние и запрашиваем рендер компонента до того, как он отрендерился в первый раз. Из-за такого наслоения вместо одного рендера происходит перерендер с новым состоянием.

Нам можно использовать обновления состояний компонента только на этапе "commit"

constructor(props) {
   super(props);
   this.updateChar();
   console.log('constructor');
}

И чтобы поправить ошибку, просто вызовем функцию отправки запроса на сервер на этапе коммита компонента, а именно в методе componentDidMount()

constructor(props) {
   super(props);
   console.log('constructor');
}
 
componentDidMount() {
   this.updateChar();
   console.log('mount');
}

Ну и чтобы при размонтировании компонента у нас остановился и таймер, нужно останавливать этот таймер на этапе размонтирования компонента

Так же нужно отметить:

  • Что данную отписку нужно выполнять всегда, так как она продолжит работать, даже если мы перейдём на другую страницу (все компоненты уничтожатся, а подписка останется)
  • Что если мы добавили подписку через стандартное DOM-api (например, addEventListener), то и удалять эту подписку нужно через DOM-api (тут - removeEventListener)
componentDidMount() {
   this.updateChar();
   this.timerId = setInterval(this.updateChar, 3000);
   console.log('mount');
}
 
componentWillUnmount() {
   clearInterval(this.timerId);
   console.log('unmount');
}

006 Практика с жизненным циклом, componentDidUpdate

Сейчас нам нужно реализовать загрузку списка персонажей на странице, а так же реализовать вывод информации по ним при нажатии на карточку в боковом меню страницы

В компоненте App добавим состояние selectedChar, которое будет хранить id выбранного персонажа и метод onSelectedChar, который мы будем вызывать внутри компонента CharList, чтобы получить этот нужный нам id персонажа

src > components > app > App.js

class App extends Component {
   // это состояние выбранного персонажа
   state = {
      selectedChar: null,
   };
 
   // это метод, который будет вызываться из CharList и передаст в родительский компонент id выбранного персонажа
   onSelectedChar = (id) => {
      this.setState({ selectedChar: id });
   };
 
   render() {
      return (
         <div className='app'>
            <AppHeader />
            <main>
	           <RandomChar />
               <div className='char__content'>
                  <CharList onCharSelected={this.onSelectedChar} />
                  <CharInfo charId={this.state.selectedChar} />
               </div>
               <img className='bg-decoration' src={decoration} alt='vision' />
            </main>
         </div>
      );
   }
}
 
export default App;

Таким образом реализована логика списка персонажей на странице

Так же при клике на карточку будет вызваться функция onCharSelected(), которая вернёт в компонент App нужный нам id персонажа

src > components > charList > CharList.js

import './charList.scss';
import { Component } from 'react';
import MarvelService from '../../services/marvel.service';
import ErrorMessage from '../ErrorMessage/ErrorMessage';
import Spinner from '../Spinner/Spinner';
 
class CharList extends Component {
   state = {
      charList: [],
      loading: true,
      error: false,
   };
 
   marvelService = new MarvelService();
 
   componentDidMount() {
      this.marvelService.getAllCharacters().then(this.onCharListLoaded).catch(this.onError);
   }
 
   onCharListLoaded = (charList) => {
      this.setState({
         charList,
         loading: false,
      });
   };
 
   onError = () => {
      this.setState({
         error: true,
         loading: false,
      });
   };
 
   // это метод рендера отдельных элементов карточек
   renderItems(arr) {
      const items = arr.map((item) => {
         let imgStyle = { objectFit: 'cover' };
         if (
            item.thumbnail ===
            'http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available.jpg'
         ) {
            imgStyle = { objectFit: 'unset' };
         }
 
         return (
            <li
               className='char__item'
               key={item.id}
            // при клике будет срабатывать колл-бэк функция, которая мы передали из App
               onClick={() => this.props.onCharSelected(item.id)}
            >
			   <img src={item.thumbnail} alt={item.name} style={imgStyle} />
               <div className='char__name'>{item.name}</div>
            </li>
         );
      });
 
      // А эта конструкция вынесена для центровки спиннера/ошибки
      return <ul className='char__grid'>{items}</ul>;
   }
 
   render() {
      const { charList, loading, error } = this.state;
 
      const items = this.renderItems(charList);
 
      const errorMessage = error ? <ErrorMessage /> : null;
      const spinner = loading ? <Spinner /> : null;
      const content = !(loading || error) ? items : null;
 
      return (
         <div className='char__list'>
            {errorMessage}
            {spinner}
            {content}
            <button className='button button__main button__long'>
               <div className='inner'>load more</div>
            </button>
         </div>
      );
   }
}
 
export default CharList;

Так же добавим получение комиксов с сервера

src > service > marvel.service.js

_transformCharacter = (char) => {
   return {
      id: char.id,
      name: char.name,
      description: char.description
         ? char.description.slice(0, 150) + '...'
         : 'No description for this person',
      thumbnail: char.thumbnail.path + '.' + char.thumbnail.extension,
      homepage: char.urls[0].url,
      wiki: char.urls[1].url,
      comics: char.comics.items,
   };
};

И далее нам нужно реализовать рендер компонента информации персонажей:

  • при загрузке страницы componentDidMount обновляем персонажа updateChar
  • при обновлении пропсов так же через componentDidUpdate обновляем персонажа

src > components > charInfo > CharInfo.js

import { Component } from 'react';
import './charInfo.scss';
import MarvelService from '../../services/marvel.service';
import ErrorMessage from '../ErrorMessage/ErrorMessage';
import Spinner from '../Spinner/Spinner';
import Skeleton from '../skeleton/Skeleton';
 
// это основной компонент логики и состояния
class CharInfo extends Component {
   state = {
      char: null,
      loading: false,
      error: false,
   };
 
   marvelService = new MarvelService();
 
   componentDidMount() {
      this.updateChar();
   }
 
   // получает при срабатывании предыдущее состояние и предыдущие пропсы
   componentDidUpdate(prevProps, prevState, screenshot) {
      // если текущий ID персонажа из пропсов не равен предыдущим, то обновляем компонент
      // это условие исключит бесконечный цикл обновлений, который может начаться
      if (this.props.charId !== prevProps.charId) {
         this.updateChar();
      }
   }
 
   // этот метот будет обновлять выводимого в компоненте персонажа
   updateChar = () => {
      const { charId } = this.props;
 
	  // если id персонажа нет, то возвращаемся из функции
      if (!charId) return;
 
	  // активируем загрузку
      this.onCharLoading();
 
	  // получаем персонажа по запросу
	  this.marvelService.getCharacter(charId)
		.then(this.onCharLoaded)
		.catch(this.onError);
   };
 
   // три стандартных метода для обновления состояния персонажа:
   onCharLoaded = (char) => {
      this.setState({ char, loading: false });
   };
 
   onCharLoading = () => {
      this.setState({ loading: true });
   };
 
   onError = () => {
      this.setState({ loading: false, error: true });
   };
 
   render() {
      const { char, loading, error } = this.state;
 
      const showSkeleton = !(error || loading || char) ? <Skeleton /> : null;
      const showError = error ? <ErrorMessage /> : null;
      const showLoading = loading ? <Spinner /> : null;
      const showContent = !(error || loading || !char) ? <View char={char} /> : null;
 
      return (
         <div className='char__info'>
            {showSkeleton}
            {showLoading}
            {showContent}
            {showError}
         </div>
	  );
   }
}
 
// а это компонент интерфейса
const View = ({ char }) => {
   const { name, description, thumbnail, homepage, wiki, comics } = char;
 
   // определяем стили для картинки, если она не найдена
   let imgStyle = { objectFit: 'cover' };
   if (thumbnail === 'http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available.jpg') {
      imgStyle = { objectFit: 'contain' };
   }
 
   return (
      <>
         <div className='char__basics'>
            <img style={imgStyle} src={thumbnail} alt={name} />
            <div>               <div className='char__info-name'>{name}</div>
               <div className='char__btns'>
                  <a href={homepage} className='button button__main'>
                     <div className='inner'>homepage</div>
                  </a>
                  <a href={wiki} className='button button__secondary'>
                     <div className='inner'>Wiki</div>
                  </a>
               </div>
            </div>
         </div>
         <div className='char__descr'>{description}</div>
         <div className='char__comics'>Comics:</div>
         <ul className='char__comics-list'>
            {comics.length < 1 ? 'No comics for this person' : null}
            {comics.map((c, i) => (
               <li key={i} className='char__comics-item'>
                  {c.name}
               </li>
            ))}
         </ul>
      </>
   );
};
 
export default CharInfo;

Итог: до нажатия на персонажей - отображается скелетон, а уже при нажатии на персонажа, у нас подгружаются данные в info для отображения

007 Предохранители (Error Boundaries)

Попробуем воспользоваться методом, который срабатывает при появлении ошибки в компоненте componentDidCatch. В нём мы установим состояние ошибки компонента и выведем информацию о полученных аргументах.

src > component > charInfo > CharInfo.js

// сработает при ошибке
// error - сама ошибка / errorInfo - информация о компоненте, в котором произошла ошибка
componentDidCatch(error, errorInfo) {
   console.log(error, errorInfo);
   this.setState({ error: true });
}
 
updateChar = () => {
   const { charId } = this.props;
 
   if (!charId) return;
 
   this.onCharLoading();
 
   this.marvelService.getCharacter(charId).then(this.onCharLoaded).catch(this.onError);
 
   // ошибка
   this.foo.bar = 0;
};

И, как мы видим, ошибка всё равно выскакивает и в консоли нет информации из метода. Логика данного хука была изменена в 16 версии реакта и теперь она не предотвращает краш всей страницы. Это было сделано с целью предотвращения отправки на сервер некорректных данных.

И тут мы переходим к такому подходу, как Error Boundary (предохранители) - это функционал, который собой оборачивает наш компонент, и если в нём происходит ошибка, то он её отлавливает

Подобный компонент можно сделать только через класс - в функциональном подходе он ещё не реализован

И так выглядит реализация классового компонента ErrorBoundary:

  • создаём состояние ошибки
  • метод componentDidCatch будет совершать определённое действие при отлове ошибки (выведет нам объекты в консоль и запишет состояние ошибки)
  • далее по условию выведем компонент ошибки, если переловили ошибку, а если не переловили, то выведем просто обёрнутый компонент

Метод getDerivedStateFromError тут не нужен, но он может пригодиться, если нам нужно только записать состояние внутри данного класса

src > component > ErrorBoundary > ErrorBoundary.js

import React, { Component } from 'react';
import ErrorMessage from '../ErrorMessage/ErrorMessage';
 
class ErrorBoundary extends Component {
   // состояние ошибки
   state = {
      error: false,
   };
 
   // эта функция может выполнить только изменение состояния - больше ничего
   static getDerivedStateFromError(error) {
      return { error: true };
   }
 
   // сейчас метод перелова ошибки сработает
   componentDidCatch(error, errorInfo) {
      console.log(error, errorInfo);
      this.setState({ error: true });
   }
 
   render() {
      // возвращаем ошибку
      if (this.state.error) {
         return <ErrorMessage />;
      }
 
      // возвращаем компонент, который был вложен внутрь этого компонента
      return this.props.children;
   }
}
 
export default ErrorBoundary;

И далее нам нужно просто обернуть предполагаемый компонент с ошибкой внутрь ErrorBoundary

src > component > app > App.js

render() {
   return (
      <div className='app'>
         <AppHeader />
         <main>
	        <ErrorBoundary>
               <RandomChar />
            </ErrorBoundary>
            <div className='char__content'>
               <ErrorBoundary>
                  <CharList onCharSelected={this.onSelectedChar} />
               </ErrorBoundary>
               <ErrorBoundary>
	               <CharInfo charId={this.state.selectedChar} />
               </ErrorBoundary>
            </div>
            <img className='bg-decoration' src={decoration} alt='vision' />
         </main>
      </div>
   );
}

Итог: на месте, где у нас появилась ошибка будет отображаться компонент ошибки

Но так же важно знать:

Предохранитель ловит ошибки только в:

  • Методе render()
  • В методах жизненного цикла
  • В конструкторах дочерних компонентов
  • На серверном рендеринге

008 Пагинация данных (дозагрузка персонажей)

Когда мы меняем состояние через методы, можно обращаться по полному пути к состоянию А можно деструктурировать нужное нам свойство

Далее в сервисе укажем отдельным полем число оффсета и вставим его в запрос. По умолчанию оффсет внутри метода будет принимать в себя значение поля

src > service > marverl.service.js

_baseOffsetForPerson = 210;
 
getAllCharacters = async (offset = this._baseOffsetForPerson) => {
   const res = await this.getResource(
      `${this._apiBase}characters?limit=9&offset=${offset}&${this._apiKey}`,
   );
 
   return res.data.results.map(this._transformCharacter);
};

Далее нужно добавить функционал пагинации в наше приложение. Пагинация - это дозагрузка контента страницы по запросу пользователя.

  • изменим состояние charList на массив, добавим 3 состояния: newItemLoading (состояние загрузки новых персонажей), offset (отступ), charEnded (кончился ли список персонажей)
  • Добавим метод onRequest, который будет осуществлять запрос на сервер. Он будет принимать в себя тот оффсет, который нужно отступить
  • Добавим спиннер при загрузке списка перснажей onCharListLoading
  • Модифицируем метод onCharListLoaded так, чтобы он редактировал состояния при загрузке персонажей и проверял, кончился ли список
  • Далее в рендере добавляем на кнопку по условию атрибут disabled, которая заблокирует кнопку, если список грузится, добавим стили под блокировку кнопки и добавим функцию onRequest, которая дозагрузит персонажей

src > components > charList > CharList.js

class CharList extends Component {
   state = {
      charList: [],
      loading: true,
      error: false,
      newItemLoading: false, // загрузка новых персонажей
      offset: 210, // начальное положение от которого считается отступ
      charEnded: false, // отвечает за то состояние, когда кончился список персонажей
   };
 
   marvelService = new MarvelService();
 
   // при монтировании компонента вызваем запрос на сервер
   componentDidMount() {
      this.onRequest(this.state.offset);
   }
 
   // это метод, который теперь отвечает за запросы на сервер для получения данных
   onRequest = (offset) => {
      this.onCharListLoading(); // при запросе запускается спиннер загрузки
      this.marvelService.getAllCharacters(offset)
	      .then(this.onCharListLoaded)
	      .catch(this.onError);
   };
 
   // запускаем спиннер при загрузке персонажей
   onCharListLoading = () => {
      this.setState({
         newItemLoading: true,
      });
   };
 
   onCharListLoaded = (newCharList) => {
      // если сервер вернул меньше 9 персонажей, то список кончился
      let ended = false;
      if (newCharList.length < 9) ended = false;
 
      // теперь мы устанавливаем в стейт новые и старые данные
      this.setState(({ offset, charList }) => ({
         // сюда разворачиваем новые и старые элементы
         charList: [...charList, ...newCharList],
         loading: false,
         newItemLoading: false, // когда загрузился - отключаем
         offset: offset + 9, // тут уже меняем состояние пагинации
         charEnded: ended, // устанавливаем состояние окончания списка (кончился / не кончился)
      }));
   };
 
   onError = () => {
      this.setState({
         error: true,
         loading: false,
      });
   };
 
   renderItems(arr) {
      const items = arr.map((item) => {
         let imgStyle = { objectFit: 'cover' };
         if (
            item.thumbnail ===
            'http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available.jpg'
         ) {
            imgStyle = { objectFit: 'unset' };
         }
 
         return (
            <li
               className='char__item'
               key={item.id}
               onClick={() => this.props.onCharSelected(item.id)}
            >               <img src={item.thumbnail} alt={item.name} style={imgStyle} />
               <div className='char__name'>{item.name}</div>
            </li>
         );
      });
 
      return <ul className='char__grid'>{items}</ul>;
   }
 
   render() {
      const { charList, loading, error, offset, newItemLoading, charEnded } = this.state;
 
      const items = this.renderItems(charList);
 
      const errorMessage = error ? <ErrorMessage /> : null;
      const spinner = loading ? <Spinner /> : null;
      const content = !(loading || error) ? items : null;
 
      return (
         <div className='char__list'>
            {errorMessage}
            {spinner}
            {content}
            <button
               className='button button__main button__long'
               // кнопка заблокирована, если загружает что-либо
               disabled={newItemLoading}
               // при нажатии на кнопку будут подгружаться данные
               onClick={() => this.onRequest(offset)}
               style={{ display: charEnded ? 'none' : 'block' }}
            >
               <div className='inner'>load more</div>
            </button>
         </div>
      );
   }
}
 
export default CharList;

Ну и так же накинем на кнопку фильтр, чтобы пользователь понимал, что она некликабельна через :disabled

styles > button.scss

@import './variables';
 
.button {
    &:disabled {
        filter: grayscale(.5);
    }
}

Итог: у нас подгружаются новые персонажи и кнопка становится тёмной

009 Проверка типов с помощью PropTypes

PropTypes

Установка модуля:

npm i prop-types

И так выглядит самая простая реализация типизации пропсов:

import PropTypes from 'prop-types';
 
CharInfo.propTypes = {
   charId: PropTypes.number,
   onCharSelected: PropTypes.func.isRequired,
};

И если у нас будет свойство charId: PropTypes.string, то мы получим подобную ошибку

Проверка типов с помощью PropTypes

Примечание:

С версии React 15.5 React.PropTypes были вынесены в отдельный пакет. Так что используйте библиотеку prop-types.

Вы можете использовать codemod-скрипт, чтобы провести замену в коде на использование этой библиотеки.

По мере роста вашего приложения вы можете отловить много ошибок с помощью проверки типов. Для этого можно использовать расширения JavaScript вроде Flow и TypeScript. Но, даже если вы ими не пользуетесь, React предоставляет встроенные возможности для проверки типов. Для запуска этой проверки на пропсах компонента вам нужно использовать специальное свойство propTypes:

import PropTypes from 'prop-types';
 
class Greeting extends React.Component {
  render() {
    return (
      <h1>Привет, {this.props.name}</h1>
    );
  }
}
 
Greeting.propTypes = {
  name: PropTypes.string
};

В данном примере проверка типа показана на классовом компоненте, но она же может быть применена и к функциональным компонентам, или к компонентам, созданным с помощью React.memo или React.forwardRef.

PropTypes предоставляет ряд валидаторов, которые могут использоваться для проверки, что получаемые данные корректны. В примере мы использовали PropTypes.string. Когда какой-то проп имеет некорректное значение, в консоли будет выведено предупреждение. По соображениям производительности propTypes проверяются только в режиме разработки.

PropTypes

Пример использования возможных валидаторов:

import PropTypes from 'prop-types';
 
MyComponent.propTypes = {
  // Можно объявить проп на соответствие определённому JS-типу.
  // По умолчанию это не обязательно.
  optionalArray: PropTypes.array,
  optionalBool: PropTypes.bool,
  optionalFunc: PropTypes.func,
  optionalNumber: PropTypes.number,
  optionalObject: PropTypes.object,
  optionalString: PropTypes.string,
  optionalSymbol: PropTypes.symbol,
 
  // Все, что может быть отрендерено:
  // числа, строки, элементы или массивы
  // (или фрагменты) содержащие эти типы
  optionalNode: PropTypes.node,
 
  // React-элемент
  optionalElement: PropTypes.element,
 
  // Тип React-элемент (например, MyComponent).
  optionalElementType: PropTypes.elementType,
 
  // Можно указать, что проп должен быть экземпляром класса
  // Для этого используется JS-оператор instanceof.
  optionalMessage: PropTypes.instanceOf(Message),
 
  // Вы можете задать ограничение конкретными значениями
  // при помощи перечисления
  optionalEnum: PropTypes.oneOf(['News', 'Photos']),
 
  // Объект, одного из нескольких типов
  optionalUnion: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number,
    PropTypes.instanceOf(Message)
  ]),
 
  // Массив объектов конкретного типа
  optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
 
  // Объект со свойствами конкретного типа
  optionalObjectOf: PropTypes.objectOf(PropTypes.number),
 
  // Объект с определённой структурой
  optionalObjectWithShape: PropTypes.shape({
    color: PropTypes.string,
    fontSize: PropTypes.number
  }),
 
  // При наличии необъявленных свойств в объекте будут вызваны предупреждения
  optionalObjectWithStrictShape: PropTypes.exact({
    name: PropTypes.string,
    quantity: PropTypes.number
  }),
 
  // Можно добавить`isRequired` к любому приведённому выше типу,
  // чтобы показывать предупреждение,
  // если проп не передан
  requiredFunc: PropTypes.func.isRequired,
 
  // Обязательное значение любого типа
  requiredAny: PropTypes.any.isRequired,
 
  // Можно добавить собственный валидатор.
  // Он должен возвращать объект `Error` при ошибке валидации.
  // Не используйте `console.warn` или `throw`
  // - это не будет работать внутри `oneOfType`
  customProp: function(props, propName, componentName) {
    if (!/matchme/.test(props[propName])) {
      return new Error(
        'Проп `' + propName + '` компонента' +
        ' `' + componentName + '` имеет неправильное значение'
      );
    }
  },
 
  // Можно задать свой валидатор для `arrayOf` и `objectOf`.
  // Он должен возвращать объект Error при ошибке валидации.
  // Валидатор будет вызван для каждого элемента в массиве
  // или для каждого свойства объекта.
  // Первые два параметра валидатора
  // - это массив или объект и ключ текущего элемента
  customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
    if (!/matchme/.test(propValue[key])) {
      return new Error(
        'Проп `' + propFullName + '` компонента' +
        ' `' + componentName + '` имеет неправильное значение'
      );
    }
  })
};

Требование одного дочернего элемента

С помощью PropTypes.element вы можете указать, что только один дочерний элемент может быть передан компоненту в качестве потомка.

import PropTypes from 'prop-types';
 
class MyComponent extends React.Component {
  render() {
    // Это должен быть ровно один элемент, иначе вы увидите предупреждение.
    const children = this.props.children;
    return (
      <div>
        {children}
      </div>
    );
  }
}
 
MyComponent.propTypes = {
  children: PropTypes.element.isRequired
};

Значения пропсов по умолчанию

Вы можете задать значения по умолчанию для ваших props с помощью специального свойства defaultProps:

class Greeting extends React.Component {
  render() {
    return (
      <h1>Привет, {this.props.name}</h1>
    );
  }
}
 
// Задание значений по умолчанию для пропсов:
Greeting.defaultProps = {
  name: 'Незнакомец'
};
 
// Отрендерит "Привет, Незнакомец":
const root = ReactDOM.createRoot(document.getElementById('example'));
root.render(<Greeting />);

C ES2022 вы можете объявить defaultProps как статическое свойство внутри классового React компонента. Подробнее можно узнать в статье про публичные статические поля класса. Для поддержки этого современного синтаксиса в старых браузерах потребуется компиляция.

class Greeting extends React.Component {
  static defaultProps = {
    name: 'Незнакомец'
  }
 
  render() {
    return (
      <div>Привет, {this.props.name}</div>
    )
  }
}

Определение defaultProps гарантирует, что this.props.name будет иметь значение, даже если оно не было указано родительским компонентом. Сначала применяются значения по умолчанию, заданные в defaultProps. После запускается проверка типов с помощью propTypes. Так что проверка типов распространяется и на значения по умолчанию.

Функциональные компоненты

К функциональным компонентам можно также применять PropTypes.

Допустим, есть такой компонент:

export default function HelloWorldComponent({ name }) {
  return (
    <div>Hello, {name}</div>
  )
}

Для добавления PropTypes нужно объявить компонент в отдельной функции, которую затем экспортировать:

function HelloWorldComponent({ name }) {
  return (
    <div>Hello, {name}</div>
  )
}
 
export default HelloWorldComponent

А затем добавить PropTypes напрямую к компоненту HelloWorldComponent:

import PropTypes from 'prop-types'
 
function HelloWorldComponent({ name }) {
  return (
    <div>Hello, {name}</div>
  )
}
 
HelloWorldComponent.propTypes = {
  name: PropTypes.string
}
 
export default HelloWorldComponent

010 Вставка элементов через props.children

Пропс children в React содержит в себе все те элементы, что мы передали внутрь компонента между тегами <тег>ЭТО_CHILDREN</тег>

Создадим компонент DynamicGreating, который будет выводить элементы в определённой карточке. Внутри него мы будем выводить дочерние элементы через props.children. Далее вызовем этот компонент в App и обернём внутрь него определённые данные

Так же, если мы передаём несколько элементов в качестве ребёнка, то они образуют собой массив, для которого работают стандартные методы высшего порядка

map вызывается для каждого непосредственного потомка, содержащегося в children передавая их по очереди в thisArg. Если children — это массив, он будет пройден, и функция будет вызвана для каждого потомка в массиве. Если children равен null или undefined, этот метод вернёт null или undefined, а не массив.

React.Children.map(children, function[(thisArg)])

Метод cloneElement клонирует и возвращает новый React-элемент, используя элемент в качестве отправной точки. config должен содержать все новые пропсы, key, а также ref Полученный элемент будет иметь пропсы исходного элемента, а новые пропсы будут поверхностно слиты воедино. Новые дочерние элементы заменят существующие. key и ref из исходного элемента будут сохранены, если в config не было передано key и ref.

React.cloneElement(
  element,
  [config],
  [...children]
)

Первый подход Тут будет пройден массив из всех переданных элементов внутрь компонента DynamicGreating и на каждого ребёнка будут повешены классы стилей

Второй подход Мы так же можем заранее вписать в определённую заготовку нужные нам пропсы, которые она будет принимать

И далее просто пропсами передаём вёрстку

И на выходе мы получим данную переданную вёрстку, отображённую внутри заготовки

011 Специализация и наследование

Наследование в React у нас обычно происходит от Component для классов

Композиция же предполагает под собой специализирование компонента за счёт использования другого компонента. Тут мы из компонента динамического приветствия реализовали специфическое HelloGreating

Что лучше использовать: композицию или наследование?

  • В React реализованы все инструменты для удобного использования композиции (когда мы специфицируем один компонент за счёт использования его внутри другого).
  • Сами разработчики React не находили надобности в развитии наследования компонентов

012 Render-props паттерн

Обычно, чтобы передавать состояние из одного компонента в другой, мы пользуемся таким подходом:

Но иногда нам может понадобиться логика определённого компонента сразу в нескольких местах, что заставит нас переделывать один и тот же компонент

Чтобы можно было воспользоваться логикой одного компонента внутри другого компонента и не делать жёсткую вёрстку, можно воспользоваться следующим подходом:

// это функция, которая выводит сообщение с определённым каунтером
const Message = (props) => {
    return (
        <h2>The counter is {props.counter}</h2>
    )
}
 
// Это сам каунтер с кнопкой
class Counter extends Component {
    state = {
        counter: 0
    }
 
    changeCounter = () => {
        this.setState(({counter}) => ({
            counter: counter + 1
        }))
    }
 
    render() {
        return (
            <>
                <button
                    className={'btn btn-primary'}
                    onClick={this.changeCounter}>
                    Click me
                </button>
 
                {/* тут мы вызваем рендер одного компонента внутри другого и передаём в него нужный пропс */}
                {this.props.render(this.state.counter)}
            </>
        )
    }
}
 
function App() {
  return (
    <Wrapper>
 
		{/* тут мы используем подход, когда независимый компонент передаёт состояние в другой независимый компонент */}
        <Counter render={counter => (
            <Message counter={counter}/>
        )}/>
 
        <HelloGreating/>
        <BootstrapTest
            left = {
                <DynamicGreating color={'primary'}>
                    <h2>This weel was hard</h2>
                    <h2>Hello world!</h2>
                </DynamicGreating>
            }
            right = {
                <DynamicGreating color={'primary'}>
                    <h2>RIGHT!</h2>
                </DynamicGreating>
            }
        />
 
        <WhoAmI name='John' surname="Smith" link="facebook.com"/>
        <WhoAmI name='Alex' surname="Shepard" link="vk.com"/>
    </Wrapper>
  );
}
 
export default App;

Такой подход называется Render-props, когда мы передаём внутрь одного компонента в качестве пропса другой компонент на отрисовку

  • При таком подходе мы передаём в компонент функцию, которая при вызове возвращает вёрстку и на вход принимает аргументы от родителя

013 Что такое ref и зачем он нужен

Стартовый проект:

import React, { Component } from 'react';
import { Container } from 'react-bootstrap';
import './App.css';
 
class Form extends Component {
	constructor(props) {
		super(props);
	}
 
	render() {
		return (
			<Container>
				<form className='w-50 border mt-5 p-3 m-auto'>
					<div className='mb-3'>
						<label htmlFor='exampleFormControlInput1' className='form-label'>
							Email address
						</label>
						<input
							type='email'
							className='form-control'
							id='exampleFormControlInput1'
							placeholder='name@example.com'
						/>
					</div>
					<div className='mb-3'>
						<label htmlFor='exampleFormControlTextarea1' className='form-label'>
							Example textarea
						</label>
						<textarea
							className='form-control'
							id='exampleFormControlTextarea1'
							rows='3'
						></textarea>
					</div>
				</form>
			</Container>
		);
	}
}
 
function App() {
	return <Form />;
}
 
export default App;

ref - это ссылка на элемент вёрстки из ДОМ-дерева на странице

Чтобы создать ссылку в классовом компоненте, нужно вызвать через React функцию createRef(). Далее на нужный нам элемент навешиваем данный реф.

Так же можно рефы указывать в полях класса

И далее мы можем воспользоваться стандартными функциями API браузера на элементах вёрстки

И сейчас можно увидеть, что фокус срабатывает на форме

Если нам нужно будет навесить ref на компонент React, то тут уже придётся проделать определённые манипуляции

И теперь, чтобы мы смогли воспользоваться рефом компонента у нас есть два пути:

  • обернуть функциональный компонент в forwardRef и прокинуть вторым аргументом ref в нужное нам поле компонента
  • можно просто сделать компонент классовым

Но проблема теперь будет заключаться в том, что мы не сможем воспользоваться стандартным API и вызвать focus(), так как он вызывается сейчас на компоненте, а не на элементе ДОМ-дерева. Однако мы можем вызвать функции самого компонента, поэтому можно будет вызывать фокус в самом компоненте.

Так же мы имеем такой подход как коллбэк-реф. Это подход, при котором мы должны навешивать реф к элементу через функцию. Так же тут нужно сказать, что у нас будет отсутствовать свойство current и ref будет хранить чистую ссылку на элемент вне объекта

И далее реализуем выделение карточки персонажа через клавиатуру и навешивание на него стилей

src > components > charList > CharList.js

class CharList extends Component {
   state = {
      charList: [],
      loading: true,
      error: false,
      newItemLoading: false,
      offset: 210,
      charEnded: false,
   };
 
   marvelService = new MarvelService();
 
   componentDidMount() {
      this.onRequest();
   }
 
   onRequest = (offset) => {
      this.onCharListLoading();
      this.marvelService.getAllCharacters(offset)
	      .then(this.onCharListLoaded)
	      .catch(this.onError);
   };
 
   onCharListLoading = () => {
      this.setState({
         newItemLoading: true,
      });
   };
 
   onCharListLoaded = (newCharList) => {
      let ended = false;
      if (newCharList.length < 9) {
         ended = true;
      }
 
      this.setState(({ offset, charList }) => ({
         charList: [...charList, ...newCharList],
         loading: false,
         newItemLoading: false,
         offset: offset + 9,
         charEnded: ended,
      }));
   };
 
   onError = () => {
      this.setState({
         error: true,
         loading: false,
      });
   };
 
   // массив элементов ссылок
   itemRefs = [];
 
   // функция установки рефа
   setRef = (ref) => {
      this.itemRefs.push(ref);
   };
 
   // при фокусе на элемент
   focusOnItem = (id) => {
      // Я реализовал вариант чуть сложнее, и с классом и с фокусом
      // Но в теории можно оставить только фокус, и его в стилях использовать вместо класса
      // На самом деле, решение с css-классом можно сделать, вынеся персонажа в отдельный компонент. Но кода будет больше, появится новое состояние и не факт, что мы выиграем по оптимизации за счет бОльшего кол-ва элементов
      // По возможности, не нужно злоупотреблять рефами
      this.itemRefs.forEach((item) => item.classList.remove('char__item_selected'));
      this.itemRefs[id].classList.add('char__item_selected');
      this.itemRefs[id].focus();
   };
 
   renderItems(arr) {
      const items = arr.map((item, i) => {
         let imgStyle = { objectFit: 'cover' };
         if (
            item.thumbnail ===
            'http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available.jpg'
         ) {
            imgStyle = { objectFit: 'unset' };
         }
 
         return (
            <li
               className='char__item'
               // делаем элемент кликабельным
               tabIndex={0}
               // устанавливаем его в массив рефов через метод
               ref={this.setRef}
               key={item.id}
               // при клике на элемент вызваем функции
               onClick={() => {
                  this.props.onCharSelected(item.id);
                  // добавляем функцию фокуса
                  this.focusOnItem(i);
               }}
               // и те же функции будем вызывать при нажатии на кнопку
               onKeyDown={(e) => {
                  if (e.key === ' ' || e.key === 'Enter') {
                     this.props.onCharSelected(item.id);
                     this.focusOnItem(i);
                  }
               }}
            >
	               <img src={item.thumbnail} alt={item.name} style={imgStyle} />
               <div className='char__name'>{item.name}</div>
            </li>
         );
      });
 
		// тут уже выводим все элементы списка
		return <ul className='char__grid'>{items}</ul>;
   }
 
   render() {
      const { charList, loading, error, offset, newItemLoading, charEnded } = this.state;
 
      const items = this.renderItems(charList);
 
      const errorMessage = error ? <ErrorMessage /> : null;
      const spinner = loading ? <Spinner /> : null;
      const content = !(loading || error) ? items : null;
 
      return (
         <div className='char__list'>
            {errorMessage}
            {spinner}
            {content}
            <button
               className='button button__main button__long'
               disabled={newItemLoading}
               style={{ display: charEnded ? 'none' : 'block' }}
               onClick={() => this.onRequest(offset)}
            >
               <div className='inner'>load more</div>
            </button>
         </div>
      );
   }
}

Итог: мы имеем выделение персонажей при клике

014 Порталы

Так выглядит стартовое приложение:

import React, { Component } from 'react';
import { Container } from 'react-bootstrap';
import './App.css';
 
class Form extends Component {
	render() {
		return (
			<Container>
				<form
					className='w-50 border mt-5 p-3 m-auto'
					style={{ overflow: 'hidden', position: 'relative' }}
				>
					<div className='mb-3'>
						<label htmlFor='exampleFormControlInput1' className='form-label'>
							Email address
						</label>
						<input
							type='email'
							className='form-control'
							id='exampleFormControlInput1'
							placeholder='name@example.com'
						/>
					</div>
					<div className='mb-3'>
						<label htmlFor='exampleFormControlTextarea1' className='form-label'>
							Example textarea
						</label>
						<textarea
							className='form-control'
							id='exampleFormControlTextarea1'
							rows='3'
						></textarea>
					</div>
					<div
						style={{
							width: '500px',
							height: '150px',
							backgroundColor: 'red',
							position: 'absolute',
							right: '-50%',
							bottom: '-50%',
						}}
					>
						Hello
					</div>
				</form>
			</Container>
		);
	}
}
 
function App() {
	return <Form />;
}
 
export default App;

Порталы позволяют отрендерить определённые элементы вне своего родительского компонента

Конкретно сейчас мы имеем выскакивающее модальное окно, которое скрывается внутри нашего компонента

Вынесем модальное окно в отдельный компонент

И далее реализуем портал:

  • он принимает в себя пропс
  • далее мы создаём ноду, в которой он отрендерится
  • добавляем ноду в тело страницы
  • возвращаем созданный портал через метод createPortal, который в себя принимает вёрстку (children) и место вставки (node)

И добавляем портал в вёрстку родителя

Таким образом модальное окно отрендерилось внутри своего компонента, но в другом месте, отдельно от родительского компонента

Так же тут нужно упомянуть, что событие, которое было сгенерировано изнутри портала будет распространяться и на родителя

То есть, если мы на родителя повесим событие onClick и внутри этого же родителя находится портал, то метод будет срабатывать и на дочернем элементе

015 “Бандлинг” и выгрузка проекта на сервер

Для выгрузки сайта на сервер, нужно две вещи:

  • Имя сайта - домен
  • Место хранения сайта - хостинг

Хостинг для обычного сайта подойдёт любой. Однако, если мы собираемся заливать веб-приложения, то нам нужны WPS-сервера, которые выделяют нам отдельный виртуальный ПК.

Но чтобы что-то выгрузить на сервер, нужно сначала собрать проект.

npm run eject - позволит получить доступ к настройкам вебпака в create-react-app. Эта операция необратима, поэтому нужно использовать её только в крайних случаях.

npm run build - сбилдит проект, который можно будет выгрузить на сервер

И сейчас мы можем попробовать открыть сбилженный проект

npm i -g http-server
http-server build/

Весь процесс деплоя сайта на сервер можно просмотреть на сайте документации CRA

Чтобы залить приложение на сервер, перейдём в heroku

далее запишем домен

И далее мы можем залить на хироку сайт прямо с гитхаба или через утилиту

И тут нужно оставить примечание, что все зависимости в нашем проекте должны стоять в нужных местах, и если они нужны для работы сайта, то их нужно перенести в dependencies

Устанавливаем утилиту и со страницы деплоя берём команду

И тут мы можем увидеть статус деплоя нашего приложения

И сейчас мы имеем доступ к нашему сайту с хироку

Библиотека React. Хуки и средний продвинутый уровень

React ReactHooks

001 Введение в хуки

Хуки - это функции, которые позволяют заменить функционал реакта в классах для использования внутри функциональных компонентов

Хуки появились в версии 16.8

Пока не существует хуков, реализующих методы жизненного цикла getSnapshotBeforeUpdategetDerivedStateFromError и componentDidCatch.

2 правила использования хуков:

  • Хуки можно вызывать только на верхнем уровне - нельзя использовать внутри циклов, условий и вложенных функций
  • Хуки стоит вызывать только из функциональных компонентов реакта (исключение только одно - это пользовательские хуки)

002 useState

И далее представим два компонента, которые будут рендерить одну и ту же картинку, но будут иметь два разных подхода реализации: функциональный и классовый

Для построения функционального компонента нужно уже будет писать все состояния используя хуки. Конкретно в данном случае пригодится useState()

useState() - это хук, который отвечает за управлением состояниями в приложении. Он возвращает массив из двух элементов: [состояние, функцияУстановкиСостояния]. Для установки нового состояния нельзя мутировать старое и поэтому в функцию нужно передавать стейт + новое изменённое значение

import { Component, useState } from 'react';
import { Container } from 'react-bootstrap';
import './App.css';
 
// классовая версия компонента
class SliderClass extends Component {
	constructor(props) {
		super(props);
		this.state = {
			autoplay: false,
			slide: 0,
		};
	}
 
	changeSlide = (i) => {
		this.setState(({ slide }) => ({
			slide: slide + i,
		}));
	};
 
	toggleAutoplay = () => {
		this.setState(({ autoplay }) => ({
			autoplay: !autoplay,
		}));
	};
 
	render() {
		return (
			<Container className='wrapper'>
				<div className='slider w-50 m-auto'>
					<img
						className='d-block w-100'
						src='https://www.planetware.com/wpimages/2020/02/france-in-pictures-beautiful-places-to-photograph-eiffel-tower.jpg'
						alt='slide'
					/>
					<div className='text-center mt-5'>
						Active slide {this.state.slide} <br /> {this.state.autoplay ? 'auto' : null}
					</div>
					<div className='buttons mt-3'>
						<button
							className='btn btn-primary me-2'
							onClick={() => this.changeSlide(-1)}
						>
							-1
						</button>
						<button
							className='btn btn-primary me-2'
							onClick={() => this.changeSlide(1)}
						>
							+1
						</button>
						<button className='btn btn-primary me-2' onClick={this.toggleAutoplay}>
							toggle autoplay
						</button>
					</div>
				</div>
			</Container>
		);
	}
}
 
// функциональная версия компонента
const SliderFunction = (props) => {
	// первая возможная запись элемента состояния
	const slideStateArray = useState(0);
	console.log(slideStateArray);
 
	// вторая запись, которую мы сразу деструктурируем
	const [slide, setSlide] = useState(0);
 
	// так же можно вынести изменение состояния в отдельную функцию
	function changeSlide(i) {
		setSlide((slide) => slide + i);
	}
 
	const [autoplay, setAutoplay] = useState(false);
 
	function toggleAutoplay() {
		// так же функция изменения значения может выглядить таким образом
		setAutoplay((autoplay) => !autoplay);
	}
 
	return (
		<Container className='wrapper'>
			<div className='slider w-50 m-auto'>
				<img
					className='d-block w-100'
					src='https://www.planetware.com/wpimages/2020/02/france-in-pictures-beautiful-places-to-photograph-eiffel-tower.jpg'
					alt='slide'
				/>
				<div className='text-center mt-5'>
					Active slide {slide} <br /> {autoplay ? 'auto' : null}
				</div>
				<div className='buttons mt-3'>
					<button
						className='btn btn-primary me-2'
						// изменяем состояние слайдера через внешнюю
						onClick={() => changeSlide(-1)}
					>
						-1
					</button>
					<button
						className='btn btn-primary me-2'
			// изменяем состояние слайдера через встроенную функцию
						onClick={() => setSlide(slide + 1)}
					>
						+1
					</button>
					<button
						className='btn btn-primary me-2'
						// изменяем состояние на обратное
						onClick={() => setAutoplay(!autoplay)}
					>
						toggle autoplay
					</button>
				</div>
			</div>
		</Container>
	);
};
 
function App() {
	return (
		<div>
			<SliderClass />
			<SliderFunction />
		</div>
	);
}
 
export default App;

И далее мы имеем два одинаковых компонента на странице:

  • классовый
  • функциональный

Вот как выглядит возврат useState()

Если мы попытаемся вызывать функцию установки нового состояния дважды просто через передачу внутрь аргумента, у нас сработают обе функции асинхронно. Это приведёт к тому, что значение состояния будет меняться ровно один раз, потому что оба аргумента (состояние) функции ссылаются на одно и то же значение

То есть тут произойдёт увеличение состояния slide ровно на 1

function changeSlide(i) {
	setSlide(slide + i);
	setSlide(slide + i);
}

Однако при таком подходе, когда мы изменяем состояние через колбэк-функцию, стейт будет меняться два раза подряд и при передаче аргумента 1 стейт увеличится на 2

function changeSlide(i) {
	setSlide((slide) => slide + i);
	setSlide((slide) => slide + i);
}

Так же мы можем вынести несколько состояний в одну переменную и хранить в ней объект с несколькими значениями. Особенность заключается в том, что в отличие от классов объекты автоматически не складываются

  • this.setState(({ slide }) => ({ slide: slide + i })) - в классах будет работать и свойство autoplay не потеряется
  • setState((state) => ({ ...state, slide: state.slide + value })) - нужно использовать в функциях деструктуризацию, потому что состояния в них иммутабельны и нужно вставлять полностью новые значения
const [state, setState] = useState({ slide: 0, autoplay: false });
 
function changeSlide(value) {
	setState((state) => ({ ...state, slide: state.slide + value }));
}
 
function toggleAutoplay() {
	setState((state) => ({ ...state, autoplay: !state.autoplay }));
}

Так же, если мы передадим в качестве начального значения состояния функцию, то она вызовется ровно один раз - при сборке компонента

Так же будет себя вести функция, если мы передадим колбэк-функцию

Если функцию просто вызвать внутри установки стейта, то она будет вызваться каждый раз при перерендере

003 useEffect

useEffect() - это хук, который выполняет эффекты на определённых этапах жизненного состояния компонента

Побочными действиями (эффектами) обычно являются:

  • дозагрузка данных
  • использования сторонних модулей
  • запуск таймаутов
  • логирование
  • изменение ДОМ-структуры

И далее нам нужно обновлять заголовок страницы в зависимости от состояния слайда

И тут показан пример использования хуков жизненного состояния componentDidMount и componentDidUpdate в классовом компоненте. Основная проблема такого подхода заключается в повторении кода.

И в функциональном компоненте эту же самую операцию выполняет одна функция useEffect()

import { Component, useEffect, useState } from 'react';
import { Container } from 'react-bootstrap';
import './App.css';
 
class SliderClass extends Component {
	constructor(props) {
		super(props);
		this.state = {
			autoplay: false,
			slide: 0,
		};
	}
 
	// при монтировании компонента, будет показываться элемент
	componentDidMount() {
		document.title = `Slide: ${this.state.slide}`;
	}
 
	// при обновлении компонента, будет показываться слайд
	componentDidUpdate() {
		document.title = `Slide: ${this.state.slide}`;
	}
 
	changeSlide = (i) => {
		this.setState(({ slide }) => ({
			slide: slide + i,
		}));
	};
 
	toggleAutoplay = () => {
		this.setState(({ autoplay }) => ({
			autoplay: !autoplay,
		}));
	};
 
	render() {
		return (
			<Container className='wrapper'>
				<div className='slider w-50 m-auto'>
					<img
						className='d-block w-100'
						src='https://www.planetware.com/wpimages/2020/02/france-in-pictures-beautiful-places-to-photograph-eiffel-tower.jpg'
						alt='slide'
					/>
					<div className='text-center mt-5'>
						Active slide {this.state.slide} <br /> {this.state.autoplay ? 'auto' : null}
					</div>
					<div className='buttons mt-3'>
						<button
							className='btn btn-primary me-2'
							onClick={() => this.changeSlide(-1)}
						>
							-1
						</button>
						<button
							className='btn btn-primary me-2'
							onClick={() => this.changeSlide(1)}
						>
							+1
						</button>
						<button className='btn btn-primary me-2' onClick={this.toggleAutoplay}>
							toggle autoplay
						</button>
					</div>
				</div>
			</Container>
		);
	}
}
 
const SliderFunction = (props) => {
	const [slide, setSlide] = useState(0);
	const [autoplay, setAutoplay] = useState(false);
 
	// и сейчас мы будем каждый раз вызывать перерендер данного элемента
	useEffect(() => {
		document.title = `Slide: ${slide}`;
	});
 
	function changeSlide(i) {
		setSlide((slide) => slide + i);
	}
 
	function toggleAutoplay() {
		setAutoplay((autoplay) => !autoplay);
	}
 
	return (
		<Container className='wrapper'>
			<div className='slider w-50 m-auto'>
				<img
					className='d-block w-100'
					src='https://www.planetware.com/wpimages/2020/02/france-in-pictures-beautiful-places-to-photograph-eiffel-tower.jpg'
					alt='slide'
				/>
				<div className='text-center mt-5'>
					Active slide {slide} <br /> {autoplay ? 'auto' : null}
				</div>
				<div className='buttons mt-3'>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(-1)}>
						-1
					</button>
					<button className='btn btn-primary me-2' onClick={() => setSlide(slide + 1)}>
						+1
					</button>
					<button className='btn btn-primary me-2' onClick={toggleAutoplay}>
						toggle autoplay
					</button>
				</div>
			</div>
		</Container>
	);
};
 
function App() {
	return (
		<div>
			<SliderClass />
			<SliderFunction />
		</div>
	);
}
 
export default App;

И теперь оба компонента выполняют свои функции одинаково

И так же нужно сказать про разные состояния работы хука useEffect

При такой записи эффект будет выполняться каждый раз при обновлении компонента. Дело заключается в том, что так функция будет работать как componentDidMount и функция будет меняться при каждом рендере, то есть каждый раз будет создаваться новая функция (которая передаётся в useEffect). Реализован так хук, чтобы не было багов с замыканием, чтобы получать актуальную переменную из состояния.

Такая запись хука будет вызвать срабатывание функции при любом изменении стейта на странице, что не очень хороший подход, так как у нас есть и другие стейты на странице, которые не относятся к выполнению данной функции

Чтобы исправить проблему выше, можно передать второй аргумент в хук - зависимости, которые будут триггерить срабатывание функции

И теперь вызываться функция будет только при изменении целевого состояния

Если нам нужно сэмулировать работу функции componentDidMount, тогда нам нужно передать пустой массив зависимостей, что вызовет срабатывание функции только один раз - при загрузке

Так же мы можем создать несколько хуков useEffect.Желательно создавать отдельные эффекты на каждое действие.

И далее рассмотрим поведение, когда нам нужно реализовать поведение componentWillUnmount, когда при размонтировании компонента нам нужно произвести все отписки (отключить листенеры и таймауты)

Чтобы выполнить данную операцию, нужно просто из эффекта вернуть другую функцию, которая выполнит заданную операцию при размонтировании

Добавим в родительский компонент возможность удалить компонент со страницы

function App() {
	const [slide, setSlide] = useState(true);
 
	return (
		<>
			<button onClick={() => setSlide(!slide)}>Слайдер</button>
			{/* <SliderClass /> */}
			{slide ? <SliderFunction /> : null}
		</>
	);
}

И теперь при каждом монтировании компонента появляется уведомление

004 useCallback

Представим такую ситуацию: нам нужно получать изображения со стороннего ресурса

// функция получения изображений (вне компонента)
const getSomeImg = () => {
	console.log('fetching');
 
	return [
		'https://www.planetware.com/i/home-promo-italy.jpg',
		'https://www.planetware.com/wpimages/2023/02/scotland-isle-of-arran-top-things-to-do-intro-paragraph-goat-fell.jpg',
	];
};
 
// return компонента
return (
	<Container className='wrapper'>
		<div className='slider w-50 m-auto'>
 
			// формируем массив изображений
			{getSomeImg().map((url, i) => (
				<img key={i} className='d-block w-100' src={url} alt='slide' />
			))}
 
			<div className='text-center mt-5'>
				Active slide {slide} <br /> {autoplay ? 'auto' : null}
			</div>
			<div className='buttons mt-3'>
				<button className='btn btn-primary me-2' onClick={() => changeSlide(-1)}>
					-1
				</button>
				<button className='btn btn-primary me-2' onClick={() => setSlide(slide + 1)}>
					+1
				</button>
				<button className='btn btn-primary me-2' onClick={toggleAutoplay}>
					toggle autoplay
				</button>
			</div>
		</div>
	</Container>
);

Но теперь перед нами встаёт проблема, что после каждого изменения стейта, у нас вызывается функция отправки запроса на сервер

И тут на помощь к нам приходит хук useCallback(). Он принимает в себя функцию и мемоизирует ссылку на неё. Функция будет вызываться заново только после того, как у нас поменяется значение зависимости.

Чтобы правильно воспользоваться функцией, нужно создать новый компонент, который будет иметь своё состояние. Внутри неё нужно и отображать те изменения, данные для которых возвращает useCallback().

Конкретно тут мы через useCallback реализовали возврат новой ссылки на функцию, если изменится состояние слайда (если слайд не будет меняться, то ссылаться хук будет на старую версию функции, которая закэширована)

const SliderFunction = (props) => {
	const [slide, setSlide] = useState(0);
 
	useEffect(() => {
		document.title = `Slide: ${slide}`;
	}, [slide]);
 
	// чтобы закешировать выполнение функции, нужно обернуть его в хук
	const getSomeImg = useCallback(() => {
		console.log('fetching');
 
		return [
			'https://www.planetware.com/i/home-promo-italy.jpg',
			'https://www.planetware.com/wpimages/2023/02/scotland-isle-of-arran-top-things-to-do-intro-paragraph-goat-fell.jpg',
		];
	}, [slide]);
 
	const [autoplay, setAutoplay] = useState(false);
 
	function changeSlide(i) {
		setSlide((slide) => slide + i);
	}
 
	function toggleAutoplay() {
		setAutoplay((autoplay) => !autoplay);
	}
 
	return (
		<Container className='wrapper'>
			<div className='slider w-50 m-auto'>
 
				{/* тут уже просто вызываем наш компонент слайдов */}
				<Slide getSomeImg={getSomeImg} />
 
				<div className='text-center mt-5'>
					Active slide {slide} <br /> {autoplay ? 'auto' : null}
				</div>
				<div className='buttons mt-3'>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(-1)}>
						-1
					</button>
					<button className='btn btn-primary me-2' onClick={() => setSlide(slide + 1)}>
						+1
					</button>
					<button className='btn btn-primary me-2' onClick={toggleAutoplay}>
						toggle autoplay
					</button>
				</div>
			</div>
		</Container>
	);
};
 
// компонент отдельного слайда
const Slide = ({ getSomeImg }) => {
	const [images, setImages] = useState([]);
 
	useEffect(() => {
		setImages(getSomeImg());
	}, [getSomeImg]);
 
	return (
		<div>
			{images.map((url, i) => (
				<img key={i} className='d-block w-100' src={url} alt='slide' />
			))}
		</div>
	);
};

И теперь каждый раз мы будем вызвать мемоизированную функцию. При перезагрузке страницы те изображения, которые возвращает функция, будут закешированы в браузере.

005 useMemo

useMemo() - это хук, который возвращает мемоизированное значение (результат вычислений запоминается в кеше)

И далее мы реализовали функцию подсчёта суммарного количества слайдов через countTotal()

// функция подсчёта общего количества слайдов
const countTotal = (num) => {
	console.log('counting...');
	return num + 10;
};
 
const SliderFunction = (props) => {
	const [slide, setSlide] = useState(0);
	const [autoplay, setAutoplay] = useState(false);
 
	useEffect(() => {
		document.title = `Slide: ${slide}`;
	}, [slide]);
 
	const getSomeImg = useCallback(() => {
		return [
			'https://www.planetware.com/i/home-promo-italy.jpg',
			'https://www.planetware.com/wpimages/2023/02/scotland-isle-of-arran-top-things-to-do-intro-paragraph-goat-fell.jpg',
		];
	}, [slide]);
 
	// вызываем функцию подсчёта слайдов
	const total = countTotal(slide);
 
	return (
		<Container className='wrapper'>
			<div className='slider w-50 m-auto'>
				<Slide getSomeImg={getSomeImg} />
 
				<div className='text-center mt-5'>
					Active slide {slide} <br /> {autoplay ? 'auto' : null}
				</div>
 
				<div className='text-center mt-5'>Total slides: {total}</div>
 
				<div className='buttons mt-3'>
					<button className='btn btn-primary me-2' onClick={() => setSlide(slide - 1)}>
						-1
					</button>
					<button className='btn btn-primary me-2' onClick={() => setSlide(slide + 1)}>
						+1
					</button>
					<button className='btn btn-primary me-2' onClick={() => setAutoplay(!autoplay)}>
						toggle autoplay
					</button>
				</div>
			</div>
		</Container>
	);
};

Основная проблема, которую мы сейчас имеем - функция вызывается ещё раз даже когда мы мы меняем совсем не связанное с ней состояние. Если операция подсчёта была бы очень ресурсозатратной, то производительность бы сильно упала

И чтобы исправить вышеописанную ситуацию, можно воспользоваться хуком useMemo(). Этот хук принимает первым аргументом функцию, а вторым аргументом зависимости, при изменении которых будет пересчитываться значение.

Данный хук используется для запоминания значения, которое возвращает функция, чтобы не выполнять тяжёлые операции пересчёта каждый раз.

Если передать пустой массив зависимостей, то значение посчитается ровно один раз.

Представим такую ситуацию, что нам нужно менять стили при определённых условиях у объекта. Если выводить в консоль уведомление об изменении стилей, то можно увидеть, что при смене любого состояния у нас переприсваивается значение свойств в компоненте. Это происходит потому, что при обновлении компонента у нас будет в переменную style заноситься новый объект, от чего и будет срабатывать useEffect.

Чтобы исправить данную ситуацию, нужно занести объект в useMemo, что сохранит значение переменной в кеше браузера и внутри компонента объект не будет обновляться каждый раз и не будет вызвать срабатывание хука useEffect

006 useRef

useRef() - это хук, который предоставляет прямой доступ к ДОМ-элементам на странице

  1. Создаём переменную, которая будет хранить ссылку на нужный нам элемент
  2. Передаём реф в элемент ДОМ-дерева
  3. Вызываем срабатывание функции

Далее представленный ниже код выполняет:

  • при изменении состояния (внутри первого инпута) useEffect выводит в консоль значение свойства current у рефа
  • при клике по текстэрии, значение в рефе будет увеличиваться на 1
  • при дальнейшем вводе в первый инпут будет выводиться значение рефа, которое уже было увеличено

Таким образом мы сохранили динамическое значение внутри свойства current, которое будет изменяться без перерендера компонента

const Form = () => {
	const [text, setText] = useState('');
 
	const myRef = useRef(1);
 
	// отображаем значение рефа в консоли
	useEffect(() => console.log(myRef.current));
 
	return (
		<Container>
			<form className='w-50 border mt-5 p-3 m-auto'>
				<div className='mb-3'>
					<label htmlFor='exampleFormControlInput1' className='form-label'>
						Email address
					</label>
					<input
						// заносим текст в состояние
						onChange={(e) => setText(e.target.value)}
						type='email'
						className='form-control'
						id='exampleFormControlInput1'
						placeholder='name@example.com'
					/>
				</div>
				<div className='mb-3'>
					<label htmlFor='exampleFormControlTextarea1' className='form-label'>
						Example textarea
					</label>
					<textarea
						// увеличиваем значение на 1
						onClick={() => myRef.current++}
						className='form-control'
						id='exampleFormControlTextarea1'
						rows='3'
					></textarea>
				</div>
			</form>
		</Container>
	);
};

И при вынесении изменения свойства в эффект, можно увидеть, что компонент не перерендеривается каждый раз, а просто увеличивает свойство, находящееся в рефе

const Form = () => {
	const [text, setText] = useState('');
 
	const myRef = useRef(1);
 
	useEffect(() => {
		myRef.current++; // обновление свойства происходит тут
		console.log(myRef.current);
	});
 
	return (
		<Container>
			<form className='w-50 border mt-5 p-3 m-auto'>
				<div className='mb-3'>
					<label htmlFor='exampleFormControlInput1' className='form-label'>
						Email address
					</label>
					<input
						onChange={(e) => setText(e.target.value)}
						type='email'
						className='form-control'
						id='exampleFormControlInput1'
						placeholder='name@example.com'
					/>
				</div>
				<div className='mb-3'>
					<label htmlFor='exampleFormControlTextarea1' className='form-label'>
						Example textarea
					</label>
					<textarea
						className='form-control'
						id='exampleFormControlTextarea1'
						rows='3'
					></textarea>
				</div>
			</form>
		</Container>
	);
};

Так же мы можем использовать реф для сохранения предыдущего состояния компонента. Конкретно тут после записи в первый input его прошлое состояние переносится в textarea

const Form = () => {
	const [text, setText] = useState('');
 
	const myRef = useRef(1);
 
	useEffect(() => {
		myRef.current = text;
	});
 
	return (
		<Container>
			<form className='w-50 border mt-5 p-3 m-auto'>
				<div className='mb-3'>
					<label htmlFor='exampleFormControlInput1' className='form-label'>
						Email address
					</label>
					<input
						onChange={(e) => setText(e.target.value)}
						type='email'
						className='form-control'
						id='exampleFormControlInput1'
						placeholder='name@example.com'
					/>
				</div>
				<div className='mb-3'>
					<label htmlFor='exampleFormControlTextarea1' className='form-label'>
						Example textarea
					</label>
					<textarea
						value={myRef.current}
						className='form-control'
						id='exampleFormControlTextarea1'
						rows='3'
					></textarea>
				</div>
			</form>
		</Container>
	);
};

007 Практика. Перепишем весь проект на хуки

Далее можно попробовать перевести весь старый проект с классов на хуки

Основной компонент персонажа:

components > app > App.js

const App = () => {
	const [selectedChar, setSelectedChar] = useState(null);
 
	const onCharSelected = (id) => {
		setSelectedChar(id);
	};
 
	return (
		<div className='app'>
			<AppHeader />
			<main>
				<ErrorBoundary>
					<RandomChar />
				</ErrorBoundary>
				<div className='char__content'>
					<ErrorBoundary>
						<CharList onCharSelected={onCharSelected} />
					</ErrorBoundary>
					<ErrorBoundary>
						<CharInfo charId={selectedChar} />
					</ErrorBoundary>
				</div>
				<img className='bg-decoration' src={decoration} alt='vision' />
			</main>
		</div>
	);
};

components > CharList > CharList.js

const CharList = ({ onCharSelected }) => {
	const [charList, setCharList] = useState([]);
	const [loading, setLoading] = useState(true);
	const [error, setError] = useState(false);
	const [newItemLoading, setNewItemLoading] = useState(false);
	const [offset, setOffset] = useState(210);
	const [charEnded, setCharEnded] = useState(false);
 
	const marvelService = new MarvelService();
 
	useEffect(() => {
		onRequest();
	}, []);
 
	const onRequest = (offset) => {
		onCharListLoading();
		marvelService.getAllCharacters(offset).then(onCharListLoaded).catch(onError);
	};
 
	const onCharListLoading = () => {
		setNewItemLoading(true);
	};
 
	const onCharListLoaded = (newCharList) => {
		let ended = false;
		if (newCharList.length < 9) {
			ended = true;
		}
 
		setCharList((charList) => [...charList, ...newCharList]);
		setLoading(false);
		setNewItemLoading((newItemLoading) => false);
		setOffset((offset) => offset + 9);
		setCharEnded(ended);
	};
 
	const onError = () => {
		setError(true);
		setLoading(false);
	};
 
	const itemRefs = useRef([]);
 
	const focusOnItem = (id) => {
		itemRefs.current.forEach((item) => item.classList.remove('char__item_selected'));
		itemRefs.current[id].classList.add('char__item_selected');
		itemRefs.current[id].focus();
	};
 
	const renderItems = (arr) => {
		const items = arr.map((item, i) => {
			let imgStyle = { objectFit: 'cover' };
			if (
				item.thumbnail ===
				'http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available.jpg'
			) {
				imgStyle = { objectFit: 'unset' };
			}
 
			return (
				<li
					className='char__item'
					tabIndex={0}
					ref={(element) => (itemRefs.current[i] = element)}
					key={item.id}
					onClick={() => {
						onCharSelected(item.id);
						focusOnItem(i);
					}}
					onKeyDown={(e) => {
						if (e.key === ' ' || e.key === 'Enter') {
							onCharSelected(item.id);
							focusOnItem(i);
						}
					}}
				>
					<img src={item.thumbnail} alt={item.name} style={imgStyle} />
					<div className='char__name'>{item.name}</div>
				</li>
			);
		});
 
		return <ul className='char__grid'>{items}</ul>;
	};
 
	const items = renderItems(charList);
 
	const errorMessage = error ? <ErrorMessage /> : null;
	const spinner = loading ? <Spinner /> : null;
	const content = !(loading || error) ? items : null;
 
	return (
		<div className='char__list'>
			{errorMessage}
			{spinner}
			{content}
			<button
				className='button button__main button__long'
				disabled={newItemLoading}
				style={{ display: charEnded ? 'none' : 'block' }}
				onClick={() => onRequest(offset)}
			>
				<div className='inner'>load more</div>
			</button>
		</div>
	);
};

Тут нужно упомянуть, что мы используем стрелочную функцию выше её инициализации в коде. Это будет работать, так как useEffect срабатывает уже после рендера компонента

Так же такой короткой записью можно показать, какой аргумент получает функция (newItemLoading) и что она вернёт наружу (false).

Стрелочные функции используются для того, чтобы асинхронное выполнение установки состояния перевести в синхронное и выполнять по порядку. Так же использование колбэк-функции позволяет воспользоваться значением прошлого состояния

Перевод рефов будет чуть более сложным. Когда мы работаем с хуками, то у нас появляется свойство current, в которое и нужно заносить значения хука. Просто так добавить в реф ссылку не получится.

Для этого инициализируем useRef, передаём в него массив, обращаемся в методе фокуса к свойству current

И далее уже в самом рендере будет вызываться функция, которая сформирует массив ссылок на те элементы, которые сгенерируются внутри мапы

Дальше идёт компонент информации о персонаже:

components > CharInfo > CharInfo.js

const CharInfo = (props) => {
	const [char, setChar] = useState(null);
	const [loading, setLoading] = useState(false);
	const [error, setError] = useState(false);
 
	const marvelService = new MarvelService();
 
	useEffect(() => {
		updateChar();
	}, [props.charId]);
 
	const updateChar = () => {
		const { charId } = props;
		if (!charId) {
			return;
		}
		onCharLoading();
		marvelService.getCharacter(charId).then(onCharLoaded).catch(onError);
	};
 
	const onCharLoaded = (char) => {
		setLoading(false);
		setChar(char);
	};
 
	const onCharLoading = () => {
		setLoading(true);
	};
 
	const onError = () => {
		setError(true);
		setLoading(false);
	};
 
	const skeleton = char || loading || error ? null : <Skeleton />;
	const errorMessage = error ? <ErrorMessage /> : null;
	const spinner = loading ? <Spinner /> : null;
	const content = !(loading || error || !char) ? <View char={char} /> : null;
 
	return (
		<div className='char__info'>
			{skeleton}
			{errorMessage}
			{spinner}
			{content}
		</div>
	);
};
 
const View = ({ char }) => {
	const { name, description, thumbnail, homepage, wiki, comics } = char;
 
	let imgStyle = { objectFit: 'cover' };
	if (thumbnail === 'http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available.jpg') {
		imgStyle = { objectFit: 'contain' };
	}
 
	return (
		<>
			<div className='char__basics'>
				<img src={thumbnail} alt={name} style={imgStyle} />
				<div>
					<div className='char__info-name'>{name}</div>
					<div className='char__btns'>
						<a href={homepage} className='button button__main'>
							<div className='inner'>homepage</div>
						</a>
						<a href={wiki} className='button button__secondary'>
							<div className='inner'>Wiki</div>
						</a>
					</div>
				</div>
			</div>
			<div className='char__descr'>{description}</div>
			<div className='char__comics'>Comics:</div>
			<ul className='char__comics-list'>
				{comics.length > 0 ? null : 'There is no comics with this character'}
				{comics.map((item, i) => {
					// eslint-disable-next-line
					if (i > 9) return;
					return (
						<li key={i} className='char__comics-item'>
							{item.name}
						</li>
					);
				})}
			</ul>
		</>
	);
};
 
CharInfo.propTypes = {
	charId: PropTypes.number,
};
 
export default CharInfo;

И компонент рандомного персонажа:

components > RandomChar > RandomChar.js

const RandomChar = () => {
	const [char, setChar] = useState(null);
	const [loading, setLoading] = useState(true);
	const [error, setError] = useState(false);
 
	const marvelService = new MarvelService();
 
	useEffect(() => {
		updateChar();
		const timerId = setInterval(updateChar, 60000);
 
		return () => {
			clearInterval(timerId);
		};
	}, []);
 
	const onCharLoaded = (char) => {
		setLoading(false);
		setChar(char);
	};
 
	const onCharLoading = () => {
		setLoading(true);
	};
 
	const onError = () => {
		setError(true);
		setLoading(false);
	};
 
	const updateChar = () => {
		const id = Math.floor(Math.random() * (1011400 - 1011000)) + 1011000;
		onCharLoading();
		marvelService.getCharacter(id).then(onCharLoaded).catch(onError);
	};
 
	const errorMessage = error ? <ErrorMessage /> : null;
	const spinner = loading ? <Spinner /> : null;
	const content = !(loading || error || !char) ? <View char={char} /> : null;
 
	return (
		<div className='randomchar'>
			{errorMessage}
			{spinner}
			{content}
			<div className='randomchar__static'>
				<p className='randomchar__title'>
					Random character for today!
					<br />
					Do you want to get to know him better?
				</p>
				<p className='randomchar__title'>Or choose another one</p>
				<button onClick={updateChar} className='button button__main'>
					<div className='inner'>try it</div>
				</button>
				<img
					src={mjolnir}
					alt='mjolnir'
					className='randomchar__decoration'
				/>
			</div>
		</div>
	);
};
 
const View = ({ char }) => {
	const { name, description, thumbnail, homepage, wiki } = char;
	let imgStyle = { objectFit: 'cover' };
	if (thumbnail === 'http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available.jpg') {
		imgStyle = { objectFit: 'contain' };
	}
 
	return (
		<div className='randomchar__block'>
			<img
				src={thumbnail}
				alt='Random character'
				className='randomchar__img'
				style={imgStyle}
			/>
			<div className='randomchar__info'>
				<p className='randomchar__name'>{name}</p>
				<p className='randomchar__descr'>{description}</p>
				<div className='randomchar__btns'>
					<a href={homepage} className='button button__main'>
						<div className='inner'>homepage</div>
					</a>
					<a href={wiki} className='button button__secondary'>
						<div className='inner'>Wiki</div>
					</a>
				</div>
			</div>
		</div>
	);
};
 
export default RandomChar;

008 Создание собственных хуков

const Form = () => {
	const [text, setText] = useState('');
 
	return (
		<Container>
			<form className='w-50 border mt-5 p-3 m-auto'>
				<div className='mb-3'>
					<input
						value={text}
						type='text'
						className='form-control'
						readOnly
					/>
					<label
						htmlFor='exampleFormControlInput1'
						className='form-label mt-3'
					>
						Email address
					</label>
					<input
						onChange={(e) => setText(e.target.value)}
						type='email'
						className='form-control'
						id='exampleFormControlInput1'
						placeholder='name@example.com'
					/>
				</div>
				<div className='mb-3'>
					<label
						htmlFor='exampleFormControlTextarea1'
						className='form-label'
					>
						Example textarea
					</label>
					<textarea
						className='form-control'
						id='exampleFormControlTextarea1'
						rows='3'
					></textarea>
				</div>
			</form>
		</Container>
	);
};

// первая версия
const validateInput = (text) => {
	if (text.search(/\d/) >= 0) {
		return true;
	}
 
	return false;
};
 
// вторая версия
const validateInput = (text) => {
	return text.search(/\d/) >= 0 ? true : false;
};
 
// третья версия
const validateInput = (text) => {
	return text.search(/\d/) >= 0;
};

И сейчас мы добавили в работу ещё одно поле для ввода текста - код был повторён. Тут мы сталкиваемся с такой ситуацией, что мы постоянно повторяем код, который написали единожды

const Form = () => {
	const [text, setText] = useState('');
	const [textArea, setTextArea] = useState('');
 
	// функция, которая будет валидировать инпут (если символов 0, то вернёт фолс)
	const validateInput = (text) => {
		return text.search(/\d/) >= 0;
	};
 
	// тут уже будем хранить условие с выбором класса
	const colorInput = validateInput(text) ? 'text-danger' : 'text-success';
 
	return (
		<Container>
			<form className='w-50 border mt-5 p-3 m-auto'>
				<div className='mb-3'>
					<input
						/* вставляем текст с нескольких инпутов */
						value={`${text} / ${textArea}`}
						type='text'
						/* вставляем класс */
						className={`form-control ${colorInput}``}
						readOnly
					/>
					<label
						htmlFor='exampleFormControlInput1'
						className='form-label mt-3'>
						Email address
					</label>
					<input
						onChange={(e) => setText(e.target.value)}
						type='email'
						className='form-control'
						id='exampleFormControlInput1'
						placeholder='name@example.com'
					/>
				</div>
				<div className='mb-3'>
					<label
						htmlFor='exampleFormControlTextarea1'
						className='form-label'>
						Example textarea
					</label>
					<textarea
						/* установка нвого состояния */
						onChange={(e) => setTextArea(e.target.value)}
						className='form-control'
						id='exampleFormControlTextarea1'
						rows='3'
					></textarea>
				</div>
			</form>
		</Container>
	);
};

И теперь мы можем выделить всю вышеописанную повторяемую логику в отдельный хук. Кастомный хук - это механизм повторого использования логики с состоянием.

Таким образом выглядит классический кастомный хук. Обычно он возвращает несколько объектов в массиве и сохраняет в себе определённую логику

Либо мы можем собрать всю логику хука в отдельную переменную, если возвращать из него объект. Такой подход позволит создать несколько независимых объектов

Пример использования хука внутри компонента:

  • Инициализируем хук два раза для двух наших элементов формы
  • Мы не передаём внутрь функции validateInput текст (переменная colorInput), так как он берётся из внутреннего состояния хука, который относится к данному инпуту
  • В элементы мы передаём value и onChange, которые относятся к инкапсулированной логике их хуков
const useInputWithValidate = (initialValue) => {
	const [value, setValue] = useState(initialValue);
 
	const onChange = (event) => {
		setValue(event.target.value);
	};
 
	const validateInput = () => {
		return value.search(/\d/) >= 0;
	};
 
	return { value, onChange, validateInput };
};
 
const Form = () => {
	// использование хука
	const input = useInputWithValidate('');
	const textArea = useInputWithValidate('');
 
	const colorInput = input.validateInput() ? 'text-danger' : 'text-success';
 
	return (
		<Container>
			<form className='w-50 border mt-5 p-3 m-auto'>
				<div className='mb-3'>
					<input
						/* вставляем текст с нескольких инпутов */
						value={`${input.value} / ${textArea.value}`}
						type='text'
						/* вставляем класс */
						className={`form-control ${colorInput}``}
						readOnly
					/>
					<label
						htmlFor='exampleFormControlInput1'
						className='form-label mt-3'
					>
						Email address
					</label>
					<input
						// установим изменение состояния из хука
						onChange={input.onChange}
						// значение инпута
						value={input.value}
						type='email'
						className='form-control'
						id='exampleFormControlInput1'
						placeholder='name@example.com'
					/>
				</div>
				<div className='mb-3'>
					<label
						htmlFor='exampleFormControlTextarea1'
						className='form-label'
					>
						Example textarea
					</label>
					<textarea
						// установим изменение состояния из хука
						onChange={textArea.onChange}
						// значение инпута
						value={textArea.value}
						className='form-control'
						id='exampleFormControlTextarea1'
						rows='3'
					></textarea>
				</div>
			</form>
		</Container>
	);
};

Итог: мы имеем оптимизированную форму

Так же можно взглянуть на:

Пример хука тогглера из второй ссылки:

import { useCallback, useState } from 'react';
// Usage
function App() {
    // Call the hook which returns, current value and the toggler function
    const [isTextChanged, setIsTextChanged] = useToggle();
 
    return (
        <button
	        onClick={setIsTextChanged}
	    >
	        {isTextChanged ? 'Toggled' : 'Click to Toggle'}
	    </button>
    );
}
// Hook
// Parameter is the boolean, with default "false" value
const useToggle = (initialState = false) => {
    // Initialize the state
    const [state, setState] = useState(initialState);
 
    // Define and memorize toggler function in case we pass down the component,
    // This function change the boolean value to it's opposite value
    const toggle = useCallback(() => setState(state => !state), []);
 
    return [state, toggle]
}

009 Практика собственных хуков на проекте

Создадим хук, который будет получать данные о персонажах с сервера и выдавать их. Так же он будет контролировать состояние загрузки и ошибки

src > hooks > http.hook.js

import { useCallback, useState } from 'react';
 
export const useHttp = () => {
	const [loading, setLoading] = useState(false);
	const [error, setError] = useState(null); // == false
 
	// эта функция будет выполнять запрос на сервер и возвращать данные
	const request = useCallback(
		async (
			url,
			method = 'GET',
			body = null,
			headers = { 'Content-type': 'application/json' },
		) => {
			// поставим загрузку
			setLoading(true);
 
			try {
				// отправляем запрос на сервер
				const response = await fetch(url, { method, body, headers });
 
				// если запрос не ок, то выкидываем ошибку
				if (!response.ok) {
					throw new Error(`Could not fetch ${url}, status: ${response.status}`);
				}
 
				// распарсим данные
				const data = await response.json();
 
				// окончим загрузку
				setLoading(false);
 
				// вернём данные
				return data;
			} catch (e) {
				setLoading(false);
				setError(e.message);
				throw e;
			}
		},
		[],
	);
 
	// чтобы избежать бага, когда у нас будет постоянно висеть ошибка, нам нужно очищать эту ошибку
	const clearError = useCallback(() => setError(false), []);
 
	return { loading, request, error, clearError };
};

Далее нужно переделать сервис по общению с сервером под хуки и встроить в его запросы request из прошлого хука

src > services > marvel.service.js

import { useHttp } from '../hooks/http.hook';
 
const useMarvelService = () => {
	const { request, loading, error, clearError } = useHttp();
 
	const _apiBase = 'https://gateway.marvel.com:443/v1/public/';
	const _apiKey = 'apikey=abfdaba95091affea928543eb9253ded';
	const _baseOffsetForPerson = 210;
	const _baseOffsetForComics = 210;
 
	const getAllCharacters = async (offset = _baseOffsetForPerson) => {
		const res = await request(`${_apiBase}characters?limit=9&offset=${offset}&${_apiKey}`);
 
		return res.data.results.map(_transformCharacter);
	};
 
	const getCharacter = async (id) => {
		const res = await request(`${_apiBase}characters/${id}?${_apiKey}`);
		return _transformCharacter(res.data.results[0]);
	};
 
	const _transformCharacter = (char) => {
		return {
			id: char.id,
			name: char.name,
			description: char.description
				? char.description.slice(0, 150) + '...'
				: 'No description for this person',
			thumbnail: char.thumbnail.path + '.' + char.thumbnail.extension,
			homepage: char.urls[0].url,
			wiki: char.urls[1].url,
			comics: char.comics.items,
		};
	};
 
	return { loading, error, clearError, getCharacter, getAllCharacters };
};
 
export default useMarvelService;
 

Далее нужно поменять общение с сервером в остальных компонентах.

Чтобы всё работало нормально, нужно так же правильно вставить clearError.

src > component > randomChar > RandomChar.js

import { Component, useEffect, useState } from 'react';
import Spinner from '../Spinner/Spinner';
import ErrorMessage from '../ErrorMessage/ErrorMessage';
 
import './randomChar.scss';
import mjolnir from '../../resources/img/mjolnir.png';
import useMarvelService from '../../services/marvel.service';
 
const RandomChar = () => {
	const [char, setChar] = useState(null);
 
	// отсюда получаем состояния и функцию
	const { loading, error, clearError, getCharacter } = useMarvelService();
 
	useEffect(() => {
		updateChar();
		const timerId = setInterval(updateChar, 60000);
 
		return () => {
			clearInterval(timerId);
		};
	}, []);
 
	// тут уже просто устанавливаем персонажа
	const onCharLoaded = (char) => {
		setChar(char);
	};
 
	const updateChar = () => {
		// тут нужно сбросить ошибку, чтобы появилась возможность обновить персонажа
		clearError();
 
		const id = Math.floor(Math.random() * (1011400 - 1011000)) + 1011000;
 
		// вся логика получения данных описана в хуке
		getCharacter(id).then(onCharLoaded);
	};
 
	// тут берутся состояния ошибки и загрузки из состояния
	const errorMessage = error ? <ErrorMessage /> : null;
	const spinner = loading ? <Spinner /> : null;
	const content = !(loading || error || !char) ? <View char={char} /> : null;
 
	return (
		<div className='randomchar'>
			{errorMessage}
			{spinner}
			{content}
			<div className='randomchar__static'>
				<p className='randomchar__title'>
					Random character for today!
					<br />
					Do you want to get to know him better?
				</p>
				<p className='randomchar__title'>Or choose another one</p>
				<button onClick={updateChar} className='button button__main'>
					<div className='inner'>try it</div>
				</button>
				<img src={mjolnir} alt='mjolnir' className='randomchar__decoration' />
			</div>
		</div>
	);
};
 
const View = ({ char }) => {
	const { name, description, thumbnail, homepage, wiki } = char;
	let imgStyle = { objectFit: 'cover' };
	if (thumbnail === 'http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available.jpg') {
		imgStyle = { objectFit: 'contain' };
	}
 
	return (
		<div className='randomchar__block'>
			<img
				src={thumbnail}
				alt='Random character'
				className='randomchar__img'
				style={imgStyle}
			/>
			<div className='randomchar__info'>
				<p className='randomchar__name'>{name}</p>
				<p className='randomchar__descr'>{description}</p>
				<div className='randomchar__btns'>
					<a href={homepage} className='button button__main'>
						<div className='inner'>homepage</div>
					</a>
					<a href={wiki} className='button button__secondary'>
						<div className='inner'>Wiki</div>
					</a>
				</div>
			</div>
		</div>
	);
};
 
export default RandomChar;

Для правильного перерендера объектов карточек, нужно убрать использование переменной content и использовать просто items, который у нас генерирует другая функция.

src > component > charList > CharList.js

import { Component, useEffect, useRef, useState } from 'react';
import Spinner from '../Spinner/Spinner';
import ErrorMessage from '../ErrorMessage/ErrorMessage';
import MarvelService from '../../services/marvel.service';
import './charList.scss';
import useMarvelService from '../../services/marvel.service';
 
const CharList = ({ onCharSelected }) => {
	const [charList, setCharList] = useState([]);
	const [newItemLoading, setNewItemLoading] = useState(false);
	const [offset, setOffset] = useState(210);
	const [charEnded, setCharEnded] = useState(false);
 
	// получаем нужные состояния и функцию
	const { getAllCharacters, error, loading } = useMarvelService();
 
	useEffect(() => {
		onRequest(offset, true);
	}, []);
 
	// удаляем функцию для установки setNewItemLoading и переносим все внутренности сюда
	const onRequest = (offset, initial) => {
		// если загрузка первая, то нужно оставить установку новых карточек в false
		initial ? setNewItemLoading(false) : setNewItemLoading(true);
		getAllCharacters(offset).then(onCharListLoaded);
	};
 
	const onCharListLoaded = (newCharList) => {
		let ended = false;
		if (newCharList.length < 9) {
			ended = true;
		}
 
		// убираем setLoading
		setCharList((charList) => [...charList, ...newCharList]);
		setNewItemLoading((newItemLoading) => false);
		setOffset((offset) => offset + 9);
		setCharEnded(ended);
	};
 
	const itemRefs = useRef([]);
 
	const focusOnItem = (id) => {
		itemRefs.current.forEach((item) => item.classList.remove('char__item_selected'));
		itemRefs.current[id].classList.add('char__item_selected');
		itemRefs.current[id].focus();
	};
 
	const renderItems = (arr) => {
		const items = arr.map((item, i) => {
			let imgStyle = { objectFit: 'cover' };
			if (
				item.thumbnail ===
				'http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available.jpg'
			) {
				imgStyle = { objectFit: 'unset' };
			}
 
			return (
				<li
					className='char__item'
					tabIndex={0}
					ref={(element) => (itemRefs.current[i] = element)}
					key={item.id}
					onClick={() => {
						onCharSelected(item.id);
						focusOnItem(i);
					}}
					onKeyDown={(e) => {
						if (e.key === ' ' || e.key === 'Enter') {
							onCharSelected(item.id);
							focusOnItem(i);
						}
					}}
				>
					<img src={item.thumbnail} alt={item.name} style={imgStyle} />
					<div className='char__name'>{item.name}</div>
				</li>
			);
		});
 
		return <ul className='char__grid'>{items}</ul>;
	};
 
	const items = renderItems(charList);
 
	const errorMessage = error ? <ErrorMessage /> : null;
	// спиннер нужно показывать только тогда, когда у нас loading !!
	const spinner = loading && !newItemLoading ? <Spinner /> : null;
 
	// уже эту строку нужно удалить, так как наш компонент пересоздаётся, что приводит к перерисовке всех элементов списка
	// const content = !(loading || error) ? items : null;
 
	return (
		<div className='char__list'>
			{errorMessage}
			{spinner}
 
			{/* и теперь тут рендерим не content, а items */}
			{items}
			<button
				className='button button__main button__long'
				disabled={newItemLoading}
				style={{ display: charEnded ? 'none' : 'block' }}
				onClick={() => onRequest(offset)}
			>
				<div className='inner'>load more</div>
			</button>
		</div>
	);
};
 
export default CharList;

В CharInfo нужно просто поменять общение с сервером на хуковый и добавить очистку ошибки, если сервер не сможет вернуть данные (чтобы в принципе информация обновлялась)

src > component > charInfo > CharInfo.js

import { Component, useEffect, useState } from 'react';
 
import MarvelService from '../../services/marvel.service';
import Spinner from '../Spinner/Spinner';
import ErrorMessage from '../ErrorMessage/ErrorMessage';
import Skeleton from '../skeleton/Skeleton';
 
import './charInfo.scss';
import PropTypes from 'prop-types';
import useMarvelService from '../../services/marvel.service';
 
const CharInfo = (props) => {
	const [char, setChar] = useState(null);
 
	const { loading, error, clearError, getCharacter } = useMarvelService();
 
	useEffect(() => {
		updateChar();
	}, [props.charId]);
 
	const updateChar = () => {
		// тут нужно сбросить ошибку, чтобы появилась возможность обновить персонажа
		clearError();
 
		const { charId } = props;
		if (!charId) {
			return;
		}
		getCharacter(charId).then(onCharLoaded);
	};
 
	const onCharLoaded = (char) => {
		setChar(char);
	};
 
	const skeleton = char || loading || error ? null : <Skeleton />;
	const errorMessage = error ? <ErrorMessage /> : null;
	const spinner = loading ? <Spinner /> : null;
	const content = !(loading || error || !char) ? <View char={char} /> : null;
 
	return (
		<div className='char__info'>
			{skeleton}
			{errorMessage}
			{spinner}
			{content}
		</div>
	);
};
 
const View = ({ char }) => {
	const { name, description, thumbnail, homepage, wiki, comics } = char;
 
	let imgStyle = { objectFit: 'cover' };
	if (thumbnail === 'http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available.jpg') {
		imgStyle = { objectFit: 'contain' };
	}
 
	return (
		<>
			<div className='char__basics'>
				<img src={thumbnail} alt={name} style={imgStyle} />
				<div>
					<div className='char__info-name'>{name}</div>
					<div className='char__btns'>
						<a href={homepage} className='button button__main'>
							<div className='inner'>homepage</div>
						</a>
						<a href={wiki} className='button button__secondary'>
							<div className='inner'>Wiki</div>
						</a>
					</div>
				</div>
			</div>
			<div className='char__descr'>{description}</div>
			<div className='char__comics'>Comics:</div>
			<ul className='char__comics-list'>
				{comics.length > 0 ? null : 'There is no comics with this character'}
				{comics.map((item, i) => {
					// eslint-disable-next-line
					if (i > 9) return;
					return (
						<li key={i} className='char__comics-item'>
							{item.name}
						</li>
					);
				})}
			</ul>
		</>
	);
};
 
CharInfo.propTypes = {
	charId: PropTypes.number,
};
 
export default CharInfo;

010 Что такое batching и как он работает в React 18+

==Batching== - это объединение обновления нескольких состояний в одну операцию для улучшения производительности. Объединение нескольких обновлений позволяет экономить ресурсы ПК за счёт единоразового перерендера

У нас есть функция, которая вызывает внутри себя срабатывание изменения двух разных состояний, то есть компонент должен перерендерится два раза и вывести два лога в консоль

Однако при каждом срабатывании функции и изменении двух состояний, мы получаем только один лог в консоль - обе эти операции совмещаются

Но если поместить вызов изменения внутрь колбэка (то есть изменения происходят асинхронно), то мы будем получать два лога в консоль, так как эти состояния будут меняться отдельно друг от друга и каждый раз обновлять компонент

Так же если запихнуть log() в компонент CharList, то в консоли выведется сразу несколько таких логов, так как наши изменения состояний находятся в асинхронных функциях

Однако тут используется версия React 18, в котором оптимизированы некоторые изменения состояний и их бэтчинг, что сокращает количество перерендеров

Однако, если нам нужно разъединить операции обновления, то мы можем воспользоваться функцией flushSync (эту функцию нужно использовать отдельно на каждую операцию)

011 (д) useTransition, useDeferredValue и другие нововведения React 18+

useId() - генерирует уникальный идентификатор (он не должен использоваться для формирования атрибута key)

function Checkbox() {
  const id = useId(); // сгенерирует уникальный ключ
  return (
    <>
      <label htmlFor={id}>Do you like React?</label>
      <input id={id} type="checkbox" name="react"/>
    </>
  );

Так же были добавлены хуки для интеграции сторонних библиотек:

Конкурентный режим — это нововведение в React. Его задача — адаптировать приложение к разным устройствам и скорости сети. Пока что Concurrent Mode — эксперимент, который может быть изменён разработчиками библиотеки, а значит, новых инструментов нет в стабильной версии.

Сам конкурентный режим может ставить на рендер сразу несколько компонентов или ставить их на паузу определяя приоритет

import data from './data';
import {useState, useMemo} from 'react';
 
function App() {
    const [text, setText] = useState('');
    const [posts, setPosts] = useState(data);
 
    const filteredPosts = useMemo(() => {
        return posts.filter(item => item.name.toLowerCase().includes(text));
    }, [text]);
 
    const onValueChange = (e) => {
        setText(e.target.value);
    }
 
    return (
        <>
            <input value={text} type='text' onChange={onValueChange}/>
 
            <hr/>
 
            <div>
                {filteredPosts.map(post => (
                    <div key={post._id}>
                        <h4>{post.name}</h4>
                    </div>
                ))}
            </div>
        </>
    );
}
 
export default App;

И сейчас мы столкнулись с такой проблемой, что ввод в инпут очень сильно лагает. Дело в том, что наш стейт меняется сразу при вводе новых данных, что тормозит ввод новых символов из-за постоянного рендера

useDeferredValue() - данный хук позволит получить нужное нам значение с небольшим интервалом, чтобы задержать рендер компонента

useTransition() - так же позволяет задержать перерендер компонента, но предоставляет возможность самому указать, что будет в интервале и как на него реагировать

Хук возвращает нам булеан ожидания рендера isPending и функцию, в которой будет находиться функция, которая выполняется длительное время. Далее нам нужно будет только сделать условный рендеринг, куда мы вставим спиннер (или другой элемент для ожидания загрузки)

И тут появляется элемент загрузки

012 Навигация в приложении, React Router v5+

Сейчас имеется сразу несколько версий реакт-роутер-дома, но стоит начать с пятой

Устанавливаем нужную нам версию через @версия

npm i react-router-dom@5.3.4

И далее нам нужно закинуть в проект три компонента из роутер-дома:

  • BrowserRouter - роутер по всем страницам приложения (отслеживает переход по ссылкам)
  • Route - отдельный роут приложения
  • Switch

И далее обернём все наши страницы подобным образом:

  • В BrowserRouter (который переименовали в Router) поместим всё наше приложение
  • А в отдельный Route поместим компоненты, которые должны рендериться на отдельной странице

components > app > App.js

import { useState } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
 
const App = () => {
	const [selectedChar, setSelectedChar] = useState(null);
 
	const onCharSelected = (id) => {
		setSelectedChar(id);
	};
 
	return (
		<Router>
			<div className='app'>
				<AppHeader />
				<main>
					<Route path={'/'}>
						<ErrorBoundary>
							<RandomChar />
						</ErrorBoundary>
						<div className='char__content'>
							<ErrorBoundary>
								<CharList onCharSelected={onCharSelected} />
							</ErrorBoundary>
							<ErrorBoundary>
								<CharInfo charId={selectedChar} />
							</ErrorBoundary>
						</div>
						<img className='bg-decoration' src={decoration} alt='vision' />
					</Route>
					<Route path={'/comics'}>
						<AppBanner />
						<ComicsList />
					</Route>
				</main>
			</div>
		</Router>
	);
};
 
export default App;

Но тут стоит заметить, что роутер компонует между собой элементы с ссылками, которые хранят одинаковые значения. То есть в данном случае первый роут и второй объединятся, так как / и /comics имеют при себе слеш

И при переходе на /comics у нас будет следующая картина:

Такой подход был бы уместен, если мы заранее спланировали бы вёрстку таким образом, что нам нужно показывать дополнительные данные (тыкаем по карточке товара и раскрывается его расширенное описание)

Switch - переключатель по роутам - он уже загружает отдельный роут как новую страницу

Однако тут мы упираемся в такую особенность работы свича, что он грузит только первую ссылку, которая совпадает с url, имеющейся на странице

То есть свитч грузит только первую страницу, которая совпала с первой ссылкой (которая всегда /) и не смотрит на следующие ссылки, которые имеют тот же маршрут

Чтобы исправить проблему, у нас есть два пути решения:

  1. Главную страницу / расположить в конце списка свича
  2. Добавить атрибут exact, который обязует, чтобы рендер был только по написанию полного и правильного пути

Вот пример использования первого подхода (все /имя нужно будет писать до / главной страницы)

И вот пример использования обязующего атрибута

Результат:

И далее, чтобы добавить ссылки в наш проект, нужно в нужное место в компоненте добавить компонент Link, который в качестве ссылки в себя принимает атрибут to

components > appHeader > AppHeader.js

import { Link } from 'react-router-dom';
 
const AppHeader = () => {
	return (
		<header className='app__header'>
			<h1 className='app__title'>
				// вместо a и href вставляем Link и to
				<Link to={'/'}>
					<span>Marvel</span> information portal
				</Link>
			</h1>
			<nav className='app__menu'>
				<ul>
					<li>
						<Link to={'/'}>Characters</Link>
					</li>
					/
					<li>
						<Link to={'/comics'}>Comics</Link>
					</li>
				</ul>
			</nav>
		</header>
	);
};
 
export default AppHeader;

И сейчас ссылки для перехода по страницам работают

Так же мы имеем функцию redirect, которая при определённых условиях позволяет заредиректить пользователя (например, если он не залогинен или определённой ссылки не существует)

import { redirect } from "react-router-dom";
 
const loader = async () => {
  const user = await getUser();
  if (!user) {
    return redirect("/login");
  }
  return null;
};

Так же мы имеем атрибут NavLink, который позволяет нам стилизовать активную ссылку. Его особенностью является наличие атрибута activeStyle

Однако, когда мы добавляем стили для наших элементов, стоит добавлять атрибут exact, чтобы стили применялись не ко всем элементам сразу, а только к нужным

components > appHeader > AppHeader.js

import { Link, NavLink } from 'react-router-dom';
 
const AppHeader = () => {
	return (
		<header className='app__header'>
			<h1 className='app__title'>
				<Link to={'/'}>
					<span>Marvel</span> information portal
				</Link>
			</h1>
			<nav className='app__menu'>
				<ul>
					<li>
						<NavLink
							exact
							activeStyle={{ color: '#9F0013' }}
							to={'/'}
						>
							Characters
						</NavLink>
					</li>
					/
					<li>
						<NavLink
							exact
							activeStyle={{ color: '#9F0013' }}
							to={'/comics'}
						>
							Comics
						</NavLink>
					</li>
				</ul>
			</nav>
		</header>
	);
};

И далее мы можем вынести страницы в отдельные компоненты и поместить их в папку pages

src > components > pages > ComicsList.js

import React from 'react';
import AppBanner from '../appBanner/AppBanner';
import ComicsList from '../comicsList/ComicsList';
 
const ComicsPage = () => {
	return (
		<>
			<AppBanner />
			<ComicsList />
		</>
	);
};
 
export default ComicsPage;

src > components > pages > MainPage.js

import React, { useState } from 'react';
import ErrorBoundary from '../ErrorBoundary/ErrorBoundary';
import RandomChar from '../randomChar/RandomChar';
import CharList from '../charList/CharList';
import CharInfo from '../charInfo/CharInfo';
import decoration from '../../resources/img/vision.png';
 
const MainPage = () => {
	const [selectedChar, setSelectedChar] = useState(null);
 
	const onCharSelected = (id) => {
		setSelectedChar(id);
	};
 
	return (
		<>
			<ErrorBoundary>
				<RandomChar />
			</ErrorBoundary>
			<div className='char__content'>
				<ErrorBoundary>
					<CharList onCharSelected={onCharSelected} />
				</ErrorBoundary>
				<ErrorBoundary>
					<CharInfo charId={selectedChar} />
				</ErrorBoundary>
			</div>
			<img className='bg-decoration' src={decoration} alt='vision' />
		</>
	);
};
 
export default MainPage;

А далее экспортировать их через index.js, который сократит до них путь

src > components > pages > index.js

import MainPage from './MainPage';
import ComicsPage from './ComicsPage';
 
export { MainPage, ComicsPage };

И тут используем импорт

src > components > app > App.js

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
 
import AppHeader from '../appHeader/AppHeader';
// импортируем страницы из одного файла
import { ComicsPage, MainPage } from '../pages';
 
const App = () => {
	return (
		<Router>
			<div className='app'>
				<AppHeader />
				<main>
					<Switch>
						<Route exact path={'/'}>
							<MainPage />
						</Route>
						<Route exact path={'/comics'}>
							<ComicsPage />
						</Route>
					</Switch>
				</main>
			</div>
		</Router>
	);
};
 
export default App;

013 React Router v6+

И теперь нужно установить последнюю версию роутера

npm i react-router-dom@latest

Тут находится руководство о переходе с пятой версии на шестую

Вместо компонента Switch используется компонент Routes.

Нужный компонент для отрисовки теперь передаётся не в качестве child, а передаётся внутрь атрибута element.

Теперь вместо хука useHistory нужно использовать useNavigate

Теперь мы пишем не так:

const history = useHistory();
 
/// CODE ...
 
<button onClick={() => history.push('/')}>BACK</button>

А так:

import { Link, useNavigate, useParams } from 'react-router-dom';
 
interface IUserItemPageParams {
	id: string;
}
 
const UserItemPage: FC = () => {
	const [user, setUser] = useState<IUser | null>(null);
	const params = useParams();
 
	// используем навигацию
	const navigate = useNavigate();
 
	async function fetchUser() {
		try {
			const response = await axios.get<IUser>(
				'https://jsonplaceholder.typicode.com/users/' + params.id,
			);
			setUser(response.data);
		} catch (e) {
			console.error(e);
		}
	}
 
	useEffect(() => {
		fetchUser();
	}, []);
 
	return (
		<div>
			{/* перемещаемся в нужное место */}
			<button onClick={() => navigate('/users')}>back</button>
			<h1>Страница пользователя {user?.name}</h1>
			<h4>Проживает в {user?.address.city}</h4>
		</div>
	);
};

Хук useRouteMatch заменили на useMatch

Компонент Prompt больше не поддерживается

Так же нужно сказать, что такого атрибута как exact теперь не существует. Внутри Routes проходит правильное сравнение ссылок, что не приводит к рендеру одного компонента внутри другого. Если нам нужно будет использовать эквивалент этому атрибуту в NavLink, то там мы вместо него пишем end

components > app > App.js

import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
 
const App = () => {
	return (
		<Router>
			<div className='app'>
				<AppHeader />
				<main>
					<Routes>
						<Route path={'/'} element={<MainPage />} />
						<Route path={'/comics'} element={<ComicsPage />} />
					</Routes>
				</main>
			</div>
		</Router>
	);
};

Так же в новой версии у нас пропала наша классическая композиция, когда у нас свитч рендерил сразу все страницы, если их не разделять атрибутом exact. Чтобы использовать данную функциональность и подгружать другую страницу внутри нашей страницы, нужно использовать компонент <Outlet />. Он загрузит другой компонент на нашей странице при клике на нужную ссылку.

Так же нужно указать, что ссылки внутри роутов будут относиться к этим роутам. То есть, если родительский роут имеет ссылку to='/comics', то при выборе внутри него ссылки to='/deadpool' мы перейдём по ссылке /comics/deadpool. В пятой версии с этим были определённые трудности.

Из вышеописанных исправлений вытекает дополнительный функционал:

  • to='.' будет осуществлять переход на эту же страницу
  • to='..' будет вызывать страницу на один уровень выше (родительскую)
  • to='../bayonette' выйдет на уровень выше и перейдёт оттуда на другую страницу (которая находится в родительском компоненте)

Так же из компонента NavLink удалили атрибуты activeStyle и activeClassName. Вместо них нужно самому делать функции по добавлению нужного функционала

Исправим хедер страницы, чтобы он поддерживал 6 версию роутер-дома:

  • заменяем exact на end
  • заменяем activeStyle на style. Сам же стиль будет автоматически принимать в себя аргумент активности (isActive), чтобы мы могли навесить нужные нам стили
const AppHeader = () => {
	return (
		<header className='app__header'>
			<h1 className='app__title'>
				<Link to={'/'}>
					<span>Marvel</span> information portal
				</Link>
			</h1>
			<nav className='app__menu'>
				<ul>
					<li>
						<NavLink
							// вставляем вместо exact
							end
							// можно так передать стили
							style={({ isActive }) =>
								({ color: isActive ? '#9F0013' : 'inherit' })}
							to={'/'}
						>
							Characters
						</NavLink>
					</li>
					/
					<li>
						<NavLink
							end
							// а можно так
							style={({ isActive }) =>
								(isActive ? { color: '#9F0013' } : {})}
							to={'/comics'}
						>
							Comics
						</NavLink>
					</li>
				</ul>
			</nav>
		</header>
	);
};

Теперь применение стилей правильно работает:

014 Практика создания динамических путей

Страница ошибки

import React from 'react';
import ErrorMessage from '../ErrorMessage/ErrorMessage';
import { Link } from 'react-router-dom';
 
const Error404 = () => {
	return (
		<div style={{ textAlign: 'center' }}>
			<ErrorMessage />
			<h1>Page not found!</h1>
			<Link
				style={{
					display: 'inline-block',
					padding: '10px',
					margin: '10px',
					textDecoration: 'underline',
					border: '1px solid black',
				}}
				to={'/'}
			>
				Back to main page
			</Link>
		</div>
	);
};
 
export default Error404;
import MainPage from './MainPage';
import ComicsPage from './ComicsPage';
import Error404 from './404';
 
export { MainPage, ComicsPage, Error404 };
import { ComicsPage, Error404, MainPage } from '../pages';
 
const App = () => {
	return (
		<Router>
			<div className='app'>
				<AppHeader />
				<main>
					<Routes>
						<Route path={'/'} element={<MainPage />} />
						<Route path={'/comics'} element={<ComicsPage />} />
						<Route path={'*'} element={<Error404 />} />
					</Routes>
				</main>
			</div>
		</Router>
	);
};

Динамические страницы

Дальше используется Router Dom v5

Match хранит в себе данные о том, как именно path совпал с текущим адресом History представляет из себя API для организации перехода между страницами Location хранит в себе состояние положения роутера

Для работы с данными объектами используются хуки: useParams, useHistory, useLocation

Для реализации побочной ссылки, которая будет грузиться изнутри другого роута, нужно добавить новый роут с родительским путём и указать дополнительный динамический путь через :. То есть ссылка будет выглядеть следующим образом: /comics/:comicId - передаём параметр comicId в динамическую ссылку

components > app > App.js

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
 
import { MainPage, ComicsPage, Error404, SingleComicPage } from '../pages';
import AppHeader from '../appHeader/AppHeader';
 
const App = () => {
	return (
		<Router>
			<div className='app'>
				<AppHeader />
				<main>
					<Switch>
						<Route exact path='/'>
							<MainPage />
						</Route>
						<Route exact path='/comics'>
							<ComicsPage />
						</Route>
						<Route exact path='/comics/:comicId'>
							<SingleComicPage />
						</Route>
						<Route path='*'>
							<Error404 />
						</Route>
					</Switch>
				</main>
			</div>
		</Router>
	);
};
 
export default App;

В сервисе нужно иметь функцию, которая будет по id возвращать нужный нам комикс

service > marvel.service.js

const getComics = async (id) => {
   const res = await request(`${_apiBase}comics/${id}?${_apiKey}`);
   return _transformComics(res.data.results[0]);
};

Далее в компоненте списка комиксов поменяем ссылку a на Link и в параметры ссылки передадим item.id, который будет ссылаться на определённый комикс

components > comicsList > ComicsList.js

function renderItems(arr) {
	const items = arr.map((item, i) => {
		return (
			<li className='comics__item' key={i}>
				// добавляем сюда динамическую ссылку в качестве линка
				<Link to={`/comics/${item.id}``}>
					<img src={item.thumbnail} alt={item.title} className='comics__item-img' />
					<div className='comics__item-name'>{item.title}</div>
					<div className='comics__item-price'>{item.price}</div>
				</Link>
			</li>
		);
	});
 
	return <ul className='comics__grid'>{items}</ul>;
}

Далее нам нужно реализовать страницу отдельного комикса

components > pages > SingleComicPage.js

import { useParams, Link } from 'react-router-dom';
import { useState, useEffect } from 'react';
 
import useMarvelService from '../../services/marvel.service';
import Spinner from '../Spinner/Spinner';
import ErrorMessage from '../ErrorMessage/ErrorMessage';
import AppBanner from '../appBanner/AppBanner';
import './singleComicPage.scss';
 
const SingleComicPage = () => {
	// берём id комикса из нашей адресной строки с помощью хука
	const { comicId } = useParams();
	// состояние комикса
	const [comic, setComic] = useState(null);
	// получаем функции для работы с сервером
	const { loading, error, getComics, clearError } = useMarvelService();
 
	useEffect(() => {
		// обновляем комикс при изменении id
		updateComic();
	}, [comicId]);
 
	// обновление комикса
	const updateComic = () => {
		clearError();
		getComics(comicId).then(onComicLoaded);
	};
 
	// тут устанавливаем комикс в стейт
	const onComicLoaded = (comic) => {
		setComic(comic);
	};
 
	const errorMessage = error ? <ErrorMessage /> : null;
	const spinner = loading ? <Spinner /> : null;
	const content = !(loading || error || !comic) ? <View comic={comic} /> : null;
 
	return (
		<>
			<AppBanner />
			{errorMessage}
			{spinner}
			{content}
		</>
	);
};
 
// рендерим сам компонент комикса
const View = ({ comic }) => {
	// деструктурируем ответ
	const { title, description, pageCount, thumbnail, language, price } = comic;
 
	return (
		<div className='single-comic'>
			<img src={thumbnail} alt={title} className='single-comic__img' />
			<div className='single-comic__info'>
				<h2 className='single-comic__name'>{title}</h2>
				<p className='single-comic__descr'>{description}</p>
				<p className='single-comic__descr'>{pageCount}</p>
				<p className='single-comic__descr'>Language: {language}</p>
				<div className='single-comic__price'>{price}</div>
			</div>
			<Link to='/comics' className='single-comic__back'>
				Back to all
			</Link>
		</div>
	);
};
 
export default SingleComicPage;

Экспортируем страницу одиночного комикса

components > pages > index.js

import MainPage from './MainPage';
import ComicsPage from './ComicsPage';
import Error404 from './404';
import SingleComicPage from './SingleComicPage';
 
export { MainPage, ComicsPage, Error404, SingleComicPage };

Чтобы слово Comics горело даже в отдельном комиксе, нужно убрать строгое сравнение ссылки через exact из компонента AppHeader

components > appHeader > AppHeader.js

import { Link, NavLink } from 'react-router-dom';
import './appHeader.scss';
 
const AppHeader = () => {
	return (
		<header className='app__header'>
			<h1 className='app__title'>
				<Link to='/'>
					<span>Marvel</span> information portal
				</Link>
			</h1>
			<nav className='app__menu'>
				<ul>
					<li>
						<NavLink exact activeStyle={{ color: '#9f0013' }} to='/'>
							Characters
						</NavLink>
					</li>
					/
					<li>
						<NavLink activeStyle={{ color: '#9f0013' }} to='/comics'>
							Comics
						</NavLink>
					</li>
				</ul>
			</nav>
		</header>
	);
};
 
export default AppHeader;

015 Динамические импорты и React.lazy

На определённом этапе разработки приложение станет настолько большим, что уже оно начнёт загружаться крайне длительное время. Но мы можем определить, какие участки приложения нам не нужны на этапе первичной загрузки и так же мы можем указать с помощью JS на эти блоки кода.

Для примера создадим функцию логгера:

components > charList > someFunc.js

export function logger(logString = 'Hello, World!') {
   console.log(logString);
}
 
export function secondLog(logString = 'Second Log!') {
   console.log(logString);
}

Динамический импорт возвращает промис с объектом модуля

Тут нужно напомнить, что любой экспорт из файла в JS экспортирует единый объект (в данном случае - obj), который хранит данную функцию в качестве свойства (obj.logger). Если мы экспортируем по умолчанию через export default, то на выходе мы получаем объект с одним свойством - obj.default

Так же обязательно всегда нужно указывать catch, который будет срабатывать, когда не сработал импорт / неправильно был указан путь

components > charList > CharList.js

Но зачастую используется более простой синтаксис - получение нужной функции через деструктуризацию

components > charList > CharList.js

И если нам нужно будет вытащить дефолтную функцию, то в импортах нужно будет обратиться не к функции по имени, а к свойству default, которое содержит функцию

components > charList > CharList.js

if (loading) {
   import('./someFunc').then((obj) => obj.default());
}

Далее переходим к функционалу реакта - React.lazy

  • Основным условием является то, что компонент должен экспортироваться дефолтно из файла
  • Так же все динамические импорты нужно вставлять после статических, иначе может произойти ошибка

Далее нужно как и с промисами обработать возможную ошибку. Для этого предназначен дополнительный компонент Suspense. Он принимает в себя атрибут fallback, который будет показываться пока подгружается нужный нам компонент из динамического импорта

components > app > App.js

import { lazy, Suspense } from 'react';
 
// нужен дефолтный экспорт объекта
const Error404 = lazy(() => import('../pages/404'));
 
const App = () => {
	return (
		<Router>
			<div className='app'>
				<AppHeader />
				<main>
		// оборачиваем страницу в саспенс, который и будет подгружать нужный компонент
					<Suspense fallback={<Spinner />}>
						<Switch>
							<Route exact path='/'>
								<MainPage />
							</Route>
							<Route exact path='/comics'>
								<ComicsPage />
							</Route>
							<Route exact path='/comics/:comicId'>
								<SingleComicPage />
							</Route>
							<Route path='*'>
								<Error404 />
							</Route>
						</Switch>
					</Suspense>
				</main>
			</div>
		</Router>
	);
};
 
export default App;

И теперь во время загрузки этой страницы у нас будет показываться спиннер пока не загрузится ошибка

Так же можно сделать подобную подгрузку для всех страниц, чтобы они не грузились сразу все пользователю, а только по надобности

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import AppHeader from '../appHeader/AppHeader';
 
import { lazy, Suspense } from 'react';
import Spinner from '../Spinner/Spinner';
 
// нужен дефолтный экспорт объекта
const Error404 = lazy(() => import('../pages/404'));
const MainPage = lazy(() => import('../pages/MainPage'));
const ComicsPage = lazy(() => import('../pages/ComicsPage'));
const SingleComicPage = lazy(() => import('../pages/SingleComicPage'));
 
const App = () => {
	return (
		<Router>
			<div className='app'>
				<AppHeader />
				<main>
					<Suspense fallback={<Spinner />}>
						<Switch>
							<Route exact path='/'>
								<MainPage />
							</Route>
							<Route exact path='/comics'>
								<ComicsPage />
							</Route>
							<Route exact path='/comics/:comicId'>
								<SingleComicPage />
							</Route>
							<Route path='*'>
								<Error404 />
							</Route>
						</Switch>
					</Suspense>
				</main>
			</div>
		</Router>
	);
};
 
export default App;

И уже таким образом будет выглядеть ошибка, если ленивые импорты вставить внутрь статических

До ленивых импортов мы имели 3 файла JS и вся папка весила 751 килобайт. Тут нужно сказать, что все эти файлы пользователь подгружал сразу, даже если их функционал ему не нужен был

После lazy-импорта количество файлов возросло в несколько раз и вес папки со скриптами вырос до 880 килобайт. Хоть скрипты и весят больше в общем, но теперь пользователь не будет скачивать все страницы сразу - он будет получать только актуальные ему страницы и подгружать их в процессе использования приложения.

Использовать ленивую загрузку стоит уже в больших приложениях, где первая скорость загрузки уже будет переваливать за 3 секунды

  • Зачастую такую загрузку применяют уже к целым страницам приложения
  • Но таким же образом можно выносить в ленивую загрузку и отдельные компоненты

016 React.memo, Pure Component и оптимизация скорости работы приложения

React.memo - это компонент высшего порядка (HOC), который предназначен для мемоизации рендера компонента. Если в компонент не пришли новые пропсы или не изменился стейт, то компонент не перерендерится и сохранит ресурсы компьютера пользователя.

Например, мы имеем форму. При нажатии на кнопку компонент формы получает новые пропсы и перерендеривается.

import { useState } from 'react';
import { Container } from 'react-bootstrap';
import './App.css';
 
const Form = (props) => {
	console.log('render Form');
 
	return (
		<Container>
			<form className='w-50 border mt-5 p-3 m-auto'>
				<div className='mb-3'>
					<label htmlFor='exampleFormControlInput1' className='form-label mt-3'>
						Email address
					</label>
					<input
						value={props.mail}
						type='email'
						className='form-control'
						id='exampleFormControlInput1'
						placeholder='name@example.com'
					/>
				</div>
				<div className='mb-3'>
					<label htmlFor='exampleFormControlTextarea1' className='form-label'>
						Example textarea
					</label>
					<textarea
						value={props.text}
						className='form-control'
						id='exampleFormControlTextarea1'
						rows='3'
					></textarea>
				</div>
			</form>
		</Container>
	);
};
 
function App() {
	const [data, setData] = useState({
		mail: 'name@example.com',
		text: 'some text',
	});
 
	return (
		<>
			<Form mail={data.mail} text={data.text} />
			<button
				onClick={() =>
					setData({
						mail: 'second@example.com',
						text: 'another text',
					})
				}
			>
				Click me
			</button>
		</>
	);
}
 
export default App;

Чтобы решить вышеописанную проблему, нужно просто обернуть компонент в memo(), который сохранит результат рендера и при неизменных значениях не будет рендерить компонент заново

// обернём компонент в memo()
const Form = memo((props) => {
	console.log('render Form');
 
	/// CODE ...
});

Однако тут нужно упомянуть, что перерендер будет происходить, если внутри пропсов мы передаём свойства с вложенными объектами

function App() {
	const [data, setData] = useState({
		// объект внутри свойства объекта
		mail: { name: 'name@example.com' },
		text: 'some text',
	});
 
	return (
		<>
			<Form mail={data.mail} text={data.text} />
			<button
				onClick={() =>
					setData({
						// передаём mail - name
						mail: { name: 'name@example.com' },
						text: 'another text',
					})
				}
			>
				Click me
			</button>
		</>
	);
}

Чтобы указать, что в приложении нам нужно реализовать для определённого пропса глубокую проверку (проверять изменение вложенных значений), то нам нужно реализовать функцию, которая будет сравнивать значения и возвращать boolean

// функция для сравнения пропсов
const propsCompare = (prevProps, nextProps) => {
	return prevProps.mail.name === nextProps.mail.name;
};
 
const Form = memo((props) => {
	console.log('render Form');
 
	return (
		<Container>
			<form className='w-50 border mt-5 p-3 m-auto'>
				<div className='mb-3'>
					<label
						htmlFor='exampleFormControlInput1'
						className='form-label mt-3'
					>
						Email address
					</label>
					<input
						value={props.mail.name}
						type='email'
						className='form-control'
						id='exampleFormControlInput1'
						placeholder='name@example.com'
					/>
				</div>
				<div className='mb-3'>
					<label
						htmlFor='exampleFormControlTextarea1'
						className='form-label'
					>
						Example textarea
					</label>
					<textarea
						value={props.text}
						className='form-control'
						id='exampleFormControlTextarea1'
						rows='3'
					></textarea>
				</div>
			</form>
		</Container>
	);
}, propsCompare);

React.PureComponent - это расширение классовых компонентов, которое в отличе от Component триггерит каждый раз функцию shouldComponentUpdate(), которым мы определяем потребность в обновлении компонента

Всю логику, что мы реализовывали через memo данный компонент реализует сам. Однако нам нужно будет так же проводить сравнение внутри shouldComponentUpdate(), если мы будем передавать вложенные объекты

class Form extends PureComponent {
	render() {
		console.log('render Form');
 
		return (
			<Container>
				<form className='w-50 border mt-5 p-3 m-auto'>
					<div className='mb-3'>
						<label htmlFor='exampleFormControlInput1' className='form-label mt-3'>
							Email address
						</label>
						<input
							value={this.props.mail}
							type='email'
							className='form-control'
							id='exampleFormControlInput1'
							placeholder='name@example.com'
						/>
					</div>
					<div className='mb-3'>
						<label htmlFor='exampleFormControlTextarea1' className='form-label'>
							Example textarea
						</label>
						<textarea
							value={this.props.text}
							className='form-control'
							id='exampleFormControlTextarea1'
							rows='3'
						></textarea>
					</div>
				</form>
			</Container>
		);
	}
}
 
function App() {
	const [data, setData] = useState({
		mail: 'name@example.com',
		text: 'some text',
	});
 
	return (
		<>
			<Form mail={data.mail} text={data.text} />
			<button
				onClick={() =>
					setData({
						mail: 'name@example.com',
						text: 'another text',
					})
				}
			>
				Click me
			</button>
		</>
	);
}
 
export default App;

Если мы хотим контролировать перерендер в обычном компоненте, который наследуется от Component, то нам нужно будет использовать функцию shouldComponentUpdate()

Тут стоит сделать пометку, что логику очень глубокого сравнения делать не стоит

class Form extends Component {
	shouldComponentUpdate(nextProps) {
		if (this.props.mail.name === nextProps.mail.name) {
			return false;
		}
 
		return true;
	}
 
	render() {
		console.log('render Form');
 
		return (
			<Container>
				<form className='w-50 border mt-5 p-3 m-auto'>
					<div className='mb-3'>
						<label
							htmlFor='exampleFormControlInput1'
							className='form-label mt-3'
						>
							Email address
						</label>
						<input
							value={this.props.mail.name}
							type='email'
							className='form-control'
							id='exampleFormControlInput1'
							placeholder='name@example.com'
						/>
					</div>
					<div className='mb-3'>
						<label
							htmlFor='exampleFormControlTextarea1'
							className='form-label'
						>
							Example textarea
						</label>
						<textarea
							value={this.props.text}
							className='form-control'
							id='exampleFormControlTextarea1'
							rows='3'
						></textarea>
					</div>
				</form>
			</Container>
		);
	}
}

Вывод:

  • memo() используется для функциональных компонентов
  • Для классовых компонентов используется PureComponent или Component вместе с функцией shouldComponentUpdate()
  • Используется мемоизация для компонентов, которые часто получают одинаковые пропсы

Если добавить мемоизацию на компонент, который постоянно получает новые пропсы, то можно только затормозить работу приложения дополнительным сохранением данных - поэтому использовать данный функционал нужно аккуратно

Например, если мы передадим функцию, то компонент даже с memo() будет перерендериваться постоянно из-за того, что каждый раз при передаче будет создаваться новая функция (а функция является объектом в JS).

Это так же будет происходить, если мы вынесем эту функцию в отдельную именованную стрелочную, потому что наш компонент App перерендеривается каждый раз, когда в нём меняется состояние и из-за этого пересоздаётся функция внутри него

Чтобы закешировать функцию и не пересоздавать её по-новой каждый раз, можно просто замемоизировать её через useCallback()

017 React Context и useContext

React.createContext и useContext - это функциональность, которая позволит создать один глобальный провайдер пропсов, чтобы пользоваться ими из любого участка приложения. То есть мы можем передавать данные по дереву компонентов не прибегая к property drill (сверлим компоненты пропсами, которые нужно передать ниже)

Вот пример антипаттерна передачи пропсов через несколько промежуточных компонентов:

<Page user={user} avatarSize={avatarSize} />
// ... который рендерит ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... который рендерит ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... который рендерит ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>
  • Функция createContext создаёт единый контекст в приложении
  • В эту функцию можно передать дефолтное значение, которое будет передаваться во все провайдеры, если в них не было передано значение в атрибут value
  • Сам Provider является компонентом и в себя он принимает любое значение своего компонента (компонент App раздаёт состояние data)
  • Consumer так же является компонентом, который получает все данные из провайдера. Данный компонент получает функцию с данными в виде одного аргумента и через неё и можно отрендерить внутренности (просто так вставить компонент не получится)
  • Все компоненты, которые используют данные провайдера будут обновлены при изменении этих данных
import { Component, createContext, memo, PureComponent, useCallback, useState } from 'react';
import { Container } from 'react-bootstrap';
import './App.css';
 
// это создание самого контекста в приложении
const dataContent = createContext({
	// сюда мы можем передать дефолтные данные
	mail: 'name@example.com',
	text: 'some text',
});
 
// получаем провайдера и консьюмера
const { Provider, Consumer } = dataContent;
 
const Form = memo((props) => {
	console.log('render Form');
 
	return (
		<Container>
			<form className='w-50 border mt-5 p-3 m-auto'>
				<div className='mb-3'>
					<label htmlFor='exampleFormControlInput1' className='form-label mt-3'>
						Email address
					</label>
					<Input />
				</div>
				<div className='mb-3'>
					<label htmlFor='exampleFormControlTextarea1' className='form-label'>
						Example textarea
					</label>
					<textarea
						value={props.text}
						className='form-control'
						id='exampleFormControlTextarea1'
						rows='3'
					></textarea>
				</div>
			</form>
		</Container>
	);
});
 
// компонент инпута
class Input extends Component {
	render() {
		return (
			// объявляем получателя
			<Consumer>
			{/* получаем функцию, которая сгенерирует нужную нам вёрстку и отдаст нужное значение */}
				{(value) => (
					<input
						// используем значение value
						value={value.mail}
						type='email'
						className='form-control'
						id='exampleFormControlInput1'
						placeholder='name@example.com'
					/>
				)}
			</Consumer>
		);
	}
}
 
function App() {
	const [data, setData] = useState({
		mail: 'name@example.com',
		text: 'some text',
	});
 
	return (
		// оборачиваем нужный участок в провайдера и передаём нужное для распространения значение (data)
		<Provider value={data}>
			<Form text={data.text} />
			<button
				onClick={() =>
					setData({
						mail: 'name@example.com',
						text: 'another text',
					})
				}
			>
				Click me
			</button>
		</Provider>
	);
}
 
export default App;

Сам же объект, который располагается в контексте хранит в себе:

  • Переданные данные
  • Provider - раздаёт данные всем компонентом из единого места в приложении
  • Consumer - подписывается на провайдера и следит за изменением его данных

Так же есть более простой метод получать данные из состояний - это присвоение контекста компоненту

// компонент инпута
class Input extends Component {
	render() {
		return (
			<input
				// вытаскиваем данные из контекста
				value={this.context.mail}
				type='email'
				className='form-control'
				id='exampleFormControlInput1'
				placeholder='name@example.com'
			/>
		);
	}
}
 
// присваиваем контекст компоненту
Input.contextType = dataContent;

Так же можно использовать static присвоение контекста, но это экспериментальный способ

class Input extends Component {
	static contextType = dataContent;
 
	render() {
		return (
			<input
				// вытаскиваем данные из контекста
				value={this.context.mail}
				type='email'
				className='form-control'
				id='exampleFormControlInput1'
				placeholder='name@example.com'
			/>
		);
	}
}

Ну и работа заметно упрощается, когда мы работаем с функциональными компонентами и просто используем useContext()

const Input = (props) => {
	// подписываемся на определённый контекст
	const context = useContext(dataContext);
 
	return (
		<input
			value={context.mail}
			type='email'
			className='form-control'
			id='exampleFormControlInput1'
			placeholder='name@example.com'
		/>
	);
};
  • так же приложение можно разбить на модули с контекстом
  • контекстов может быть несколько в приложении
  • провайдеры можно вкладывать в провайдеры, чтобы добавлять дополнительно контекст
/// context.js - тут хранится сам контекст приложения
import { createContext } from 'react';
 
export const dataContext = createContext({
	mail: 'name@example.com',
	text: 'some text',
});
 
 
/// Input.js - тут хранится отдельный компонент инпута
import { useContext } from 'react';
import { dataContext } from './context';
 
export const Input = (props) => {
	const context = useContext(dataContext);
 
	return (
		<input
			value={context.mail}
			type='email'
			className='form-control'
			id='exampleFormControlInput1'
			placeholder='name@example.com'
		/>
	);
};
 
 
/// Form.js - компонент формы
import { memo } from 'react';
import { Container } from 'react-bootstrap';
import { Input } from './Input';
 
export const Form = memo((props) => {
	console.log('render Form');
 
	return (
		<Container>
			<form className='w-50 border mt-5 p-3 m-auto'>
				<div className='mb-3'>
					<label
						htmlFor='exampleFormControlInput1'
						className='form-label mt-3'
					>
						Email address
					</label>
					<Input />
				</div>
				<div className='mb-3'>
					<label
						htmlFor='exampleFormControlTextarea1'
						className='form-label'
					>
						Example textarea
					</label>
					<textarea
						value={props.text}
						className='form-control'
						id='exampleFormControlTextarea1'
						rows='3'
					></textarea>
				</div>
			</form>
		</Container>
	);
});
 
 
/// App.js - главный компонент приложения
import { useState } from 'react';
import './App.css';
 
import { dataContext } from './context';
import { Form } from './Form';
 
const { Provider } = dataContext;
 
function App() {
	const [data, setData] = useState({
		mail: 'name@example.com',
		text: 'some text',
	});
 
	return (
		<Provider value={data}>
			<Form text={data.text} />
			<button
				onClick={() =>
					setData({
						mail: 'name@example.com',
						text: 'another text',
					})
				}
			>
				Click me
			</button>
		</Provider>
	);
}
 
export default App;
  • Таким образом можно изменить состояние через функцию, переданную внутри контекста
  • Теперь нам везде так же нужно передавать функцию forceChangeMail (меняет стейт), чтобы не было ошибок
/// Input.js
export const Input = (props) => {
	// подписываемся на определённый контекст
	const context = useContext(dataContext);
 
	return (
		<input
			value={context.mail}
			type='email'
			className='form-control'
			id='exampleFormControlInput1'
			placeholder='name@example.com'
			// при фокусе будет меняться состояние
			onFocus={context.forceChangeMail}
		/>
	);
};
 
 
/// App.js
const { Provider } = dataContext;
 
function App() {
	const [data, setData] = useState({
		mail: 'name@example.com',
		text: 'some text',
		forceChangeMail, // здесь передаём функцию
	});
 
	// функция будет менять данные в стейте при фокусе
	function forceChangeMail() {
		setData({ ...data, mail: 'changed@mail.com' });
	}
 
	return (
		<Provider value={data}>
			<Form text={data.text} />
			<button
				onClick={() =>
					setData({
						mail: 'name@example.com',
						text: 'another text',
						forceChangeMail, // здесь передаём функцию
					})
				}
			>
				Click me
			</button>
		</Provider>
	);
}

Так же нужно сказать, что если мы в Provider не передаём атрибут value, то он будет равняться undefind, что вызовет ошибки

Однако, если убрать провайдера, то мы будем получать данные по умолчанию из контекста

И чтобы не получать ошибок, нужно добавлять все новые значения в значения по умолчанию контекста

Так же в провайдера не стоит передавать объекты напрямую, так как такая запись будет ухудшать оптимизацию проекта

018 useReducer

useReducer - это функция, которая управляет ограниченным набором состояний. Она заменяет useState и позволяет нам предсказывать определённые наборы состояний компонента. Хук возвращает само состояние и функцию dispatch, которая вызывает изменение состояния. dispatch принимает в себя объект с одним обязательным свойством type, которое хранит в себе тип операции

Хук принимает в себя три аргумента:

  • Функцию-reducer, которая отвечает за изменение состояния
  • Начальное состояние
  • Ленивое создание начального состояния

И тут у нас построена определённая структура:

  • Добавлен хук useReducer, который будет контролировать состояние автоплея слайдера
  • Внутрь мы передаём функцию reducer и начальное значение состояния
  • Функция reducer через switch-конструкцию возвращает определённое значение в зависимости от переданного значения action. Так же эта функция принимает в себя state, чтобы от него иметь возможность поменять состояние
  • Далее мы вызываем работу useReducer из вёрстки через функцию dispatch, которая принимает в себя экшен. Этот экшен уже будет передан в функцию reducer
import { useReducer, useState } from 'react';
import { Container } from 'react-bootstrap';
import './App.css';
 
// это функция изменения состояния
const reducer = (state, action) => {
	switch (action.type) {
		case 'toggle':
			return { autoplay: !state.autoplay };
		case 'slow':
			return { autoplay: 300 };
		case 'fast':
			return { autoplay: 1500 };
		default:
			throw new Error('Reducer is not compatible');
			break;
	}
};
 
const Slider = (props) => {
	const [slide, setSlide] = useState(0);
	// const [autoplay, setAutoplay] = useState(false);
 
	// тут создаётся редьюсер и передаётся функция изменения состояния и начальное значение состояния
	// возвращается изначение состояния и функция вызова изменения состояния
	const [autoplay, dispatch] = useReducer(reducer, { autoplay: false });
 
	function changeSlide(i) {
		setSlide((slide) => slide + i);
	}
 
	return (
		<Container>
			<div className='slider w-50 m-auto'>
				<img
					className='d-block w-100'
					src='https://www.planetware.com/wpimages/2020/02/france-in-pictures-beautiful-places-to-photograph-eiffel-tower.jpg'
					alt='slide'
				/>
				<div className='text-center mt-5'>
					Active slide {slide} <br />
					{/* тут немного меняем условие, чтобы достучаться до булеана */}
					{autoplay.autoplay ? 'auto' : null}
				</div>
				<div className='buttons mt-3'>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(-1)}>
						-1
					</button>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(1)}>
						+1
					</button>
					<button
						className='btn btn-primary me-2'
						// тут вызвается dispatch-функция редьюсера
						onClick={() => dispatch({ type: 'toggle' })}
					>
						toggle autoplay
					</button>
					<button
						className='btn btn-primary me-2'
						// тут вызвается dispatch-функция редьюсера
						onClick={() => dispatch({ type: 'slow' })}
					>
						slow autoplay
					</button>
					<button
						className='btn btn-primary me-2'
						// тут вызвается dispatch-функция редьюсера
						onClick={() => dispatch({ type: 'fast' })}
					>
						fast autoplay
					</button>
				</div>
			</div>
		</Container>
	);
};
 
function App() {
	return <Slider />;
}
 
export default App;

И тут уже можно увидеть подконтрольное изменение состояния, когда мы уже знаем, что будет в качестве значения состояния. Для этого и предназначен данный хук

Так выглядит ленивое задание начального значения через третий аргумент хука редьюсера:

// функция для ленивого задания состояния
function init(initial) {
	return { autoplay: initial };
}
 
const Slider = ({ initial }) => {
	const [slide, setSlide] = useState(0);
	// const [autoplay, setAutoplay] = useState(false);
 
	// тут создаётся редьюсер и передаётся функция изменения состояния и начальное значение состояния
	// возвращается изначение состояния и функция вызова изменения состояния
	const [autoplay, dispatch] = useReducer(reducer, initial, init);
 
	/// CODE ....
};
 
function App() {
	return <Slider initial={false} />;
}

При таком подходе у нас уже изначально стоит правильное значение в состоянии, которое соответствует будущим объектам

Так же в dispatch наряду с type обычно передают второе свойство - payload. Оно хранит в себе кастомное значение, которое мы хотим передать в состояние.

019 Компоненты высшего порядка (HOC)

Первым делом нужно посмотреть на то, какой механизм отвечает за логику работы ХОКов. За них отвечает подобная логика, когда у нас вызывается и возвращается одна функция внутри другой. Каждая из этих функций обогащает друг друга.

const f = (a) => {
	return (b) => {
		console.log(a+b);
	};
}
 
f(1)(2); // выход - 3

Так же можно будет работать и с классовыми компонентами

const f = (a) => {
	return class extends Component {
		render() {
			return <H1>Hello, World!</H1>
		}
	};
}

ХОКи могут пригодиться, когда нам нужно обогатить функционал достаточно похожей логики. Например, нам нужно вывести список товаров для клиента на сайте и для администратора внутри административной панели - это один и тот же список, но для разных пользователей он будет иметь немного разную информацию (конкретно администратор сможет каждый компонент изменить или удалить).

В изначальном варианте у нас представлена логика, когда мы получаем информацию о том, на каком слайде мы находимся, с условного “сервера”. Из-за определённых ограничений мы не можем использовать одну эту функцию в обоих слайдерах.

const getDataFromFirstFetch = () => {
	return 10;
};
const getDataFromSecondFetch = () => {
	return 20;
};
 
const SliderFirst = () => {
	const [slide, setSlide] = useState(0);
 
	useEffect(() => {
		setSlide(getDataFromFirstFetch());
	}, []);
 
	function changeSlide(i) {
		setSlide((slide) => slide + i);
	}
 
	return (
		<Container>
			<div className='slider w-50 m-auto mb-3'>
				<img
					className='d-block w-100'
					src='https://www.planetware.com/wpimages/2020/02/france-in-pictures-beautiful-places-to-photograph-eiffel-tower.jpg'
					alt='slide'
				/>
				<div className='text-center mt-5'>Active slide {slide}</div>
				<div className='buttons mt-3'>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(-1)}>
						-1
					</button>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(1)}>
						+1
					</button>
				</div>
			</div>
		</Container>
	);
};
 
const SliderSecond = () => {
	const [slide, setSlide] = useState(0);
	const [autoplay, setAutoplay] = useState(false);
 
	useEffect(() => {
		setSlide(getDataFromSecondFetch());
	}, []);
 
	function changeSlide(i) {
		setSlide((slide) => slide + i);
	}
 
	return (
		<Container>
			<div className='slider w-50 m-auto'>
				<img
					className='d-block w-100'
					src='https://www.planetware.com/wpimages/2020/02/france-in-pictures-beautiful-places-to-photograph-eiffel-tower.jpg'
					alt='slide'
				/>
				<div className='text-center mt-5'>
					Active slide {slide} <br />
					{autoplay ? 'auto' : null}{' '}
				</div>
				<div className='buttons mt-3'>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(-1)}>
						-1
					</button>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(1)}>
						+1
					</button>
					<button
						className='btn btn-primary me-2'
						onClick={() => setAutoplay((autoplay) => !autoplay)}
					>
						toggle autoplay
					</button>
				</div>
			</div>
		</Container>
	);
};
 
function App() {
	return (
		<>
			<SliderFirst />
			<SliderSecond />
		</>
	);
}

Все ХОКи начинаются с with.

Конкретно тут ХОК оставил страницу ровно такой же, но он сохранил в себе обобщённую логику из двух данных компонентов и передал её в них обратно.

// HOC, который принимает в себя компонент и функцию получения данных
const withSlider = (BaseComponent, getData) => {
	// сам возвращаемый компонент
	return (props) => {
		const [slide, setSlide] = useState(0);
		const [autoplay, setAutoplay] = useState(false);
 
		useEffect(() => {
			setSlide(getData());
		}, []);
 
		function changeSlide(i) {
			setSlide((slide) => slide + i);
		}
 
		return (
			<BaseComponent
				// все остальные пропсы, которые будут переданы в ХОК, попадут сюда
				{...props}
				// передаём обязательные пропсы отсюда
				changeSlide={changeSlide}
				slide={slide}
				autoplay={autoplay}
				setAutoplay={setAutoplay}
			/>
		);
	};
};
 
const getDataFromFirstFetch = () => {
	return 10;
};
const getDataFromSecondFetch = () => {
	return 20;
};
 
// вырезаем из слайдеров всю логику для примера
// передаём все нужные данные через пропсы
const SliderFirst = ({ slide, changeSlide }) => {
	return (
		<Container>
			<div className='slider w-50 m-auto mb-3'>
				<img
					className='d-block w-100'
					src='https://www.planetware.com/wpimages/2020/02/france-in-pictures-beautiful-places-to-photograph-eiffel-tower.jpg'
					alt='slide'
				/>
				<div className='text-center mt-5'>Active slide {slide}</div>
				<div className='buttons mt-3'>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(-1)}>
						-1
					</button>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(1)}>
						+1
					</button>
				</div>
			</div>
		</Container>
	);
};
 
const SliderSecond = ({ slide, autoplay, setAutoplay, changeSlide }) => {
	return (
		<Container>
			<div className='slider w-50 m-auto'>
				<img
					className='d-block w-100'
					src='https://www.planetware.com/wpimages/2020/02/france-in-pictures-beautiful-places-to-photograph-eiffel-tower.jpg'
					alt='slide'
				/>
				<div className='text-center mt-5'>
					Active slide {slide} <br />
					{autoplay ? 'auto' : null}{' '}
				</div>
				<div className='buttons mt-3'>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(-1)}>
						-1
					</button>
					<button className='btn btn-primary me-2' onClick={() => changeSlide(1)}>
						+1
					</button>
					<button
						className='btn btn-primary me-2'
						onClick={() => setAutoplay((autoplay) => !autoplay)}
					>
						toggle autoplay
					</button>
				</div>
			</div>
		</Container>
	);
};
 
// тут мы создаём ХОКовые компоненты
const SliderWithFetchFirst = withSlider(SliderFirst, getDataFromFirstFetch);
const SliderWithFetchSecond = withSlider(SliderSecond, getDataFromSecondFetch);
 
function App() {
	return (
		<>
			{/* вызываем ХОКовые компоненты */}
			<SliderWithFetchFirst />
			<SliderWithFetchSecond />
		</>
	);
}

Так же есть второй вариант создания ХОКа, когда мы создаём функцию, которая вызывает создание функции, возвращающей компонент из переданного пропса в первую функцию.

Конкретно тут ХОК обогащает переданный компонент в него логгером при рендере на странице

const withLogger = (WrappedComponent) => (props) => {
	useEffect(() => {
		console.log('first render');
	}, []);
 
	return <WrappedComponent {...props} />;
};
 
const Hello = () => {
	return <h1>Hello</h1>;
};
 
const HelloWithLogger = withLogger(Hello);
 
function App() {
	return (
		<>
			<HelloWithLogger />
			<SliderWithFetchFirst />
			<SliderWithFetchSecond />
		</>
	);
}

И теперь компонент приветствия вызывает лог в консоли

Итог:

Когда не стоит использовать HOC:

  • Если компоненты слишком разные и их логику не получается обобщить. Самый идеальный вариант, когда мы передаём минимальное количество пропсов в возвращаемый компонент:
  • Если в проекте имеется только один компонент, который подходит для использования вместе с компонентом высшего порядка
  • Если приходится каждый раз модифицировать HOC при подключении нового компонента

Когда использовать:

  • Когда много компонентов имеют схожую логику выполнения
  • Когда понятно, что ХОК не будет расти со временем из-за схожести логики
  • Когда нужно добавить общую логику для выполнения самых разных компонентов

020 Библиотеки и экосистема React

Современная разработка веб-приложений не представляется без использования сторонних библиотек: нужно быстро выполнить задачу, нужно выполнить задачу на уровне, хочется просто не придумывать велосипед - за всем этим можно обратиться к уже готовым библиотекам

Полезные ссылки, на которых можно узнать побольше о библиотеках реакта:

  • Тут находится список из 10 библиотек, которые позволяет заменить простой функционал реакта
  • Тут находятся полезные хуки, которые можно использовать в любом проекте
  • Тут находится список библиотек компонентов реакта
  • Тут находится топ реакт-хук-библиотек
  • Это портал с полезными хуками реакта
  • Тут находится информация по всей актуальной экосистеме реакта

021 React Transition Group

React Transition Group - классический модуль для создания анимаций в React.

npm install react-transition-group

Компонент Transition. Он принимает в себя на вход nodeRef (элемент ссылки), in (элемент появляется или исчезает со страницы) и timeout (длительность анимации)

Элемент при появлении делится на три этапа (пропс in в позиции false, что говорит об отсутствии элемента):

  • onEnter - инициализация появления
  • onEntering - появление
  • onEntered - окончание появления на странице

Противоположные состояния имеются, при наличии элемента на странице (актуальна анимация исчезновения элемента со страницы)

Все этапы изображены тут:

Все промежутки ожидания имеют определённую длительность. Анимировать мы можем переход от entering к entered и от exiting к exited

Тут стоит отметить, что свойство display (none и block) невозможно анимировать, поэтому их не используем

import { useState, useRef } from 'react';
import { Container } from 'react-bootstrap';
import { Transition } from 'react-transition-group'; // импорт
import './App.css';
 
const Modal = ({ showModal, onClose }) => {
	// длительность
	const duration = 300;
 
	// базовые стили
	const defaultStyle = {
		transition: `all ${duration}ms ease-in-out`,
		opacity: 0,
		visibility: 'hidden',
	};
 
	// стили на разных этапах перехода
	const transitionStyles = {
		entering: { opacity: 1, visibility: 'visible' },
		entered: { opacity: 1, visibility: 'visible' },
		exiting: { opacity: 0, visibility: 'hidden' },
		exited: { opacity: 0, visibility: 'hidden' },
	};
 
	return (
		<Transition in={showModal} timeout={duration}>
			{(state) => (
				<div className='modal mt-5 d-block' style={{ ...defaultStyle, ...transitionStyles[state] }}>
					<div className='modal-dialog'>
						<div className='modal-content'>
							<div className='modal-header'>
								<h5 className='modal-title'>Typical modal window</h5>
								<button
									onClick={() => onClose(false)}
									type='button'
									className='btn-close'
									aria-label='Close'
								></button>
							</div>
							<div className='modal-body'>
								<p>Modal body content</p>
							</div>
							<div className='modal-footer'>
								<button onClick={() => onClose(false)} type='button' className='btn btn-secondary'>
									Close
								</button>
								<button onClick={() => onClose(false)} type='button' className='btn btn-primary'>
									Save changes
								</button>
							</div>
						</div>
					</div>
				</div>
			)}
		</Transition>
	);
};
 
function App() {
	const [showModal, setShowModal] = useState(false);
 
	return (
		<Container>
			<Modal showModal={showModal} onClose={setShowModal} />
			<button type='button' className='btn btn-warning mt-5' onClick={() => setShowModal(true)}>
				Open Modal
			</button>
		</Container>
	);
}
 
export default App;

Так же в транзишене есть атрибут, который размонтирует компонент, когда его нет на странице

Так же стоит рассказать, что для транзишена ещё имеются и функции, которые позволяют выполнять разные операции на разных этапах анимации компонента.

Например, onEntering принимает в себя функцию, которая может выполниться во время появления компонента

Тут показан жизненный цикл выполнения данных функций:

Например, нам нужно скрыть кнопку, которая является триггером для модального окна

const Modal = ({ showModal, setShowTrigger, onClose }) => {
	// длительность
	const duration = 300;
 
	// базовые стили
	const defaultStyle = {
		transition: `all ${duration}ms ease-in-out`,
		opacity: 0,
		visibility: 'hidden',
	};
 
	// стили на разных этапах перехода
	const transitionStyles = {
		entering: { opacity: 1, visibility: 'visible' },
		entered: { opacity: 1, visibility: 'visible' },
		exiting: { opacity: 0, visibility: 'hidden' },
		exited: { opacity: 0, visibility: 'hidden' },
	};
 
	return (
		<Transition in={showModal} timeout={duration} onEnter={() => setShowTrigger(false)} onExited={() => setShowTrigger(true)}>
			{(state) => (
				<div className='modal mt-5 d-block' style={{ ...defaultStyle, ...transitionStyles[state] }}>
					<div className='modal-dialog'>
						<div className='modal-content'>
							<div className='modal-header'>
								<h5 className='modal-title'>Typical modal window</h5>
								<button
									onClick={() => onClose(false)}
									type='button'
									className='btn-close'
									aria-label='Close'
								></button>
							</div>
							<div className='modal-body'>
								<p>Modal body content</p>
							</div>
							<div className='modal-footer'>
								<button onClick={() => onClose(false)} type='button' className='btn btn-secondary'>
									Close
								</button>
								<button onClick={() => onClose(false)} type='button' className='btn btn-primary'>
									Save changes
								</button>
							</div>
						</div>
					</div>
				</div>
			)}
		</Transition>
	);
};
 
function App() {
	const [showModal, setShowModal] = useState(false);
	const [showTrigger, setShowTrigger] = useState(true);
 
	return (
		<Container>
			<Modal showModal={showModal} setShowTrigger={setShowTrigger} onClose={setShowModal} />
			{showTrigger ? (
				<button type='button' className='btn btn-warning mt-5' onClick={() => setShowModal(true)}>
					Open Modal
				</button>
			) : null}
		</Container>
	);
}

Далее идёт компонент CSSTransition. Его отличительная особенность заключается в том, что мы не должны передавать состояния внутрь компонента и рендерить весь компонент через функцию тоже не нужно. Этот компонент работает с готовыми стилями и производит анимацию на её основе.

Данный компонент принимает в себя атрибут classNames, где указывается начальное наименование стилей, которые относятся к этому компоненту и далее они воспроизводятся в зависимости от их префикса:

  • -enter
  • -enter-active
  • -exit
  • -exit-active
const Modal = ({ showModal, setShowTrigger, onClose }) => {
	// длительность
	const duration = 300;
 
	return (
		<CSSTransition
			in={showModal}
			timeout={duration}
			onEnter={() => setShowTrigger(false)}
			onExited={() => setShowTrigger(true)}
			// базовый className
			classNames={'modal'}
			// для решения проблем с обрывом анимации
			mountOnEnter
			unmountOnExit
		>
			<div className='modal mt-5 d-block'>
				<div className='modal-dialog'>
					<div className='modal-content'>
						<div className='modal-header'>
							<h5 className='modal-title'>Typical modal window</h5>
							<button
								onClick={() => onClose(false)}
								type='button'
								className='btn-close'
								aria-label='Close'
							></button>
						</div>
						<div className='modal-body'>
							<p>Modal body content</p>
						</div>
						<div className='modal-footer'>
							<button onClick={() => onClose(false)} type='button' className='btn btn-secondary'>
								Close
							</button>
							<button onClick={() => onClose(false)} type='button' className='btn btn-primary'>
								Save changes
							</button>
						</div>
					</div>
				</div>
			</div>
		</CSSTransition>
	);
};
/* дефолтное состояние компонента */
/* лучше не использовать такой подход, а просто атрибутами в компоненте транзишена указать mountOnEnter и unmountOnExit */
/* .modal {
  visibility: hidden;
  opacity: 0;
} */
 
/* начало появления компонента */
.modal-enter {
  opacity: 0;
}
 
/* конечное состояние анимации компонента */
.modal-enter-active {
  visibility: visible;
  opacity: 1;
  transition: all 300ms;
}
 
/* указываем конечное состояние компонента */
.modal-enter-done {
  visibility: visible;
  opacity: 1;
}
 
.modal-exit {
  opacity: 1;
}
 
/* конечное состояние вышедшего компонента */
.modal-exit-active {
  visibility: hidden;
  opacity: 0;
  transition: all 300ms;
}
 

Далее идут два компоненты - SwitchTransition и TransitionGroup - это компоненты, которые модифицируют поведение первых двух

Основной особенностью SwitchTransition является переключение режимов анимации через атрибут mode:

  • out-in - запускает анимацию и дожидаётся её окончания перед тем, как запустить анимацию другого компонента

  • in-out - сначала дожидается анимации появления второго компонента и только потом удаляет первый компонент со страницы

Компонент TransitionGroup занимается оборачиванием других компонентов анимации.

Конкретно в этом компоненте обычно разворачивают остальные компоненты транзишена из массива. Так же он позволяет не указывать атрибут in, так как этот компонент отслеживает начало всех анимаций.

022 Formik, Yup и работа с формами любой сложности

npm i formik

Formik - это популярная библиотека под React по работе с формами на странице.

  • initialValue - важный начальный атрибут для компонента Formik. В него мы вписываем связанные имена с инпутами в виде ключей и их данные являются начальными значениями. Связь устанавливается с формами через атрибут внутри формы name или id
  • validate - атрибут, который хранит функцию валидации форм
  • onSubmit - атрибут, который хранит функцию, срабатывающую при отправке формы

Тут представлен пример классического использования формика на странице:

import React from 'react';
import { Formik } from 'formik';
 
const Basic = () => (
  <div>
    <h1>Anywhere in your app!</h1>
    <Formik
      // начальные значения
      initialValues={{ email: '', password: '' }}
      // функция валидации полей
      validate={values => {
        const errors = {};
        if (!values.email) {
          errors.email = 'Required';
        } else if (
          !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
        ) {
          errors.email = 'Invalid email address';
        }
        return errors;
      }}
      // функция сабмита формы
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          setSubmitting(false);
        }, 400);
      }}
    >
      {({
        values, // значения формы
        errors, // объект с ошибками
        touched, // отображает, трогал ли пользователь данную форму
        handleChange, // событие изменения формы
        handleBlur, // действие при снятии фокуса
        handleSubmit, // сабмит формы
        isSubmitting, // состояние сабмита формы
        /* and other goodies */
      }) => (
        // форма при сабмите будет выполнять данную операцию
        <form onSubmit={handleSubmit}>
          <input
            type="email" // тип
            name="email" // имя, которое связывает с initialValues
            onChange={handleChange} // действие при изменении
            onBlur={handleBlur} // действие при снятии фокуса
            value={values.email} // связывание значения
          />
          {/* отображаем ошибку, если такая появилась */}
          {errors.email && touched.email && errors.email}
          <input
            type="password"
            name="password"
            onChange={handleChange}
            onBlur={handleBlur}
            value={values.password}
          />
          {errors.password && touched.password && errors.password}
          <button type="submit" disabled={isSubmitting}>
            Submit
          </button>
        </form>
      )}
    </Formik>
  </div>
);
 
export default Basic;

Так же формик предоставляет готовые компоненты Form, Field, ErrorMessage, которые можно использовать в проекте вместо стандартных форм:

 // Render Prop
 import React from 'react';
 import { Formik, Form, Field, ErrorMessage } from 'formik';
 
 const Basic = () => (
   <div>
     <h1>Any place in your app!</h1>
     <Formik
       initialValues={{ email: '', password: '' }}
       validate={values => {
         const errors = {};
         if (!values.email) {
           errors.email = 'Required';
         } else if (
           !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
         ) {
           errors.email = 'Invalid email address';
         }
         return errors;
       }}
       onSubmit={(values, { setSubmitting }) => {
         setTimeout(() => {
           alert(JSON.stringify(values, null, 2));
           setSubmitting(false);
         }, 400);
       }}
     >
       {({ isSubmitting }) => (
         <Form>
           <Field type="email" name="email" />
           <ErrorMessage name="email" component="div" />
           <Field type="password" name="password" />
           <ErrorMessage name="password" component="div" />
           <button type="submit" disabled={isSubmitting}>
             Submit
           </button>
         </Form>
       )}
     </Formik>
   </div>
 );
 
 export default Basic;

Далее будет реализована форма отправки данных на пожертвования. Со всей формы будут собираться данные и выводиться в логе строковый вариант объекта

  • функция handleChange будет перехватывать изменения внутри формы и определять, в какой произошли изменения
  • функция формика handleBlur записывает формы в объект touched, который передаётся в форму внутри объекта values. После добавления этой функции в инпут, можно будет воспользоваться объектом touched для проверки при выводе ошибки
import { useFormik } from 'formik';
 
// функция валидации форм
const validate = (values) => {
	const errors = {}; // массив ошибок формы
 
	// если отсутствует имя
	if (!values.name) {
		errors.name = 'Обязательное поле!';
 
		// если имя меньше двух символов
	} else if (values.name.length < 2) {
		errors.name = 'Имя должно иметь больше двух символов!';
	}
 
	// если отсутствует почта
	if (!values.email) {
		errors.email = 'Обязательное поле!';
 
		// если почта не подходит по структуре
	} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
		errors.email = 'Нужно ввести корректную почту!';
	}
 
	return errors;
};
 
const Form = () => {
	// хук Формика
	const formik = useFormik({
		initialValues: {
			name: '',
			email: '',
			amount: 0,
			currency: '',
			text: '',
			terms: false,
		},
		validate, // эта функция валидации будет применяться автоматически
		onSubmit: (values) => {
			// данная функция выведет лог с объектом, который переведён в понятную строку
			console.log(JSON.stringify(values, null, 2));
		},
	});
 
	return (
		<form
			className='form'
			// сюда передаём функцию, которая будет перехватывать сабмит из формы
			onSubmit={formik.handleSubmit}
		>
			<h2>Отправить пожертвование</h2>
			<label htmlFor='name'>Ваше имя</label>
			<input
				id='name'
				name='name'
				type='text'
				// это значение будет связано с состоянием внутри формика
				value={formik.values.name}
				// эта функция будет отслеживать какие данные и в какой форме поменялись
				onChange={formik.handleChange}
				// сообщает формику, трогал ли пользователь данную форму
				onBlur={formik.handleBlur}
			/>
			{/* вывод ошибки */}
			{formik.errors.name && formik.touched.name ? (
				<div style={{ color: 'red' }}>{formik.errors.name}</div>
			) : null}
			<label htmlFor='email'>Ваша почта</label>
			<input
				id='email'
				name='email'
				type='email'
				value={formik.values.email}
				onChange={formik.handleChange}
				onBlur={formik.handleBlur}
			/>
			{formik.errors.email && formik.touched.email ? (
				<div style={{ color: 'red' }}>{formik.errors.email}</div>
			) : null}
			<label htmlFor='amount'>Количество</label>
			<input
				id='amount'
				name='amount'
				type='number'
				value={formik.values.amount}
				onChange={formik.handleChange}
				onBlur={formik.handleBlur}
			/>
			<label htmlFor='currency'>Валюта</label>
			<select
				id='currency'
				name='currency'
				value={formik.values.currency}
				onChange={formik.handleChange}
				onBlur={formik.handleBlur}
			>
				<option value=''>Выберите валюту</option>
				<option value='USD'>USD</option>
				<option value='UAH'>UAH</option>
				<option value='RUB'>RUB</option>
			</select>
			<label htmlFor='text'>Ваше сообщение</label>
			<textarea
				id='text'
				name='text'
				value={formik.values.text}
				onChange={formik.handleChange}
				onBlur={formik.handleBlur}
			/>
			<label
				className='checkbox'
				value={formik.values.terms}
				onChange={formik.handleChange}
				onBlur={formik.handleBlur}
			>
				<input name='terms' type='checkbox' />
				Соглашаетесь с политикой конфиденциальности?
			</label>
			<button type='submit'>Отправить</button>
		</form>
	);
};
 
export default Form;
  • При вводе валидных данных форма будет их собирать и отправлять

  • Все формы могут воспринимать ошибки и реагируют на них

  • У нас выделяется всего один инпут с ошибкой (а не сразу во всех отображается ошибка, как бы было без touched)
  • Так же данные не будут отправляться, если в поле есть невалидные данные

Самый простой вариант валидации - это использовать стороннюю библиотеку, которая поможет избежать рутинных процессов, а именно - Yup. Он уже имеет много методов для валидации данных в себе и очень прост в использовании.

npm i yup

Тут показан пример, заданный в validationSchema (функция валидации данных находится второй по списку!). Все значения описаны в объекте схемы и через чейн вызваются функции проверки данных с формы. Сам Юп возвращает один объект ошибки, который работает подобно нашей самостоятельной реализации выше

import { useFormik } from 'formik';
import * as Yup from 'yup';
 
const Form = () => {
	// хук Формика
	const formik = useFormik({
		initialValues: {
			name: '',
			email: '',
			amount: 0,
			currency: '',
			text: '',
			terms: false,
		},
		// тут мы должны описать схему валидации
		validationSchema: Yup.object({
			// поле name
			name: Yup.string() // тип - строка
				.min(2, 'Имя должно иметь больше двух символов!') // минимальная длина и сообщение
				.required('Обязательное поле'), // обязательно к заполнению
			email: Yup.string()
				.email('Нужно ввести корректную почту!')
				.required('Обязательное поле'),
			amount: Yup.number()
				.min(5, 'Пожертвование не меньше 5 уе')
				.max(1000, 'Пожертвование не больше 1000 уе')
				.required('Обязательное поле'),
			currency: Yup.string().required('Выберите валюту'),
			text: Yup.string(), // необязательное поле
			terms: Yup.boolean()
				.oneOf([true], 'Необходимо согласие') // тут значение будет валидным, если оно равно одному из указанных в массиве
				.required('Подтвердите согласие'),
		}),
		onSubmit: (values) => {
			// данная функция выведет лог с объектом, который переведён в понятную строку
			console.log(JSON.stringify(values, null, 2));
		},
	});
 
	return (
		<form
			className='form'
			// сюда передаём функцию, которая будет перехватывать сабмит из формы
			onSubmit={formik.handleSubmit}
		>
			<h2>Отправить пожертвование</h2>
			<label htmlFor='name'>Ваше имя</label>
			<input
				id='name'
				name='name'
				type='text'
				// это значение будет связано с состоянием внутри формика
				value={formik.values.name}
				// эта функция будет отслеживать какие данные и в какой форме поменялись
				onChange={formik.handleChange}
				// сообщает формику, трогал ли пользователь данную форму
				onBlur={formik.handleBlur}
			/>
			{/* вывод ошибки */}
			{formik.errors.name && formik.touched.name ? (
				<div style={{ color: 'red' }}>{formik.errors.name}</div>
			) : null}
			<label htmlFor='email'>Ваша почта</label>
			<input
				id='email'
				name='email'
				type='email'
				value={formik.values.email}
				onChange={formik.handleChange}
				onBlur={formik.handleBlur}
			/>
			{formik.errors.email && formik.touched.email ? (
				<div style={{ color: 'red' }}>{formik.errors.email}</div>
			) : null}
			<label htmlFor='amount'>Количество</label>
			<input
				id='amount'
				name='amount'
				type='number'
				value={formik.values.amount}
				onChange={formik.handleChange}
				onBlur={formik.handleBlur}
			/>
			{formik.errors.amount && formik.touched.amount ? (
				<div style={{ color: 'red' }}>{formik.errors.amount}</div>
			) : null}
			<label htmlFor='currency'>Валюта</label>
			<select
				id='currency'
				name='currency'
				value={formik.values.currency}
				onChange={formik.handleChange}
				onBlur={formik.handleBlur}
			>
				<option value=''>Выберите валюту</option>
				<option value='USD'>USD</option>
				<option value='UAH'>UAH</option>
				<option value='RUB'>RUB</option>
			</select>
			{formik.errors.currency && formik.touched.currency ? (
				<div style={{ color: 'red' }}>{formik.errors.currency}</div>
			) : null}
			<label htmlFor='text'>Ваше сообщение</label>
			<textarea
				id='text'
				name='text'
				value={formik.values.text}
				onChange={formik.handleChange}
				onBlur={formik.handleBlur}
			/>
			{formik.errors.text && formik.touched.text ? (
				<div style={{ color: 'red' }}>{formik.errors.text}</div>
			) : null}
			<label
				className='checkbox'
				value={formik.values.terms}
				onChange={formik.handleChange}
				onBlur={formik.handleBlur}
			>
				<input name='terms' type='checkbox' />
				Соглашаетесь с политикой конфиденциальности?
			</label>
			{formik.errors.terms && formik.touched.terms ? (
				<div style={{ color: 'red' }}>{formik.errors.terms}</div>
			) : null}
			<button type='submit'>Отправить</button>
		</form>
	);
};
 
export default Form;

И так выглядят все реакции на ошибки в форме:

Так же вместо того, чтобы писать везде одинаковые атрибуты, можно просто вставлять деструктурированный вызов функции getFieldProps('имя_поля'), который вернёт все нужные атрибуты в инпут

Далее, чтобы использовать компоненты самого формика, нужно будет переписать код на классическое поведение без хука

  • Нам нужно будет удалить все использования переменной formik
  • Перенести все данные из хука в компонент Formik
  • Убрать хук
  • Убрать все лишние атрибуты из инпутов
  • Переименовать инпуты в Field
  • Вместо условных конструкций с выводом ошибки написать вывод через ErrorMessage
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
 
const FormInput = () => {
	return (
		<Formik
			initialValues={{
				name: '',
				email: '',
				amount: 0,
				currency: '',
				text: '',
				terms: false,
			}}
			validationSchema={Yup.object({
				name: Yup.string()
					.min(2, 'Имя должно иметь больше двух символов!')
					.required('Обязательное поле'),
				email: Yup.string()
					.email('Нужно ввести корректную почту!')
					.required('Обязательное поле'),
				amount: Yup.number()
					.min(5, 'Пожертвование не меньше 5 уе')
					.max(1000, 'Пожертвование не больше 1000 уе')
					.required('Обязательное поле'),
				currency: Yup.string().required('Выберите валюту'),
				text: Yup.string(),
				terms: Yup.boolean()
					.oneOf([true], 'Необходимо согласие')
					.required('Подтвердите согласие'),
			})}
			onSubmit={(values) => {
				console.log(JSON.stringify(values, null, 2));
			}}
		>
			<Form className='form'>
				<h2>Отправить пожертвование</h2>
				<label htmlFor='name'>Ваше имя</label>
				<Field id='name' name='name' type='text' />
				<ErrorMessage name={'name'} className={'error'} component={'div'} />
				<label htmlFor='email'>Ваша почта</label>
				<Field id='email' name='email' type='email' />
				<ErrorMessage name={'email'} className={'error'} component={'div'} />
				<label htmlFor='amount'>Количество</label>
				<Field id='amount' name='amount' type='number' />
				<ErrorMessage name={'amount'} className={'error'} component={'div'} />
				<label htmlFor='currency'>Валюта</label>
				<Field id='currency' name='currency' as={'select'}>
					<option value=''>Выберите валюту</option>
					<option value='USD'>USD</option>
					<option value='UAH'>UAH</option>
					<option value='RUB'>RUB</option>
				</Field>
				<ErrorMessage name={'currency'} className={'error'} component={'div'} />
				<label htmlFor='text'>Ваше сообщение</label>
				<Field id='text' name='text' as={'textarea'} />
				<ErrorMessage name={'text'} className={'error'} component={'div'} />
				<label className='checkbox'>
					<Field name='terms' type='checkbox' />
					Соглашаетесь с политикой конфиденциальности?
				</label>
				<ErrorMessage name={'terms'} className={'error'} component={'div'} />
				<button type='submit'>Отправить</button>
			</Form>
		</Formik>
	);
};
 
export default FormInput;

В итоге мы имеем ровно такую же форму, но более оптимизированную по коду

Когда мы говорим про чистую работу с формиком, то он сам подставляет все нужные значения в формы, которые мы обозначили как Field. Сам Field - это общее поле, которое можно через атрибут as указать как другое поле (селект или текстэриа)

Так же и с полем ошибки - использовать готовый компонент ErrorMessage куда более простой и быстрый вариант. Оно в себя принимает:

  • name - имя поля, к которому привязывается ошибка
  • component - тег, которым оно отрендерится на странице

Так же есть другой вариант отобразить ошибку - расположить функцию по отрисовке внутри компонента

<ErrorMessage name="email">{msg => <div>{msg}</div>}</ErrorMessage>

Ну и так же формик предоставляет хук useField, который позволяет нам сделать свои шаблоны под повторяющиеся формы на странице. Сам хук в себя принимает пропсы, а возвращает кортеж из значений:

  • field - хранит в себе пропсы формика (включая события onChange, onBlur и onValue)
  • meta - хранит метаданные с ошибками и был ли использован данный инпут
import { Formik, Form, Field, ErrorMessage, useField } from 'formik';
import * as Yup from 'yup';
 
const CustomInput = ({ label, ...props }) => {
	// данный хук позволит присвоить данные из формика в наши инпуты
	const [field, meta] = useField(props);
 
	return (
		<>
			<label htmlFor={props.name}>{label}</label>
			<input {...props} {...field} />
			{meta.touched && meta.error ? <div className={'error'}>{meta.error}</div> : null}
		</>
	);
};
 
const CustomCheckbox = ({ children, ...props }) => {
	// тут дополнительно разворачиваем тип инпута как чекбокс
	const [field, meta] = useField({ ...props, type: 'checkbox' });
 
	return (
		<>
			{/* лейбл без атрибута htmlFor, так как инпут внутри */}
			<label className='checkbox'>
				<input type='checkbox' {...props} {...field} />
				{children}
			</label>
			{meta.touched && meta.error ? <div className={'error'}>{meta.error}</div> : null}
		</>
	);
};
 
const CustomTextarea = ({ label, ...props }) => {
	const [field, meta] = useField({ ...props, type: 'textarea' });
 
	return (
		<>
			<label htmlFor={props.name}>{label}</label>
			<textarea {...props} {...field} />
			{meta.touched && meta.error ? <div className={'error'}>{meta.error}</div> : null}
		</>
	);
};
 
const FormInput = () => {
	return (
		<Formik
			initialValues={{
				name: '',
				email: '',
				amount: 0,
				currency: '',
				text: '',
				terms: false,
			}}
			validationSchema={Yup.object({
				name: Yup.string()
					.min(2, 'Имя должно иметь больше двух символов!')
					.required('Обязательное поле'),
				email: Yup.string()
					.email('Нужно ввести корректную почту!')
					.required('Обязательное поле'),
				amount: Yup.number()
					.min(5, 'Пожертвование не меньше 5 уе')
					.max(1000, 'Пожертвование не больше 1000 уе')
					.required('Обязательное поле'),
				currency: Yup.string().required('Выберите валюту'),
				text: Yup.string(),
				terms: Yup.boolean()
					.oneOf([true], 'Необходимо согласие')
					.required('Подтвердите согласие'),
			})}
			onSubmit={(values) => {
				console.log(JSON.stringify(values, null, 2));
			}}
		>
			<Form className='form'>
				<h2>Отправить пожертвование</h2>
				<CustomInput label={'Ваше имя'} id='name' name='name' type='text' />
				<CustomInput label={'Ваша почта'} id='email' name='email' type='email' />
				<CustomInput label={'Количество'} id='amount' name='amount' type='number' />
 
				<Field id='currency' name='currency' as={'select'}>
					<option value=''>Выберите валюту</option>
					<option value='USD'>USD</option>
					<option value='UAH'>UAH</option>
					<option value='RUB'>RUB</option>
				</Field>
				<ErrorMessage name={'currency'} className={'error'} component={'div'} />
 
				<CustomTextarea label={'Ваше сообщение'} id='text' name='text' />
 
				<CustomCheckbox name={'terms'}>
					Соглашаетесь с политикой конфиденциальности?
				</CustomCheckbox>
 
				<button type='submit'>Отправить</button>
			</Form>
		</Formik>
	);
};
 
export default FormInput;

024 Разбор домашнего задания

Первым делом, нужно добавить метод, который достанет одного персонажа с сервера по имени

service > MarvelService.js

// Вариант модификации готового метода для поиска по имени.
// Вызывать его можно вот так: getAllCharacters(null, name)
 
// const getAllCharacters = async (offset = _baseOffset, name = '') => {
//     const res = await request(`${_apiBase}characters?limit=9&offset=${offset}${name ? `&name=${name}` : '' }&${_apiKey}`);
//     return res.data.results.map(_transformCharacter);
// }
 
// Или можно создать отдельный метод для поиска по имени
 
// дополнительная функция для поиска персонажа по имени
const getCharacterByName = async (name) => {
   const res = await request(`${_apiBase}characters?name=${name}&${_apiKey}`);
   return res.data.results.map(_transformCharacter);
};

Далее добавим компонент поиска персонажа

components > CharSearchForm > CharSearchForm.js

import { useState } from 'react';
import { Formik, Form, Field, ErrorMessage as FormikErrorMessage } from 'formik';
import * as Yup from 'yup';
import { Link } from 'react-router-dom';
 
import useMarvelService from '../../services/MarvelService';
import ErrorMessage from '../errorMessage/ErrorMessage';
 
import './charSearchForm.scss';
 
// форма поиска персонажа
const CharSearchForm = () => {
	// сюда будет помещаться найденный персонаж
	const [char, setChar] = useState(null);
	// тут мы импортируем функции из сервиса
	const { loading, error, getCharacterByName, clearError } = useMarvelService();
 
	// при загрузке будем устанавливать персонажа
	const onCharLoaded = (char) => {
		setChar(char);
	};
 
	// обновляем персонажа в поиске
	const updateChar = (name) => {
		clearError();
 
		getCharacterByName(name).then(onCharLoaded);
	};
 
	// сообщение с ошибкой, если форма не прогрузится
	const errorMessage = error ? (
		<div className='char__search-critical-error'>
			<ErrorMessage />
		</div>
	) : null;
 
	// это тот ответ, который увидит пользователь по результатам поиска
	// если персонажа нет, то ничего не делаем
	// в противном случае, если длина введённого персонажа больше нуля, то выводим предложение перейти на страницу
	// в противном случае, выводим, что персонажа нет на странице
	const results = !char ? null : char.length > 0 ? (
		<div className='char__search-wrapper'>
			<div className='char__search-success'>There is! Visit {char[0].name} page?</div>
			<Link to={`/characters/${char[0].id}``} className='button button__secondary'>
				<div className='inner'>To page</div>
			</Link>
		</div>
	) : (
		<div className='char__search-error'>
			The character was not found. Check the name and try again
		</div>
	);
 
	return (
		<div className='char__search-form'>
			<Formik
				// начальное значение - имя персонажа
				initialValues={{
					charName: '',
				}}
				// схема валидации
				validationSchema={Yup.object({
					// поле обязательне и является строкой
					charName: Yup.string().required('This field is required'),
				})}
				// при сабмите отправляем апдейтим персонажа
				onSubmit={({ charName }) => {
					updateChar(charName);
				}}
			>
				<Form>
					<label className='char__search-label' htmlFor='charName'>
						Or find a character by name:
					</label>
					<div className='char__search-wrapper'>
						<Field id='charName' name='charName' type='text' placeholder='Enter name' />
						<button type='submit' className='button button__main' disabled={loading}>
							<div className='inner'>find</div>
						</button>
					</div>
					<FormikErrorMessage
						component='div'
						className='char__search-error'
						name='charName'
					/>
				</Form>
			</Formik>
			{/* выводим результат поиска пользователю */}
			{results}
 
			{/* выводим сообщение об ошибке поиска пользователю */}
			{errorMessage}
		</div>
	);
};
 
export default CharSearchForm;

Тут добавим компонент поиска персонажа на страницу под информацией о выбранном персонаже из списка

components > pages > MainPage.js

const MainPage = () => {
	const [selectedChar, setChar] = useState(null);
 
	const onCharSelected = (id) => {
		setChar(id);
	};
 
	return (
		<>
			<ErrorBoundary>
				<RandomChar />
			</ErrorBoundary>
			<div className='char__content'>
				<ErrorBoundary>
					<CharList onCharSelected={onCharSelected} />
				</ErrorBoundary>
				{/* здесь, в боковой части страницы, будет располагаться инфо о персонаже и поиск */}
				<div>
					<ErrorBoundary>
						<CharInfo charId={selectedChar} />
					</ErrorBoundary>
					<ErrorBoundary>
						<CharSearchForm />
					</ErrorBoundary>
				</div>
			</div>
			<img className='bg-decoration' src={decoration} alt='vision' />
		</>
	);
};

Тут будет содержаться логика поведения страницы для комикса или персонажа

components > pages > SinglePage.js

import { useParams } from 'react-router-dom';
import { useState, useEffect } from 'react';
 
// Хотелось бы вынести функцию по загрузке данных как отдельный аргумент
// Но тогда мы потеряем связь со стэйтами загрузки и ошибки
// А если вынесем их все в App.js - то они будут одни на все страницы
 
// страница одного персонажа или комикса
const SinglePage = ({ Component, dataType }) => {
	// получаем id персонажа или комикса из параметров
	const { id } = useParams();
    // устанавливаем данные
	const [data, setData] = useState(null);
    // получаем методы из сервиса
	const { loading, error, getComic, getCharacter, clearError } = useMarvelService();
 
    // обновляем данные при изменении id
	useEffect(() => {
		updateData();
	}, [id]);
 
    // обновляем данные в комиксе или персонаже в зависимости от свича
	const updateData = () => {
		clearError(); // очищяем ошибку, чтобы можно было обновить данные
 
		switch (dataType) {
			case 'comic':
				getComic(id).then(onDataLoaded);
				break;
			case 'character':
				getCharacter(id).then(onDataLoaded);
		}
	};
 
    // устанавливаем в состояние
	const onDataLoaded = (data) => {
		setData(data);
	};
 
	const errorMessage = error ? <ErrorMessage /> : null;
	const spinner = loading ? <Spinner /> : null;
	const content = !(loading || error || !data) ? <Component data={data} /> : null;
 
	return (
		<>
			<AppBanner />
			{errorMessage}
			{spinner}
			{content}
		</>
	);
};
 
export default SinglePage;

И тут мы создаём лейаут для отдельного персонажа

components > pages > SingleCharacterLayout > SingleCharacterLayout.js

import './singleCharacterLayout.scss';
 
const SingleCharacterLayout = ({data}) => {
 
    const {name, description, thumbnail} = data;
 
    return (
        <div className="single-comic">
            <img src={thumbnail} alt={name} className="single-comic__char-img"/>
            <div className="single-comic__info">
                <h2 className="single-comic__name">{name}</h2>
                <p className="single-comic__descr">{description}</p>
            </div>
        </div>
    )
}
 
export default SingleCharacterLayout;

Таким образом выглядят лейауты со стилями в страницах:

И финальная часть. Уже компонент App определяет то, какая страница у нас загружается - персонаж или комикс. Тут в компонент передаётся dataType, который определяет запрос свичч-конструкции

component > app > App.js

import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
 
import AppHeader from '../appHeader/AppHeader';
import Spinner from '../spinner/Spinner';
 
const Page404 = lazy(() => import('../pages/404'));
const MainPage = lazy(() => import('../pages/MainPage'));
const ComicsPage = lazy(() => import('../pages/ComicsPage'));
const SingleComicLayout = lazy(() => import('../pages/singleComicLayout/SingleComicLayout'));
const SingleCharacterLayout = lazy(() =>
	import('../pages/singleCharacterLayout/SingleCharacterLayout'),
);
const SinglePage = lazy(() => import('../pages/SinglePage'));
 
const App = () => {
	return (
		<Router>
			<div className='app'>
				<AppHeader />
				<main>
					<Suspense fallback={<Spinner />}>
						<Switch>
							<Route exact path='/'>
								<MainPage />
							</Route>
							<Route exact path='/comics'>
								<ComicsPage />
							</Route>
                            {/* рендер комиксов */}
							<Route exact path='/comics/:id'>
								<SinglePage
									Component={SingleComicLayout}
									dataType='comic'
								/>
							</Route>
                            {/* рендер персонажей */}
							<Route exact path='/characters/:id'>
								<SinglePage
									Component={SingleCharacterLayout}
									dataType='character'
								/>
							</Route>
							<Route path='*'>
								<Page404 />
							</Route>
						</Switch>
					</Suspense>
				</main>
			</div>
		</Router>
	);
};
 
export default App;

Итог: мы имеем форму поиска, которая реагирует на действия пользователя и предоставляет возможность перейти на страницу по персонажу

025 SEO-оптимизация веб-приложений, React-helmet

SEO - Search Engine Optimization - это отрасль оптимизации поисковых запросов за счёт выполнения сайтом определённых требований

Основные показатели, влияющие на СЕО положительно:

  • Валидность вёрстки
  • Использования семантической вёрстки и валидность тегов
  • Скорость загрузки
  • Заполнены для каждой страницы правильно метатеги и тайтл (они будут отображаться в поиске), а так же использование OG-тегов

Основной проблемой современных SPA является то, что они не отображают никакого контента, даже когда на них зайдёт робот (он видит только пустой div), что приводит к снижению СЕО-оптимизации.

Обычно, чтобы бороться с такой проблемой, используют фреймворки с SSR (рендерингом страницы на стороне сервера), который сразу отдаёт отрендеренную страницу любому пользователю или роботу. Самый популярный из имеющихся - NextJS. Он хранит в себе все возможности для оптимизации страницы (сам конвертирует изображения, предоставляет роутинг, рендеринг на сервере, общение с сервером через пропсы и даёт настроить метатеги на всех страницах)

Однако подход с SSR требует много вычислительных ресурсов, что приводит к сильной нагрузке на сервера. Поэтому обычно используется пререндеринг страницы через тот же react-snap, который будет отдавать боту готовую страницу

Чтобы настроить метатеги на странице можно воспользоваться модулем react-helmet, который будет работать как на клиенте, так и на сервере

npm i react-helmet

Добавление мета-тегов на страницу выглядит просто:

  • Добавляем тег Helmet в компонент
  • Внутрь него вставляем нужные мета-теги либо можем передавать их в качестве атрибутов компонента

Вставим мету на страницу со списком комиксов

components > pages > ComicsPage.js

import ComicsList from '../comicsList/ComicsList';
import AppBanner from '../appBanner/AppBanner';
import { Helmet } from 'react-helmet';
 
const ComicsPage = () => {
	return (
		<>
			<Helmet>
				<meta name={'description'} content={'Comics Marvel'} />
				<title>Marvel comics</title>
			</Helmet>
			<AppBanner />
			<ComicsList />
		</>
	);
};
 
export default ComicsPage;

Вставим мету на страницу комиксов

components > pages > SingleComicLayout.js

const SingleComicLayout = ({ data }) => {
	const { title, description, pageCount, thumbnail, language, price } = data;
 
	return (
		<div className='single-comic'>
			<Helmet>
				<meta name={'description'} content={`${title} comics``} />
				<title>{title}</title>
			</Helmet>
			<img src={thumbnail} alt={title} className='single-comic__img' />
			<div className='single-comic__info'>
				<h2 className='single-comic__name'>{title}</h2>
				<p className='single-comic__descr'>{description}</p>
				<p className='single-comic__descr'>{pageCount}</p>
				<p className='single-comic__descr'>Language: {language}</p>
				<div className='single-comic__price'>{price}</div>
			</div>
			<Link to='/comics' className='single-comic__back'>
				Back to all
			</Link>
		</div>
	);
};

Вставим мету на страницу персонажей

components > pages > SingleCharacterLayout.js

const SingleCharacterLayout = ({ data }) => {
	const { name, description, thumbnail } = data;
 
	return (
		<div className='single-comic'>
			<Helmet>
				<meta name={'description'} content={description} />
				<title>{name}</title>
			</Helmet>
			<img src={thumbnail} alt={name} className='single-comic__char-img' />
			<div className='single-comic__info'>
				<h2 className='single-comic__name'>{name}</h2>
				<p className='single-comic__descr'>{description}</p>
			</div>
		</div>
	);
};

И так же можно убрать мета-теги из хтмлки

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

И теперь на всех страницах имеются свои мета-теги

026 Принцип конечного автомата (FSM, Finite-state machine) и +1 подход к состояниям

Начнём с того, что в приведённом примере ниже, отображение на странице зависит от данных четырёх состояний. Это очень громоздкая и зачастую непонятная конструкция, но ей пользуются для реализации вёрстки на страницах.

И тут к нам приходит Концепция Конечного Автомата (FSM - Finite-State Machine - State Machine - Машина состояний). Это такая сущность (математическая модель), которая имеет определённое количество состояний. То есть данная сущность может иметь ровно одно состояние в определённый момент времени.

Если кнопка скрыта, то она уже имеет определённое состояние. Она не может быть выключена, так как она скрыта и так далее.

На примере курьера можно привести его состояния:

  • Ожидание (тут он может отдохнуть, выпить кофе)
  • Получение заказа (тут он уже уточняет адрес, общается с заказчиком и так далее)
  • Доставка заказа (уже тут он перемещается к заказчику)
  • Получение оплаты (а тут получает оплату и дальше переходит в состояние ожидания)

Данная концепция не выделяется как отдельный приём. Оно отвечает лишь за то, что мы контролируем количество состояний, которое может быть в приложении.

Для работы со машиной состояний имеется несколько библиотек под JS (их стоит использовать уже вместе с Redux):

Модифицируем хук, который выполняет получение данных с сервера. В него на каждый из этапов будем устанавливать состояние процесса, который будет вызвать определённый рендер в компонентах

Первым делом, мы можем удалить состояния loading и error из хука и так же установку этих состояний внутри хука

hooks > http.hook.js

import { useState, useCallback } from 'react';
 
export const useHttp = () => {
	// Эти две строки больше НЕ НУЖНЫ, так как мы больше не используем данные состояния, а перекладываем всю ответственность на состояние process
	// const [loading, setLoading] = useState(false);
	// const [error, setError] = useState(null);
 
	// тут будет находиться состояние процесса
	// начальное - ожидание
	const [process, setProcess] = useState('waiting');
 
	const request = useCallback(
		async (
			url,
			method = 'GET',
			body = null,
			headers = { 'Content-Type': 'application/json' },
		) => {
			// тут происходит загрузка
			setProcess('loading');
 
			try {
				const response = await fetch(url, { method, body, headers });
 
				if (!response.ok) {
					throw new Error(`Could not fetch ${url}, status: ${response.status}`);
				}
 
				const data = await response.json();
 
				return data;
			} catch (e) {
				// так же состояние может принять в себя ошибку
				setProcess('error');
				throw e;
			}
		},
		[],
	);
 
	const clearError = useCallback(() => {
		// тут так же будет стоять ожидание
		setProcess('waiting');
	}, []);
 
	return { request, clearError, process, setProcess };
};

Возвращаем хук установки процесса и состояние самого процесса через хук сервиса общения с сервером

service > MarvelService.js

const useMarvelService = () => {
	const { request, clearError, process, setProcess } = useHttp();
 
	/// CODE ...
 
	return {
		clearError,
		getAllCharacters,
		getCharacterByName,
		getCharacter,
		getAllComics,
		getComic,
		process,
		setProcess,
	};
};

Эта уже сама машина состояний. Тут мы определяем, какие процессы будут выполняться на разных этапах процесса

utils > setContent.js

import Skeleton from '../components/skeleton/Skeleton';
import Spinner from '../components/spinner/Spinner';
import ErrorMessage from '../components/errorMessage/ErrorMessage';
 
// по состоянию процесса компонент будет рендерить разные части интерфейса
export const setContent = (process, Component, data) => {
	switch (process) {
		case 'waiting':
			return <Skeleton />;
		case 'loading':
			return <Spinner />;
		case 'confirmed':
			return <Component data={data} />;
		case 'error':
			return <ErrorMessage />;
		default:
			throw new Error('Unexpected process state in this case');
	}
};

И далее тут в рендере используем setContent(). Эта функция будет каждый раз перевызываться при изменении состояния process. Так же в этом же компоненте нужно вызывать setProcess после того, как мы получили данные внутри updateChar

component > charInfo > CharInfo.js

const CharInfo = (props) => {
	const [char, setChar] = useState(null);
 
	const { getCharacter, clearError, process, setProcess } = useMarvelService();
 
	useEffect(() => {
		updateChar();
	}, [props.charId]);
 
	const updateChar = () => {
		const { charId } = props;
		if (!charId) {
			return;
		}
 
		clearError();
		getCharacter(charId)
			.then(onCharLoaded)
			// тут мы подтверждаем загрузку данных, чтобы рендер персонажа был с данными
			.then(() => setProcess('confirmed'));
	};
 
	const onCharLoaded = (char) => {
		setChar(char);
	};
 
	return <div className='char__info'>{setContent(process, View, char)}</div>;
};

Так же стоит переименовать пропс, получаемый в компоненте на тот, что передаётся внутри свича

Тут так же пришлось решать проблему с тем, что в списке во время загрузки отображалось null (это приводит к тому, что мы поднимаемся вверх страницы и заново приходится скроллить вниз по списку). Эту проблему можно было решить только переписав функцию установки контента, где мы в загрузке показываем старый список или спиннер (если старых элементов списка нет)

component > charList > CharList.js

// !!!
export const setContent = (process, Component, newItemLoading) => {
	switch (process) {
		case 'waiting':
			return <Spinner />;
		case 'loading':
			// если у нас грузятся новые элементы, то оставляем список, если нет, то показываем спиннер
			return newItemLoading ? <Component /> : <Spinner />;
		case 'confirmed':
			return <Component />;
		case 'error':
			return <ErrorMessage />;
		default:
			throw new Error('Unexpected process state in this case');
	}
};
 
const CharList = (props) => {
	const [charList, setCharList] = useState([]);
	const [newItemLoading, setnewItemLoading] = useState(false);
	const [offset, setOffset] = useState(210);
	const [charEnded, setCharEnded] = useState(false);
 
	const { loading, error, getAllCharacters, process, setProcess } = useMarvelService();
 
	useEffect(() => {
		onRequest(offset, true);
	}, []);
 
	const onRequest = (offset, initial) => {
		initial ? setnewItemLoading(false) : setnewItemLoading(true);
		getAllCharacters(offset)
			.then(onCharListLoaded)
			// !!
			.then(() => setProcess('confirmed'));
	};
 
	/// CODE ...
 
	return (
		<div className='char__list'>
			{/* !!! */}
			{setContent(process, () => renderItems(charList), newItemLoading)}
			<button
				disabled={newItemLoading}
				style={{ display: charEnded ? 'none' : 'block' }}
				className='button button__main button__long'
				onClick={() => onRequest(offset)}
			>
				<div className='inner'>load more</div>
			</button>
		</div>
	);
};

Код мы оптимизировали и компонент информации о персонаже так же работает и список персонажей тоже

Список комиксов так же переводим в стейт-машину

component > comicsList > ComicsList.js

// !!
export const setContent = (process, Component, newItemLoading) => {
	switch (process) {
		case 'waiting':
			return <Spinner />;
		case 'loading':
			return newItemLoading ? <Component /> : <Spinner />;
		case 'confirmed':
			return <Component />;
		case 'error':
			return <ErrorMessage />;
		default:
			throw new Error('Unexpected process state in this case');
	}
};
 
const ComicsList = () => {
	const [comicsList, setComicsList] = useState([]);
	const [newItemLoading, setnewItemLoading] = useState(false);
	const [offset, setOffset] = useState(0);
	const [comicsEnded, setComicsEnded] = useState(false);
 
	const { getAllComics, process, setProcess } = useMarvelService();
 
	useEffect(() => {
		onRequest(offset, true);
	}, []);
 
	const onRequest = (offset, initial) => {
		initial ? setnewItemLoading(false) : setnewItemLoading(true);
		getAllComics(offset)
			.then(onComicsListLoaded)
			// !!!
			.then(() => setProcess('confirmed'));
	};
 
	/// CODE ...
 
	return (
		<div className='comics__list'>
			{setContent(process, () => renderItems(comicsList), newItemLoading)}
			<button
				disabled={newItemLoading}
				style={{ display: comicsEnded ? 'none' : 'block' }}
				className='button button__main button__long'
				onClick={() => onRequest(offset)}
			>
				<div className='inner'>load more</div>
			</button>
		</div>
	);
};

Далее модифицируем компонент рандомного персонажа

component > randomChar > RandomChar.js

const RandomChar = () => {
	const [char, setChar] = useState(null);
	const { getCharacter, clearError, process, setProcess } = useMarvelService();
 
	useEffect(() => {
		updateChar();
		const timerId = setInterval(updateChar, 60000);
 
		return () => {
			clearInterval(timerId);
		};
	}, []);
 
	const onCharLoaded = (char) => {
		setChar(char);
	};
 
	const updateChar = () => {
		clearError();
		const id = Math.floor(Math.random() * (1011400 - 1011000)) + 1011000;
		getCharacter(id)
			.then(onCharLoaded)
			// !!!
			.then(() => setProcess('confirmed'));
	};
 
	return (
		<div className='randomchar'>
			{setContent(process, View, char)}
			<div className='randomchar__static'>
				<p className='randomchar__title'>
					Random character for today!
					<br />
					Do you want to get to know him better?
				</p>
				<p className='randomchar__title'>Or choose another one</p>
				<button onClick={updateChar} className='button button__main'>
					<div className='inner'>try it</div>
				</button>
				<img src={mjolnir} alt='mjolnir' className='randomchar__decoration' />
			</div>
		</div>
	);
};
 
const View = ({ data }) => {
	const { name, description, thumbnail, homepage, wiki } = data;
 
	/// CODE ...
};
 
export default RandomChar;

И так же можно установить машину состояний для отдельных страниц комиксов и персонажей

components > pages > SinglePage.js

const SinglePage = ({ Component, dataType }) => {
	const { id } = useParams();
	const [data, setData] = useState(null);
	const { getComic, getCharacter, clearError, process, setProcess } = useMarvelService();
 
	useEffect(() => {
		updateData();
	}, [id]);
 
	const updateData = () => {
		clearError();
 
		switch (dataType) {
			case 'comic':
				getComic(id)
					.then(onDataLoaded)
					// !!
					.then(() => setProcess('confirmed'));
				break;
			case 'character':
				getCharacter(id)
					.then(onDataLoaded)
					// !!
					.then(() => setProcess('confirmed'));
		}
	};
 
	// устанавливаем в состояние
	const onDataLoaded = (data) => {
		setData(data);
	};
 
	return (
		<>
			<AppBanner />
			{setContent(process, Component, data)}
		</>
	);
};

Ну и так же стоит переписать на стейт-машину компонент поиска персонажа

components > charSearchForm > CharSearchForm.js

// !!
export const setContent = (process, Component, updateChar) => {
	switch (process) {
		case 'waiting':
			return <Spinner />;
		case 'loading':
			return <Spinner />;
		case 'confirmed':
			return <Component updateChar={updateChar} />;
		case 'error':
			return (
				<div className='char__search-critical-error'>
					<ErrorMessage />
				</div>
			);
		default:
			throw new Error('Unexpected process state in this case');
	}
};
 
const CharSearchForm = () => {
	const [char, setChar] = useState(null);
	const { getCharacterByName, clearError, process, setProcess } = useMarvelService();
 
	// !!
	useEffect(() => setProcess('confirmed'), []);
 
	const onCharLoaded = (char) => {
		setChar(char);
	};
 
	const updateChar = (name) => {
		clearError();
 
		getCharacterByName(name)
			.then(onCharLoaded)
			// !!
			.then(() => setProcess('confirmed'));
	};
 
	const results = !char ? null : char.length > 0 ? (
		<div className='char__search-wrapper'>
			<div className='char__search-success'>There is! Visit {char[0].name} page?</div>
			<Link to={`/characters/${char[0].id}``} className='button button__secondary'>
				<div className='inner'>To page</div>
			</Link>
		</div>
	) : (
		<div className='char__search-error'>
			The character was not found. Check the name and try again
		</div>
	);
 
	return (
		<div className='char__search-form'>
			{/* !! */}
			{setContent(process, View, updateChar)}
			{results}
		</div>
	);
};
 
// !!
const View = ({ updateChar }) => {
	return (
		<Formik
			initialValues={{
				charName: '',
			}}
			validationSchema={Yup.object({
				charName: Yup.string().required('This field is required'),
			})}
			onSubmit={({ charName }) => {
				updateChar(charName);
			}}
		>
			<Form>
				<label className='char__search-label' htmlFor='charName'>
					Or find a character by name:
				</label>
				<div className='char__search-wrapper'>
					<Field id='charName' name='charName' type='text' placeholder='Enter name' />
					<button type='submit' className='button button__main'>
						<div className='inner'>find</div>
					</button>
				</div>
				<FormikErrorMessage
					component='div'
					className='char__search-error'
					name='charName'
				/>
			</Form>
		</Formik>
	);
};
 
export default CharSearchForm;

027 Разбираем ошибки сторонних библиотек и проблему с фокусом

Представленную ошибку выводит модуль react-helmet. Такие ошибки может решить только сам разработчик и реакцию сообщества на данные баги можно найти в обсуждениях на гитхабе, если загуглить ошибку

Далее в проекте появился баг, который возникает из-за подхода с конечными автоматами - у нас выделяется карточка только при втором нажатии

Чтобы найти проблему, можно поставить логгер на предполагаемом месте. После клика на персонажа у нас почему-то перерендеривается вся структура, что и не даёт анимации выделения сработать

Если выйти в компонент родителя, то можно и там оставить логгер, который нам скажет, что при выделении персонажа перерендеривается вся страница

Поэтому одним из вариантов является запоминание результата рендера, если не были изменены никакие другие стейты процесса. В данном случае нам может помочь хук useMemo()

React + Redux

Redux React

001 Основные принципы Redux. Теория

Работа с динамическими данными и со стейтом - это одна из основных задач разработчика. Если логика изменения данных написана правильно, то и их отображение будет несложной задачей.

Первое приложение у нас выглядит следующим образом:

  • Все данные хранились в одном компоненте
  • Все данные передавались по иерархии вниз, а изменения состояния передавались вверх через коллбэки
  • Так же все состояния централизованы (они все находились в одном месте - в компоненте App)

Такой подход называется Property Drill, когда мы просверливаем пути для передачи состояний по уровням через несколько компонентов. Такой подход не является достаточно логичным, так как некоторые компоненты могут хранить в себе ненужные для них состояния, которые мы просто перебрасываем дальше.

Второе приложение выглядит уже следующим образом:

  • Каждый компонент хранит своё состояние у себя (один компонент содержит список персонажей, а другой список комиксов, третий содержит информацию об одном конкретном персонаже и так далее)

Такой подход сложно масштабировать, особенно, если появятся зависимости между компонентами

Чтобы решить вышеописанные проблемы, были придуманы определённые паттерны для работы с состояниями продуктов, такие как MVC, MVP, MVVM

И чтобы решить проблему со сложными зависимостями, можно создать один большой источник стейтов для всех компонентов. Однако тут мы сталкиваемся с проблемой, что каждый компонент может поменять наш глобальный стейт

И чтобы решить уже вышеописанную проблему, был придуман следующий подход:

  • Мы имеем наши компоненты View, которые при выполнении какого-либо действия создают Actions (который уже знает, что нужно обновить в стейте)
  • Определённые события Actions (которые хранят информацию о требуемых изменениях) вызывают срабатывание определённых действий в компоненте Reducer (который уже знает, как именно обновить этот стейт). Операция передачи объекта Actions в Reducer называется dispatch
  • Компонент Reducer - это компонент, который находится в общем хранилище стейтов и он знает, что делать при любом запросе от компонентов сайта. То есть он регулирует обновление стейтов внутри S, чтобы компоненты могли перерисоваться на базе обновлённых данных
  • Компонент S так же находится внутри хранилища и сам по себе просто хранит все состояния приложения.

Так же в Redux имеются селекторы - это функции, которые получают часть данных из хранилища для дальнейшего использования (из S во View)

И вот так выглядит работа стейт-менеджера на реальном примере. Из State прошлые данные приходят в Reducer, чтобы сравнить с новыми значениями.

Примерно такой же подход использовался в хуке useReducer.

Гифка работы Redux с официального сайта

И тут важно уточнить, что запутаться в трёх разных документациях легко, поэтому нужно знать. что ищем:

Так же очень важное расширение для работы с редаксом в браузере, которое позволяет просмотреть состояния системы:

002 Основные принципы Redux. Практика

Первым делом нужно установить библиотеки редакса в приложение

npm i redux react-redux

Дальше распишем базовую схему, которая будет соответствовать архитектуре работы редакса:

  • начальное состояние
  • функция-редьюсер
  • стейт
// начальное состояние
const initialState = 0;
 
// функция изменения стейта
const reducer = (state, action) => {
	if (action.type === 'INC') {
		return state + 1;
	}
	return 0;
};
 
// стейт
const state = reducer(initialState, { type: 'INC' });
 
console.log('State after reducer = ' + state);

Уже таким образом функция редьюсера будет написана лаконичнее, так как действий может быть множество

const reducer = (state = 0, action) => {
	switch (action.type) {
		case 'INC':
			return state + 1;
		case 'DEC':
			return state - 1;
		default:
			return state;
	}
 
	return 0;
};

И тут нужно сказать, что стоит установить дефолтное значение, так как в функцию может попасть undefined, что может привести к ошибке

Ну и далее создадим единый стор, который уже принимает в себя функцию-редьюсер. Обычно в приложении располагается только один стор

import { createStore } from 'redux';
 
// начальное состояние
const initialState = 0;
 
// функция изменения стейта
const reducer = (state = 0, action) => {
	switch (action.type) {
		case 'INC':
			return state + 1;
		case 'DEC':
			return state - 1;
		default:
			return state;
	}
 
	return 0;
};
 
// это стор, который хранит функцию-редьюсер и все стейты
const store = createStore(reducer);
 
// вызываем функцию reducer и передаём в неё значение
store.dispatch({ type: 'INC' });
 
console.log('Value in state = ' + store.getState());

Так же мы можем реализовать подписку на изменения в сторе, что позволит контролировать изменение состояний в приложении

// начальное состояние
const initialState = 0;
 
// функция изменения стейта
const reducer = (state = 0, action) => {
	switch (action.type) {
		case 'INC':
			return state + 1;
		case 'DEC':
			return state - 1;
		default:
			return state;
	}
 
	return 0;
};
 
// это стор, который хранит функцию-редьюсер и все стейты
const store = createStore(reducer);
 
// подписка позволяет вызывать функцию, которая будет срабатывать каждый раз при изменении стейта внутри стора
store.subscribe(() => {
	console.log('Value in state = ' + store.getState());
});
 
// вызываем функцию reducer и передаём в неё значение
store.dispatch({ type: 'INC' });
store.dispatch({ type: 'INC' });

Важные правила работы с Reducer:

  • Эта функция должна быть чистой и зависеть только от приходящего в неё стейта и экшена
  • Она должна возвращать один и тот же результат при одинаковых аргументах и не иметь никаких побочных эффектов (никаких логов, запросов на сервер, генераций случайных чисел и никакой работы с ДОМ-деревом)

Вёрстка кнопок

<div id="rooter" class="jumbotron">
	<h1 id="counter">0</h1>
	<button id="dec" class="btn btn-primary1">DEC</button>
	<button id="inc" class="btn btn-primary1">INC</button>
	<button id="rnd" class="btn btn-primary1">RND</button>
</div>

Стили

.jumbotron {
  display: flex;
  justify-content: center;
  align-items: center;
 
  width: 99vh;
  height: 99vh;
}

И использование редакса на странице:

// начальное состояние
const initialState = 0;
 
// функция изменения стейта
const reducer = (state = 0, action) => {
	switch (action.type) {
		case 'INC':
			return state + 1;
		case 'DEC':
			return state - 1;
		case 'RND':
			return state * action.payload;
		default:
			return state;
	}
 
	return 0;
};
 
// это стор, который хранит функцию-редьюсер и все стейты
const store = createStore(reducer);
 
const updateCounter = () => {
	document.getElementById('counter').textContent = store.getState();
};
 
// подписка позволяет вызывать функцию, которая будет срабатывать каждый раз при изменении стейта внутри стора
store.subscribe(updateCounter);
 
document.getElementById('inc').addEventListener('click', () => {
	store.dispatch({ type: 'INC' });
});
 
document.getElementById('dec').addEventListener('click', () => {
	store.dispatch({ type: 'DEC' });
});
 
document.getElementById('rnd').addEventListener('click', () => {
	const value = Math.floor(Math.random() * 10);
	store.dispatch({ type: 'RND', payload: value });
});

Отдельно нужно сказать, что так делать нельзя и выше было показано, что мы передали это значение через свойство payload (полезная нагрузка)

И так же в Redux используется actionCreator функция, которая генерирует экшены. Они используются для более безопасного применения редьюсера, чтобы он возвращает не стейт по дефолтному проходу, а ошибку, если мы передали неправильный объект

// actionCreater'ы, которые создают экшены для редьюсера
const inc = () => ({ type: 'INC' });
const dec = () => ({ type: 'DEC' });
const rnd = (value) => ({ type: 'RND', payload: value });
 
document.getElementById('inc').addEventListener('click', () => {
	store.dispatch(inc());
});
 
document.getElementById('dec').addEventListener('click', () => {
	store.dispatch(dec());
});
 
document.getElementById('rnd').addEventListener('click', () => {
	const value = Math.floor(Math.random() * 10);
	store.dispatch(rnd(value));
});

Но так же мы будем часто работать с данными в виде объекта, поэтому и писать придётся код соблюдая иммутабельность:

  • переводим начальный стейт в объект
  • меняем редьюсер на работу со стейтом по принципу иммутабельности (разворачиваем старый объект и добавляем новые данные)
  • далее из стора нужно будет получить не целый объект, а одно значение store.getState().value
// начальное состояние
const initialState = { value: 0 };
 
// функция изменения стейта
const reducer = (state = initialState, action) => {
	switch (action.type) {
		case 'INC':
			return { ...state, value: state.value + 1 };
		case 'DEC':
			return { ...state, value: state.value - 1 };
		case 'RND':
			return { ...state, value: state.value * action.payload };
		default:
			return state;
	}
 
	return 0;
};
 
// это стор, который хранит функцию-редьюсер и все стейты
const store = createStore(reducer);
 
const updateCounter = () => {
	document.getElementById('counter').textContent = store.getState().value;
};
 
// подписка позволяет вызывать функцию, которая будет срабатывать каждый раз при изменении стейта внутри стора
store.subscribe(updateCounter);
 
// actionCreater'ы, которые создают экшены для редьюсера
const inc = () => ({ type: 'INC' });
const dec = () => ({ type: 'DEC' });
const rnd = (value) => ({ type: 'RND', payload: value });
 
document.getElementById('inc').addEventListener('click', () => {
	store.dispatch(inc());
});
 
document.getElementById('dec').addEventListener('click', () => {
	store.dispatch(dec());
});
 
document.getElementById('rnd').addEventListener('click', () => {
	const value = Math.floor(Math.random() * 10);
	store.dispatch(rnd(value));
});

Итог: мы имеем каунтер построенный на базе отслеживания состояния через редакс даже без использования React на чистом JS

003 Чистые функции

Понятие чистой функции исходит из обычного программирования и там это имеется ввиду, когда говорят про прозрачность работы функции

Особенности чистых функций:

  • При одинаковых данных они всегда возвращают одинаковый результат
  • Она не вызывает внутри себя побочных эффектов

Представленная функция всегда будет возвращать разные значения ровно по той причине, что она всегда выполняет в себе побочное действие (генерирует рандомное число)

И теперь, когда мы переделали функцию таким образом, она является чистой ровно потому, что при передаче одних и тех же аргументов она всегда будет возвращать тот же результат

Так же тут нужно понимать, что все зависимости должны находиться внутри данной функции - значений извне она принимать не может

Побочные действия, которые нельзя использовать в чистых функциях:

  • Все асинхронные операции (запросы на сервер, изменение файлов)
  • Получение рандомного значения
  • Вывод логов
  • Работа с ДОМ-деревом
  • Видоизменение входных данных (это нарушение иммутабельности)

004 Оптимизация через actionCreators и bindActionCreator

Далее попробуем разбить приложение на отдельные файлы

Экшены вынесем в отдельный файл

actions.js

export const inc = () => ({ type: 'INC' });
export const dec = () => ({ type: 'DEC' });
export const rnd = (value) => ({ type: 'RND', payload: value });

Сам редьюсер уберём в другой файл

reducer.js

const initialState = { value: 0 };
 
const reducer = (state = initialState, action) => {
	switch (action.type) {
		case 'INC':
			return { ...state, value: state.value + 1 };
		case 'DEC':
			return { ...state, value: state.value - 1 };
		case 'RND':
			return { ...state, value: state.value * action.payload };
		default:
			return state;
	}
 
	return 0;
};
 
export default reducer;

И далее основную логику приложения оптимизируем:

  • деструктуризируем и достанем из стора повторяющиеся функции dispatch, subscribe и getState
  • Ивентлистенеры повторяют одну и ту же вложенную функцию - вызывают экшен-функцию внутри dispatch. Это поведение можно оптимизировать и вынести в отдельную функцию-диспэтчер (incDispatch и так далее)

index.js

import { createStore } from 'redux';
import reducer from './reducer';
import { dec, inc, rnd } from './actions';
 
const store = createStore(reducer);
const { dispatch, subscribe, getState } = store;
 
const updateCounter = () => {
	document.getElementById('counter').textContent = getState().value;
};
 
subscribe(updateCounter);
 
const incDispatch = () => dispatch(inc());
const decDispatch = () => dispatch(dec());
const rndDispatch = (value) => dispatch(rnd(value));
 
document.getElementById('inc').addEventListener('click', incDispatch);
document.getElementById('dec').addEventListener('click', decDispatch);
document.getElementById('rnd').addEventListener('click', () => {
	const value = Math.floor(Math.random() * 10);
	rndDispatch(value);
});

Однако очень часто разработчики для простоты использования кода создавали функцию bindActionCreator, которая возвращала уже сбинженную функцию диспэтча для вызова в других местах

index.js

const store = createStore(reducer);
const { dispatch, subscribe, getState } = store;
 
const updateCounter = () => {
	document.getElementById('counter').textContent = getState().value;
};
 
subscribe(updateCounter);
 
const bindActionCreator =
	(creator, dispatch) =>
	(...args) =>
		dispatch(creator(...args));
 
const incDispatch = bindActionCreator(inc, dispatch);
const decDispatch = bindActionCreator(dec, dispatch);
const rndDispatch = bindActionCreator(rnd, dispatch);
 
document.getElementById('inc').addEventListener('click', incDispatch);
document.getElementById('dec').addEventListener('click', decDispatch);
document.getElementById('rnd').addEventListener('click', () => {
	const value = Math.floor(Math.random() * 10);
	rndDispatch(value);
});

Однако в редаксе уже есть подобная функция bindActionCreators, которая за нас создаёт подобный связыватель

index.js

import { createStore, bindActionCreators } from 'redux';
 
const incDispatch = bindActionCreators(inc, dispatch);
const decDispatch = bindActionCreators(dec, dispatch);
const rndDispatch = bindActionCreators(rnd, dispatch);

Так же мы можем сделать привязку нескольких функций через одну функцию bindActionCreators, но уже через объект

index.js

const { incDispatch, decDispatch, rndDispatch } = bindActionCreators(
	{
		incDispatch: inc,
		decDispatch: dec,
		rndDispatch: rnd,
	},
	dispatch,
);
 
document.getElementById('inc').addEventListener('click', incDispatch);
document.getElementById('dec').addEventListener('click', decDispatch);
document.getElementById('rnd').addEventListener('click', () => {
	const value = Math.floor(Math.random() * 10);
	rndDispatch(value);
});

Можно ещё сильнее сократить запись, если импортировать не все именованные импорты по отдельности, а импортировать целый объект и его вложить первым аргументом

index.js

// import { dec, inc, rnd } from './actions';
import * as actions from './actions';
 
const { inc, dec, rnd } = bindActionCreators(actions, dispatch);

005 Добавим React в проект

Сначала выделим компонент счётчика в отдельный реакт-компонент

Counter.js

import React from 'react';
import './counter.css';
 
const Counter = ({ counter, inc, dec, rnd }) => {
	const styles = {
		display: 'flex',
		justifyContent: 'center',
		alignItems: 'center',
		width: '99vh',
		height: '99vh',
	};
 
	return (
		<div style={styles}>
			<h1>{counter}</h1>
			<button className='btn' onClick={dec}>
				DEC
			</button>
			<button className='btn' onClick={inc}>
				INC
			</button>
			<button className='btn' onClick={rnd}>
				RND
			</button>
		</div>
	);
};
 
export default Counter;

Далее передадим все функции, которые нужны для работы компонента и обернём рендер реакт-компонента в функцию update, которая будет вызваться через subscribe, когда у нас обновится значение в редаксе

Тут нужно отметить, что такой подход не используется в реальных проектах

index.js

const store = createStore(reducer);
const { dispatch, subscribe, getState } = store;
const { inc, dec, rnd } = bindActionCreators(actions, dispatch);
 
const root = ReactDOM.createRoot(document.getElementById('root'));
 
const update = () => {
	root.render(
		<React.StrictMode>
			<Counter
				counter={getState().value}
				dec={dec}
				inc={inc}
				rnd={() => {
					const value = Math.floor(Math.random() * 10);
					rnd(value);
				}}
			/>
		</React.StrictMode>,
	);
};
 
update();
 
subscribe(update);

И счётчик работает

И сейчас подготовим проект для того, чтобы он мог работать вместе с редаксом:

Первым делом нужно убрать все импорты и экспорты разных функций и экшенов. Единственное, что нам нужно - это создать глобальное хранилище createStore и закинуть в него reducer. Далее нам нужно вложить все компоненты приложения в Provider, который отслеживает все изменения стора и распространяет данные по приложению. В провайдер нужно передать будет и сам store

Тут нужно упомянуть, что провайдер сам отслеживает изменения и сам сигнализирует компонентам, что данные были изменены

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
 
import { createStore, bindActionCreators } from 'redux';
import { Provider } from 'react-redux';
 
import reducer from './reducer';
import App from './components/App';
import './index.css';
 
// глобальное хранилище
const store = createStore(reducer);
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
	<React.StrictMode>
		<Provider store={store}>
			<App />
		</Provider>
	</React.StrictMode>,
);

И далее нужно просто вызвать компонент счётчика внутри App

components > App.js

import React from 'react';
import Counter from './Counter';
 
const App = () => {
	return <Counter />;
};
 
export default App;

Далее, чтобы приложение заработало, нужно будет с помощью connect распространить данные по всем компонентам приложения. Это позволит прокинуть в Counter нужные функции и данные для работы со счётчиком. Пока же компонент не работает без данных манипуляций.

006 Соединяем React и Redux при помощи connect

Подключить Redux к React можно двумя способами:

  • функция connect, которая используется в классовых компонентах и в старых проектах
  • хук useSelector

Преимущества и недостатки использования хука:

  • меньше бойлерплейта
  • сложнее тестировать, чем обычный коннект
  • проще в понимании
  • может быть баг с “Зомби детьми”
  • более корректен в плане написания кода, но менее производительный

Далее нужно реализовать контроль состояния в нашем каунтере

Для начала, можно перенести логику по генерации рандомного значения прямо в actionCreator-функцию

actions.js

export const inc = () => ({ type: 'INC' });
export const dec = () => ({ type: 'DEC' });
export const rnd = () => ({ type: 'RND', payload: Math.floor(Math.random() * 10) });

Первым делом, нужно обернуть вывод компонента в функцию connect, получаемую из реакт-редакса. Передаётся функция во вторые скобочки (аргументы вложенного ретёрна). В первые скобки уже будут приниматься аргументы самого коннекта

Работает connect по следующей цепочке:

  • внутри приложения какой-либо компонент задиспетчил (изменил стейт) какое-либо действие
  • глобальное состояние изменилось
  • провайдер отлавливает изменение и даёт сигнал всем компонентам, которые находятся внутри
  • дальше запускается connect от провайдера
  • запускается функция mapStateToProps
  • и если пропсы компонента поменялись, то весь компонент будет перерисован

И далее создадим две функции mapStateToProps и mapDispatchToProps, чтобы получить:

  • из первой функции значение из стора
  • с помощью второй функции сгенерировать три функции-диспэтча

Counter.js

import React from 'react';
import './counter.css';
import { connect } from 'react-redux';
import * as actions from '../actions';
import { bindActionCreators } from 'redux';
 
const Counter = ({ counter, inc, dec, rnd }) => {
	return (
		<div className='wrapper'>
			<h1>{counter}</h1>
			<button className='btn' onClick={dec}>
				DEC
			</button>
			<button className='btn' onClick={inc}>
				INC
			</button>
			<button className='btn' onClick={rnd}>
				RND
			</button>
		</div>
	);
};
 
// эта функция будет вытаскивать нужные пропсы для нашего компонента и передавать их в него
// она принимает в себя глобальный стейт, который описан в index.js
const mapStateToProps = (state) => {
	// возвращает объект со свойствами, которые нужно вытащить из стейта
	return { counter: state.value };
};
 
// данная функция передаёт внутрь коннекта функции-диспетчи
const mapDispatchToProps = (dispatch) => {
	return bindActionCreators(actions, dispatch);
};
 
export default connect(mapStateToProps, mapDispatchToProps)(Counter);

Функция коннекта принимает в себя 4 необязательных значения:

  • mapStateToProps в виде функции, которая запросит данные из стейта
  • mapDispatchToProps в виде функции, которая сгенерирует объект с диспетчами или в виде объекта (коннект сам распарсит объект и сделает из него нужные функции)
  • mergeProps и options используются для оптимизации работы функции connect
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

Функция mapStateToProps применяется для получения данных из стейта и используется внутри коннектора. Она должна быть чистой и синхронной, как функция-редьюсер

То есть функция будет получать данное начальное установленное значение

Функция mapDispatchToProps уже имеет предназначение формировать в себе нужные диспэтчи под определённые компоненты

Тут так же нужно сказать, что у нас есть 4 варианта реализации данной функции в зависимости от степени абстракции (первые три функции реализованы с учётом неизменённого actionCreatorа):

import { inc, dec, rnd } from '../actions';
 
// данная функция передаёт внутрь коннекта функции-диспетчи
const mapDispatchToProps = (dispatch) => {
	return {
		inc: () => dispatch(inc()),
		dec: () => dispatch(dec()),
		rnd: () => {
			const value = Math.floor(Math.random() * 10);
			dispatch(rnd(value));
		},
	};
};
 
/// ИЛИ ...
 
import * as actions from '../actions';
import { bindActionCreators } from 'redux';
 
const mapDispatchToProps = (dispatch) => {
	const { inc, dec, rnd } = bindActionCreators(actions, dispatch);
 
	return {
		inc,
		dec,
		rnd: () => {
			const value = Math.floor(Math.random() * 10);
			rnd(value);
		},
	};
};
 
/// ИЛИ ...
import * as actions from '../actions';
import { bindActionCreators } from 'redux';
 
const mapDispatchToProps = (dispatch) => {
	const { inc, dec, rnd } = bindActionCreators(actions, dispatch);
 
	return {
		inc,
		dec,
		rnd,
	};
};
 
/// ИЛИ...
const mapDispatchToProps = (dispatch) => {
	return bindActionCreators(actions, dispatch);
};

Однако дальше нужно сказать, что вторым аргументом connect может получить не только функцию, где мы сами разбиваем actionCreator'ы, а просто передать объект, который уже функция коннекта сама разберёт

Однако такой подход работает только тогда, когда нам не нужно проводить дополнительные манипуляции над actionCreatorами

Counter.js

import * as actions from '../actions';
 
export default connect(mapStateToProps, actions)(Counter);

Итог: каунтер наконец-то работает

Ну и так выглядит функция с использованием классового компонента:

007 Соединяем React и Redux при помощи хуков

Так же куда более простым способом в реализации подключения редакса к реакту будет использование хуков:

  • useSelector - позволяет получить из глобального хранилища (стора) нужное нам состояние
  • useDispatch - предоставляет доступ к функции dispatch

Counter.js

import { useDispatch, useSelector } from 'react-redux';
import { inc, dec, rnd } from '../actions';
 
const Counter = () => {
	// эта функция позволяет получить состояние из стора
	const { counter } = useSelector((state) => state);
 
	// эта функция отвечает за генерацию функций-диспэтчей
	const dispatch = useDispatch();
 
	return (
		<div className='wrapper'>
			<h1>{counter}</h1>
			<button className='btn' onClick={() => dispatch(dec())}>
				DEC
			</button>
			<button className='btn' onClick={() => dispatch(inc())}>
				INC
			</button>
			<button className='btn' onClick={() => dispatch(rnd())}>
				RND
			</button>
		</div>
	);
};
 
export default Counter;

Отличия useSelector от mapStateToProps:

  • хук возвращает всё, что угодно, а не только то, что идёт на пропсы
  • коллюэк функция позволяет сделать всё, что угодно с данными, но она должна оставаться чистой и синхронной
  • в само значение, которое вызывает функцию, может помещаться что угодно (строка, массив, функция и так далее)
  • в хуке отсутствует свойство ownProp, которое используется для передачи собственных пропсов для отслеживания
  • при срабатывании диспэтч-функции, хук сам проверяет не изменились ли данные, которые он возвращает. Тут уже проверка проходит не по всему объекту, как в обычной функции, а по отдельным полям объекта (если мы сразу возвращаем объект, но если мы возвращаем через return, то тут уже будет проходить проверка по всему объекту)
  • Так же хук при изменении стейта в сторе будет вызывать перерендер компонента

Так же, когда мы возвращаем из функции новый объект, то у нас каждый раз будет создаваться новый объект, что будет вызывать перерендеры компонента. Чтобы избавиться от данной ошибки, можно:

  • просто дублировать использование хука useSelector при запросе отдельных свойств из стора
  • использовать функцию Reselect из сторонней библиотеки
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
 
const selectNumCompletedTodos = createSelector(
  (state) => state.todos,
  (todos) => todos.filter((todo) => todo.completed).length
)
 
export const CompletedTodosCounter = () => {
  const numCompletedTodos = useSelector(selectNumCompletedTodos)
  return <div>{numCompletedTodos}</div>
}
 
export const App = () => {
  return (
    <>
      <span>Number of completed todos:</span>
      <CompletedTodosCounter />
    </>
  )
}
  • либо можно использовать функцию shallowEqual:
import { shallowEqual, useSelector } from 'react-redux'
 
// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)

Если мы говорим про хук useDispatch, то тут нужно упомянуть, что при передаче его дальше по иерархии в нижние компоненты, нужно обернуть его в useCallback, чтобы каждый раз не пересоздавался диспэтч. Дело в том, что пересоздание диспэтча будет вызывать пересоздание и самого компонента.

const incrementCounter = useCallback(
	() => dispatch({ type: 'increment-counter' }),
	[dispatch]
)

Так же существует хук useStore, который возвращает полностью весь объект стора, но им пользоваться не стоит

import React from 'react'
import { useStore } from 'react-redux'
 
export const CounterComponent = ({ value }) => {
  const store = useStore()
 
  // ТОЛЬКО ПРИМЕР! Такое делать в реальном примере нельзя
  // Компонент не будет автоматически обновлён, если стор будет изменён
  return <div>{store.getState()}</div>
}

В конце стоит отметить, что показанный в начале пример использования компонента с хуками - стоит использовать как конечный вариант. Не стоит использовать оборачивать хуки редакса в дополнительные хуки.

Zombie Childrens

  • zombie children: дочерние компоненты, о которых родитель ничего не знает
  • stale props: протухшие свойства - свойства, которые не являются актуальными в данный конкретный момент времени

Большинство разработчиков даже не представляют себе что это такое и когда это может возникнуть.

zombie children — давняя проблема попытки синхронизировать внешнее синхронное хранилище состояния (react-redux) с асинхронным циклом рендеринга React.

Проблема кроется в порядке возникновения события ComponentDidMount/useEffect у компонентов React при их монтировании в дерево компонент в иерархиях родитель-дети, в ситуации, когда эта связка компонент отображает структуры данных типа “список” или “дерево” и эти компоненты подписаны на изменения в источнике данных, который находится вне контекста React.

Для начала давайте рассмотрим типичный PubSub объект

function createStore(reducer) {
    var state;
    var listeners = [];
 
    function getState() {
        return state;
    }
 
    function subscribe(listener) {
        listeners.push(listener)
        return function unsubscribe() {
            var index = listeners.indexOf(listener);
            listeners.splice(index, 1);
        }
    }
 
    function dispatch(action) {
        state = reducer(state, action);
        listeners.forEach(listener => listener(state));
    }
 
    dispatch({});
 
    return { dispatch, subscribe, getState };
}

Глядя на listeners мы должны понимать одно: так как элементы массива хранятся в том порядке в котором они добавлялись — коллбэки подписчиков вызываются ровно в том порядке, в котором происходили подписки.

Теперь давайте посмотрим в каком порядке происходит монтирование компонент в иерархии компонент родитель-дети:

<A>
    <B />
    <C />
</A>

Если каждому компоненту в ComponentDidMount добавить запись в консоль имени монтируемого компонента, то мы увидим следующее:

mounting component B
mounting component C
mounting component A

Обратите внимание: родительский компонент А монтируется после своих детей (его метод ComponentDidMount вызывается последним)!

Рассмотрим использования redux контейнеров:

state = {
  list: [
    1: { id: 1, title: 'Component1', text: '...' },
    2: { id: 2, title: 'Component2', text: '...' }
  }
};
 
const List = ({ list }) => {
  return list.map(item =>
      <ListItemContainer id={item.id} title={item.title} />);
}
 
const ListContainer = connect()(List);

Если в каждом из компонентов, в методе ComponentDidMount происходит подписка subscribe на оповещение об изменении данных, то при возникновении изменений в источнике данных сначала будут вызваны коллбеки у дочерних компонент и лишь затем — у компонента-родителя.

Теперь представим, что в источнике данных мы удалили данные:

1: { id: 1, title: 'Component1', text: '...' },

Первым будет вызван коллбэк для компонента ListItemContainer с id=1 (так как он до изменения данных первым монтировался и первым подписался), компонент пойдет в источник данных за данными для отрисовки, а данных там для него уже нет!
Попытка получения данных, путем обращения

const { someProp } = store.list[1];

приведет к краху приложения с ошибкой типа “Uncaught TypeError: Cannot read property ‘1’ of undefined.” или подобной (ошибки может и не быть если сначала проверить на существование элемент в сторе, но компонент в дереве присутствует и он — зомби);

Оказывается, что компонент с id=1 в данный момент не является дочерним для компонента-контейнера ListContainer, хотя на момент возникновения изменений в источнике данных он находится в дереве DOM— зомби-ребенок

В некоторых ситуация эти брошенные дочерние компоненты могут остаться в дереве даже после перерисовки родителя.

С zombie children разобрались. Теперь пора выяснить что такое stale props.

Рассматривая последний пример: давайте представим что для элемента с id=1 мы в источнике данных поменяли title. Что произойдет?

Сработает коллбэк для компонента ListContainer с id=1, он начнет перерисовку и отобразит title, который был ему передан в свойствах компонентом ListContainer, до изменений в данных — title в данном случае является stale props!

Почему же многие разработчики этого не знают? Потому что эти проблемы от них тщательно скрывают!! 😊

К примеру, разработчики react-redux поступают следующим образом — оборачивают отрисовку дочерних компонент в try…catch, при возникновении ошибки — они устанавливают счетчик ошибок в 1 и вызывают перерисовку родителя. Если в результате перерисовки родителя и последующей перерисовке дочерних компонент снова возникает ошибка и счетчик > 0 — значит это не zombie children, а что-то более серьезное, поэтому они прокидывают эту ошибку наружу. Если ошибка не повторилась — это был зомби-ребенок и после перерисовки родителя он пропадет.
Есть и другой вариант — изменяют порядок подписки так, чтобы родитель всегда подписывался на изменения раньше чем дочерние компоненты.

Но, к сожалению, даже такие попытки не всегда спасают — в react-redux предупреждают, что при использовании их хуков все же могут возникать указанные проблемы с zombie children & stale props, т.к. у них происходит подписка на события стора в хуке useEffect (что равнозначно componentDidMount), но в отличие от HOCа connect - не кому исправлять порядок подписки и обрабатывать ошибки.

Пример с zombie children

Во избежание этих проблем советуют:

  • Не полагаться на свойства компонента в селекторе при получении данных из источника
  • В случае если без использования свойств компонента не возможно выбрать данные из источника — пытайтесь выбирать данные безопасно: вместо state.todos[props.id].name используйте todo = state.todos[props.id для начала и затем после проверки на существование todo используйте todo.name
  • чтобы избежать появления stale props — передавайте в дочерние контейнеры только ключевые свойства, по которым осуществляется выборка всех остальных свойств компонента из источника — все свойства всегда будут свежими

При разработке своих библиотек и компонент React, разработчику всегда нужно помнить об обратном порядке генерации события жизненного цикла ComponentDidMount родителя и детей в случае использования подписок на события одного источника данных, когда данные хранятся вне контекста React, чтобы не возникали ошибки данного рода.

Но лучший совет — хранить данные внутри контекста исполнения React в хуке useState или в Context /useContext— вы никогда не столкнетесь с вышеописанными проблемами т.к. в функциональных компонентах вызов этих хуков происходит в естественном порядке — сначала у родителя, а затем — у детей.

008 Redux devtools

Когда мы работаем со старым АПИ редакса, нужно использовать вторым аргументом данную строку, чтобы подключить тулзы разработчика (если пишем на современном, то обойтись можно и без этого)

// глобальное хранилище
const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());

И примерно таким образом выглядит интерфейс тулза:

Так же мы можем просмотреть список переходов изменений разных стейтов в виде графа

И самая частоиспользуемая вкладка просмотра разницы между состояниями

Ну и так же отображается список выполненных экшенов

Так же присутствует таймлайн для отмотки состояний в приложении

009 Правило названия action и домашнее задание (мини-экзамен)

Структура проекта выглядит примерно следующим образом:

Это тот файл приложения, который будет шэриться через json-server и от которого будут выводиться новые посты

heroses.json

{
    "heroes": [
        {
            "id": 1,
            "name": "Первый герой",
            "description": "Первый герой в рейтинге!",
            "element": "fire"
        },
        {
            "id": 2,
            "name": "Неизвестный герой",
            "description": "Скрывающийся в тени",
            "element": "wind"
        },
        {
            "id": 3,
            "name": "Морской герой",
            "description": "Как аквамен, но не из DC",
            "element": "water"
        }
    ],
    "filters": [
        "all",
        "fire",
        "water",
        "wind",
        "earth"
    ]
}

Стор редакса

src > store > index.js

import { createStore } from 'redux';
import reducer from '../reducers';
 
const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
 
export default store;

Редьюсер редакса. Пока он один, но в дальнейшем будет пополняться их количество.

Все типы экшенов должны быть написаны заглавными буквами. Если они относятся к запросам на сервер, то мы имеем состояние отправки запроса на сервер, полученного ответа от сервера или ошибки.

Второй кейс редьюсера так же в качестве payload принимает в себя список героев, который будет отображаться на странице

src > reducer > index.js

const initialState = {
    heroes: [], // герои
    heroesLoadingStatus: 'idle', // начальный статус загрузки
    filters: [] // фильтры просмотра
}
 
const reducer = (state = initialState, action) => {
    switch (action.type) {
        case 'HEROES_FETCHING':
            return {
                ...state,
                heroesLoadingStatus: 'loading'
            }
        case 'HEROES_FETCHED':
            return {
                ...state,
                heroes: action.payload,
                heroesLoadingStatus: 'idle'
            }
        case 'HEROES_FETCHING_ERROR':
            return {
                ...state,
                heroesLoadingStatus: 'error'
            }
        default: return state
    }
}
 
export default reducer;

А уже тут описаны экшены редакса.

Экшен heroesFetched принимает в себя так же список героев, который пришёл от сервера и сохраняет его в состояние.

src > actions > index.js

export const heroesFetching = () => {
    return {
        type: 'HEROES_FETCHING'
    }
}
 
export const heroesFetched = (heroes) => {
    return {
        type: 'HEROES_FETCHED',
        payload: heroes
    }
}
 
export const heroesFetchingError = () => {
    return {
        type: 'HEROES_FETCHING_ERROR'
    }
}

Хук отправки запроса на сервер будет возвращать один ответ от сервера

src > hooks > http.hook.js

import { useCallback } from "react";
 
export const useHttp = () => {
    // const [process, setProcess] = useState('waiting');
 
    const request = useCallback(async (url, method = 'GET', body = null, headers = {'Content-Type': 'application/json'}) => {
 
        // setProcess('loading');
 
        try {
            const response = await fetch(url, {method, body, headers});
 
            if (!response.ok) {
                throw new Error(`Could not fetch ${url}, status: ${response.status}`);
            }
 
            const data = await response.json();
 
            return data;
        } catch(e) {
            // setProcess('error');
            throw e;
        }
    }, []);
 
    // const clearError = useCallback(() => {
        // setProcess('loading');
    // }, []);
 
    return {
	    request,
		// clearError,
		// process,
		// setProcess
    }
}

Тут уже располагается вся основная часть приложения

src > index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
 
import App from './components/app/App';
import store from './store';
 
import './styles/index.scss';
 
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

Это основной компонент App

src > components > app > App.js

import HeroesList from '../heroesList/HeroesList';
import HeroesAddForm from '../heroesAddForm/HeroesAddForm';
import HeroesFilters from '../heroesFilters/HeroesFilters';
 
import './app.scss';
 
const App = () => {
 
    return (
        <main className="app">
            <div className="content">
                <HeroesList/>
                <div className="content__interactive">
                    <HeroesAddForm/>
                    <HeroesFilters/>
                </div>
            </div>
        </main>
    )
}
 
export default App;

Чтобы запустить два сервера вместе (react и json-server), нужно будет установить дополнительную библиотеку, которая позволяет запустить две команды одновременно:

npm i concurrently

И так теперь выглядит сдвоенный запрос:

package.json

"start": "concurrently \"react-scripts start\" \"npx json-server heroes.json --port 3001\"",

Это компонент, который выводит список элементов карточек героев

components > heroesList > HeroesList.js

import { useHttp } from '../../hooks/http.hook';
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
 
import { heroesFetching, heroesFetched, heroesFetchingError } from '../../actions';
import HeroesListItem from '../heroesListItem/HeroesListItem';
import Spinner from '../spinner/Spinner';
 
// список персонажей
const HeroesList = () => {
	// из глобального хранилища получаем героев и статус их загрузки
	const { heroes, heroesLoadingStatus } = useSelector((state) => state);
	const dispatch = useDispatch(); // получаем диспетч
	const { request } = useHttp(); // получаем хук запроса на сервер
 
	// при загрузке страницы
	useEffect(() => {
		// устанавливаем состояние в загрузку
		dispatch(heroesFetching());
 
		// отправляем запрос на сервер на получение персонажей
		request('http://localhost:3001/heroes')
			.then((data) => dispatch(heroesFetched(data))) // герои получены
			.catch(() => dispatch(heroesFetchingError())); // получили ошибку с сервера
	}, []);
 
    // если герои загружаются
	if (heroesLoadingStatus === 'loading') {
		// то возвращаем загрузку
        return <Spinner />;
 
        // если ошибка
	} else if (heroesLoadingStatus === 'error') {
        // то возвращаем ошибку
		return <h5 className='text-center mt-5'>Ошибка загрузки</h5>;
	}
 
    // рендер списка героев
	const renderHeroesList = (arr) => {
		if (arr.length === 0) {
			return <h5 className='text-center mt-5'>Героев пока нет</h5>;
		}
 
		return arr.map(({ id, ...props }) => {
			return <HeroesListItem key={id} {...props} />;
		});
	};
 
    // элементы списка героев
	const elements = renderHeroesList(heroes);
 
    // возвращаем список героев
    return <ul>{elements}</ul>;
};
 
export default HeroesList;

А это компонент самой карточки

components > heroesListItem > HeroesListItem.js

const HeroesListItem = ({name, description, element}) => {
    // тут будет храниться класс, который попадёт в айтем
    let elementClassName;
 
    // тут мы присваиваем класс по выбранному элементу
    switch (element) {
        case 'fire':
            elementClassName = 'bg-danger bg-gradient';
            break;
        case 'water':
            elementClassName = 'bg-primary bg-gradient';
            break;
        case 'wind':
            elementClassName = 'bg-success bg-gradient';
            break;
        case 'earth':
            elementClassName = 'bg-secondary bg-gradient';
            break;
        default:
            elementClassName = 'bg-warning bg-gradient';
    }
 
    return (
        <li
            className={`card flex-row mb-4 shadow-lg text-white ${elementClassName}``}>
            <img src="http://www.stpaulsteinbach.org/wp-content/uploads/2014/09/unknown-hero.jpg"
                 className="img-fluid w-25 d-inline"
                 alt="unknown hero"
                 style={{'objectFit': 'cover'}}/>
            <div className="card-body">
 
                <h3 className="card-title">{name}</h3>
                <p className="card-text">{description}</p>
            </div>
            <span className="position-absolute top-0 start-100 translate-middle badge border rounded-pill bg-light">
                <button type="button" className="btn-close btn-close" aria-label="Close"></button>
            </span>
        </li>
    )
}
 
export default HeroesListItem;

Тут уже находится вёрстка компонента смена активностей классов:

components > heroesFilters > HeroesFilters.js

const HeroesFilters = () => {
    return (
        <div className="card shadow-lg mt-4">
            <div className="card-body">
                <p className="card-text">Отфильтруйте героев по элементам</p>
                <div className="btn-group">
                    <button className="btn btn-outline-dark active">Все</button>
                    <button className="btn btn-danger">Огонь</button>
                    <button className="btn btn-primary">Вода</button>
                    <button className="btn btn-success">Ветер</button>
                    <button className="btn btn-secondary">Земля</button>
                </div>
            </div>
        </div>
    )
}
 
export default HeroesFilters;

Тут представлена вёрстка формы для добавления персонажей без логики

components > heroesAddForm > HeroesAddForm.js

const HeroesAddForm = () => {
    return (
        <form className="border p-4 shadow-lg rounded">
            <div className="mb-3">
                <label htmlFor="name" className="form-label fs-4">Имя нового героя</label>
                <input
                    required
                    type="text"
                    name="name"
                    className="form-control"
                    id="name"
                    placeholder="Как меня зовут?"/>
            </div>
 
            <div className="mb-3">
                <label htmlFor="text" className="form-label fs-4">Описание</label>
                <textarea
                    required
                    name="text"
                    className="form-control"
                    id="text"
                    placeholder="Что я умею?"
                    style={{"height": '130px'}}/>
            </div>
 
            <div className="mb-3">
                <label htmlFor="element" className="form-label">Выбрать элемент героя</label>
                <select
                    required
                    className="form-select"
                    id="element"
                    name="element">
                    <option >Я владею элементом...</option>
                    <option value="fire">Огонь</option>
                    <option value="water">Вода</option>
                    <option value="wind">Ветер</option>
                    <option value="earth">Земля</option>
                </select>
            </div>
 
            <button type="submit" className="btn btn-primary">Создать</button>
        </form>
    )
}
 
export default HeroesAddForm;

И так выглядит итоговое приложение, которое нужно дорабатывать, чтобы оно отправляло запросы на json-server, создавало новых персонажей, меняло стейт и фильтровало персонажей по элементам:

010 Разбор самых сложных моментов

Фильтры были расширены и внутрь них были помещены дополнительные данные по лейблу и классам, которые нужно будет вставить в кнопки

heroes.json

{
  "heroes": [
    {
      "id": 1,
      "name": "Первый герой",
      "description": "Первый герой в рейтинге!",
      "element": "fire"
    },
    {
      "id": 2,
      "name": "Неизвестный герой",
      "description": "Скрывающийся в тени",
      "element": "wind"
    },
    {
      "id": 3,
      "name": "Морской герой",
      "description": "Как аквамен, но не из DC",
      "element": "water"
    }
  ],
  "filters": [
    {
      "name": "all",
      "label": "Все",
      "className": "btn-outline-dark"
    },
    {
      "name": "fire",
      "label": "Огонь",
      "className": "btn-danger"
    },
    {
      "name": "water",
      "label": "Вода",
      "className": "btn-primary"
    },
    {
      "name": "wind",
      "label": "Ветер",
      "className": "btn-success"
    },
    {
      "name": "earth",
      "label": "Земля",
      "className": "btn-secondary"
    }
  ]
}

В экшены были добавлены креэйторы, которые отвечают за состояние фильтров и состояние добавления персонажей

actions > index.js

// отправка запроса на получение героев
export const heroesFetching = () => {
    return {
        type: 'HEROES_FETCHING'
    }
}
 
// герои получены
// так же сюда поступают и сами данные по героям, чтобы занести их в хранилище
export const heroesFetched = (heroes) => {
    return {
        type: 'HEROES_FETCHED',
        payload: heroes
    }
}
 
// ошибка отправки запроса
export const heroesFetchingError = () => {
    return {
        type: 'HEROES_FETCHING_ERROR'
    }
}
 
// получение фильтров с бэка
export const filtersFetching = () => {
    return {
        type: 'FILTERS_FETCHING'
    }
}
 
// фильтры получены
// так же сюда поступают и сами данные по фильтрам, чтобы занести их в хранилище
export const filtersFetched = (filters) => {
    return {
        type: 'FILTERS_FETCHED',
        payload: filters
    }
}
 
// ошибка фетча филтров
export const filtersFetchingError = () => {
    return {
        type: 'FILTERS_FETCHING_ERROR'
    }
}
 
// информация об изменении филтров
export const activeFilterChanged = (filter) => {
    return {
        type: 'ACTIVE_FILTER_CHANGED',
        payload: filter
    }
}
 
// герой создан
// сюда передаются данные по герою с формы
export const heroCreated = (hero) => {
    return {
        type: 'HERO_CREATED',
        payload: hero
    }
}
 
// герой удалён
// сюда поступают данные id персонажа для удаления
export const heroDeleted = (id) => {
    return {
        type: 'HERO_DELETED',
        payload: id
    }
}

В редьюсер были добавлены кейсы для добавления персонажа, удаления и реагирование на изменение фильтра. Так же было добавлены дополнительные состояния в хранилище

reducers > index.js

const initialState = {
    heroes: [], // герои
    heroesLoadingStatus: 'idle', // статус загрузки героя
    filters: [], // фильтры
    filtersLoadingStatus: 'idle', // статус загрузки фильтров
    activeFilter: 'all', // активный фильтр // по умолчанию все активны
    filteredHeroes: [] // массив отфильтрованных героев
}
 
const reducer = (state = initialState, action) => {
    switch (action.type) {
        case 'HEROES_FETCHING':
            return {
                ...state,
                heroesLoadingStatus: 'loading'
            }
        case 'HEROES_FETCHED':
            return {
                ...state,
                heroes: action.payload,
                // ЭТО МОЖНО СДЕЛАТЬ И ПО ДРУГОМУ
                // Я специально показываю вариант с действиями тут, но более правильный вариант
                // будет показан в следующем уроке
                filteredHeroes: state.activeFilter === 'all' ?
                                action.payload :
                                action.payload.filter(item => item.element === state.activeFilter),
                heroesLoadingStatus: 'idle'
            }
        case 'HEROES_FETCHING_ERROR':
            return {
                ...state,
                heroesLoadingStatus: 'error'
            }
        case 'FILTERS_FETCHING':
            return {
                ...state,
                filtersLoadingStatus: 'loading'
            }
        case 'FILTERS_FETCHED':
            return {
                ...state,
                filters: action.payload,
                filtersLoadingStatus: 'idle'
            }
        case 'FILTERS_FETCHING_ERROR':
            return {
                ...state,
                filtersLoadingStatus: 'error'
            }
        case 'ACTIVE_FILTER_CHANGED':
            return {
                ...state,
                activeFilter: action.payload,
                filteredHeroes: action.payload === 'all' ?
                                state.heroes :
                                state.heroes.filter(item => item.element === action.payload)
            }
        // Самая сложная часть - это показывать новые элементы по фильтрам
        // при создании или удалении
        case 'HERO_CREATED':
            // Формируем новый массив
            let newCreatedHeroList = [...state.heroes, action.payload];
            return {
                ...state,
                heroes: newCreatedHeroList,
                // Фильтруем новые данные по фильтру, который сейчас применяется
                filteredHeroes: state.activeFilter === 'all' ?
                                newCreatedHeroList :
                                newCreatedHeroList.filter(item => item.element === state.activeFilter)
            }
        case 'HERO_DELETED':
            // Формируем новый массив, в котором не будет удалённого персонажа
            const newHeroList = state.heroes.filter(item => item.id !== action.payload);
            return {
                ...state,
                heroes: newHeroList,
                // Фильтруем новые данные по фильтру, который сейчас применяется
                filteredHeroes: state.activeFilter === 'all' ?
                                newHeroList :
                                newHeroList.filter(item => item.element === state.activeFilter)
            }
        default: return state
    }
}
 
export default reducer;

В компонент списка героев добавилась функция, которая позволяет удалить персонажа и она передаётся в компонент с одним персонажем. Так же была добавлена анимация для удаления и появления элементов в списке

components > heroesList > HeroesList.js

import {useHttp} from '../../hooks/http.hook';
import { useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { CSSTransition, TransitionGroup} from 'react-transition-group';
 
import { heroesFetching, heroesFetched, heroesFetchingError, heroDeleted } from '../../actions';
import HeroesListItem from "../heroesListItem/HeroesListItem";
import Spinner from '../spinner/Spinner';
 
import './heroesList.scss';
 
const HeroesList = () => {
    const {filteredHeroes, heroesLoadingStatus} = useSelector(state => state);
    const dispatch = useDispatch();
    const {request} = useHttp();
 
    useEffect(() => {
        dispatch(heroesFetching());
        request("http://localhost:3001/heroes")
            .then(data => dispatch(heroesFetched(data)))
            .catch(() => dispatch(heroesFetchingError()))
 
        // eslint-disable-next-line
    }, []);
 
    // Функция берет id и по нему удаляет ненужного персонажа из store
    // ТОЛЬКО если запрос на удаление прошел успешно
    // Отслеживайте цепочку действий actions => reducers
    // так как функция передаётся ниже по иерархии, то её стоит обернуть в useCallback, чтобы она не вызывала перерендер компонента
    const onDelete = useCallback((id) => {
        // Удаление персонажа по его id
        request(`http://localhost:3001/heroes/${id}`, "DELETE")
            .then(data => console.log(data, 'Deleted'))
            .then(dispatch(heroDeleted(id)))
            .catch(err => console.log(err));
    }, [request]);
 
    if (heroesLoadingStatus === "loading") {
        return <Spinner/>;
    } else if (heroesLoadingStatus === "error") {
        return <h5 className="text-center mt-5">Ошибка загрузки</h5>
    }
 
    const renderHeroesList = (arr) => {
        if (arr.length === 0) {
            return (
                <CSSTransition
                    timeout={0}
                    classNames="hero">
                    <h5 className="text-center mt-5">Героев пока нет</h5>
                </CSSTransition>
            )
        }
 
        return arr.map(({id, ...props}) => {
            return (
                <CSSTransition
                    key={id}
                    timeout={500}
                    classNames="hero">
                    <HeroesListItem  {...props} onDelete={() => onDelete(id)}/>
                </CSSTransition>
            )
        })
    }
 
    const elements = renderHeroesList(filteredHeroes);
    return (
        <TransitionGroup component="ul">
            {elements}
        </TransitionGroup>
    )
}
 
export default HeroesList;

В компонент одного героя была добавлена только функция для удаления персонажа, которая приходит из списка

components > heroesListItem > HeroesListItem.js

const HeroesListItem = ({name, description, element, onDelete}) => {
 
    let elementClassName;
 
    switch (element) {
        case 'fire':
            elementClassName = 'bg-danger bg-gradient';
            break;
        case 'water':
            elementClassName = 'bg-primary bg-gradient';
            break;
        case 'wind':
            elementClassName = 'bg-success bg-gradient';
            break;
        case 'earth':
            elementClassName = 'bg-secondary bg-gradient';
            break;
        default:
            elementClassName = 'bg-warning bg-gradient';
    }
 
    return (
        <li
            className={`card flex-row mb-4 shadow-lg text-white ${elementClassName}``}>
            <img src="http://www.stpaulsteinbach.org/wp-content/uploads/2014/09/unknown-hero.jpg"
                 className="img-fluid w-25 d-inline"
                 alt="unknown hero"
                 style={{'objectFit': 'cover'}}/>
            <div className="card-body">
 
                <h3 className="card-title">{name}</h3>
                <p className="card-text">{description}</p>
            </div>
            <span onClick={onDelete}
                className="position-absolute top-0 start-100 translate-middle badge border rounded-pill bg-light">
                <button type="button" className="btn-close btn-close" aria-label="Close"></button>
            </span>
        </li>
    )
}
 
export default HeroesListItem;

В форму добавления нового персонажа были добавлены состояния для контроля инпутов.

Была добавлена функция onSubmitHandler, которая контролирует действие при отправке формы

components > heroesAddForm > HeroesAddForm.js

// Задача для этого компонента:
// Реализовать создание нового героя с введенными данными. Он должен попадать
// в общее состояние и отображаться в списке + фильтроваться
// Уникальный идентификатор персонажа можно сгенерировать через uiid
// Усложненная задача:
// Персонаж создается и в файле json при помощи метода POST
// Дополнительно:
// Элементы <option></option> желательно сформировать на базе
// данных из фильтров
 
import {useHttp} from '../../hooks/http.hook';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { v4 as uuidv4 } from 'uuid';
 
import { heroCreated } from '../../actions';
 
const HeroesAddForm = () => {
    // Состояния для контроля формы
    const [heroName, setHeroName] = useState('');
    const [heroDescr, setHeroDescr] = useState('');
    const [heroElement, setHeroElement] = useState('');
 
    const {filters, filtersLoadingStatus} = useSelector(state => state);
    const dispatch = useDispatch();
    const {request} = useHttp();
 
    const onSubmitHandler = (e) => {
        e.preventDefault();
        // Можно сделать и одинаковые названия состояний,
        // хотел показать вам чуть нагляднее
        // Генерация id через библиотеку
        const newHero = {
            id: uuidv4(),
            name: heroName,
            description: heroDescr,
            element: heroElement
        }
 
        // Отправляем данные на сервер в формате JSON
        // ТОЛЬКО если запрос успешен - отправляем персонажа в store
        request("http://localhost:3001/heroes", "POST", JSON.stringify(newHero))
            .then(res => console.log(res, 'Отправка успешна'))
            .then(dispatch(heroCreated(newHero)))
            .catch(err => console.log(err));
 
        // Очищаем форму после отправки
        setHeroName('');
        setHeroDescr('');
        setHeroElement('');
    }
 
    const renderFilters = (filters, status) => {
        if (status === "loading") {
            return <option>Загрузка элементов</option>
        } else if (status === "error") {
            return <option>Ошибка загрузки</option>
        }
 
        // Если фильтры есть, то рендерим их
        if (filters && filters.length > 0 ) {
            return filters.map(({name, label}) => {
                // Один из фильтров нам тут не нужен
                if (name === 'all')  return;
 
                return <option key={name} value={name}>{label}</option>
            })
        }
    }
 
    return (
        <form className="border p-4 shadow-lg rounded" onSubmit={onSubmitHandler}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label fs-4">Имя нового героя</label>
                <input
                    required
                    type="text"
                    name="name"
                    className="form-control"
                    id="name"
                    placeholder="Как меня зовут?"
                    value={heroName}
                    onChange={(e) => setHeroName(e.target.value)}/>
            </div>
 
            <div className="mb-3">
                <label htmlFor="text" className="form-label fs-4">Описание</label>
                <textarea
                    required
                    name="text"
                    className="form-control"
                    id="text"
                    placeholder="Что я умею?"
                    style={{"height": '130px'}}
                    value={heroDescr}
                    onChange={(e) => setHeroDescr(e.target.value)}/>
            </div>
 
            <div className="mb-3">
                <label htmlFor="element" className="form-label">Выбрать элемент героя</label>
                <select
                    required
                    className="form-select"
                    id="element"
                    name="element"
                    value={heroElement}
                    onChange={(e) => setHeroElement(e.target.value)}>
                    <option value="">Я владею элементом...</option>
                    {renderFilters(filters, filtersLoadingStatus)}
                </select>
            </div>
 
            <button type="submit" className="btn btn-primary">Создать</button>
        </form>
    )
}
 
export default HeroesAddForm;

Тут были добавлены фильтры, которые мы получаем с сервера

components > heroesFilters > HeroesFilters.js

import {useHttp} from '../../hooks/http.hook';
import {useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import classNames from 'classnames';
 
import {filtersFetching, filtersFetched, filtersFetchingError, activeFilterChanged} from '../../actions';
import Spinner from '../spinner/Spinner';
 
// Задача для этого компонента:
// Фильтры должны формироваться на основании загруженных данных
// Фильтры должны отображать только нужных героев при выборе
// Активный фильтр имеет класс active
 
const HeroesFilters = () => {
 
    const {filters, filtersLoadingStatus, activeFilter} = useSelector(state => state);
    const dispatch = useDispatch();
    const {request} = useHttp();
 
    // Запрос на сервер для получения фильтров и последовательной смены состояния
    useEffect(() => {
        dispatch(filtersFetching());
        request("http://localhost:3001/filters")
            .then(data => dispatch(filtersFetched(data)))
            .catch(() => dispatch(filtersFetchingError()))
 
        // eslint-disable-next-line
    }, []);
 
    if (filtersLoadingStatus === "loading") {
        return <Spinner/>;
    } else if (filtersLoadingStatus === "error") {
        return <h5 className="text-center mt-5">Ошибка загрузки</h5>
    }
 
    const renderFilters = (arr) => {
        if (arr.length === 0) {
            return <h5 className="text-center mt-5">Фильтры не найдены</h5>
        }
 
        // Данные в json-файле я расширил классами и текстом
        return arr.map(({name, className, label}) => {
 
            // Используем библиотеку classnames и формируем классы динамически
            const btnClass = classNames('btn', className, {
                'active': name === activeFilter
            });
 
            return <button
                key={name}
                id={name}
                className={btnClass}
                onClick={() => dispatch(activeFilterChanged(name))}
            >{label}</button>
        })
    }
 
    const elements = renderFilters(filters);
 
    return (
        <div className="card shadow-lg mt-4">
            <div className="card-body">
                <p className="card-text">Отфильтруйте героев по элементам</p>
                <div className="btn-group">
                    {elements}
                </div>
            </div>
        </div>
    )
}
 
export default HeroesFilters;

Теперь работает фильтрация и добавление персонажей

011 Комбинирование reducers и красивые селекторы. CreateSelector()

При разрастании приложения увеличивается и количество действий, которые должен контролировать реакт. Если экшены можно спокойно разделить по папкам и обращаться конкретно к нужным, то данное разрастание не позволит спокойно поделить функцию-редьюсер

В нашем приложении достаточно логичным будет отделить логику работы с персонажами и фильтрами. Однако мы сталкиваемся с тем, что фильтры так же используют состояние персонажей, чтобы контролировать их список.

Чтобы сократить код и разбить логику, можно:

  • разделить логику редьюсера через функцию combineReducers
  • вынести фильтрацию внутрь компонента, чтобы разбить логику стейтов

Тут мы выносим фильтрацию полученных данных из стейта и теперь её не нужно проводить внутри редьюсера

components > heroesList > HeroesList.js

const HeroesList = () => {
	const filteredHeroes = useSelector((state) => {
		if (state.activeFilter === 'all') {
			return state.heroes;
		} else {
			return state.heroes.filter((item) => state.heroes === state.activeFilter);
		}
	});
 
	// сейчас отсюда достаём просто статус загрузки
	const heroesLoadingStatus = useSelector((state) => state.heroesLoadingStatus);
 
	/// CODE ...

Теперь нам не нужно данное состояние

И данная фильтрация в редьюсере

Вынесем из главного reducer логику по работе с персонажами и его стейты в отдельный файл

reducers > heroes.js

const initialState = {
	heroes: [],
	heroesLoadingStatus: 'idle',
};
 
export const heroes = (state = initialState, action) => {
	switch (action.type) {
		case 'HEROES_FETCHING':
			return {
				...state,
				heroesLoadingStatus: 'loading',
			};
		case 'HEROES_FETCHED':
			return {
				...state,
				heroes: action.payload,
				heroesLoadingStatus: 'idle',
			};
		case 'HEROES_FETCHING_ERROR':
			return {
				...state,
				heroesLoadingStatus: 'error',
			};
		// Самая сложная часть - это показывать новые элементы по фильтрам
		// при создании или удалении
		case 'HERO_CREATED':
			return {
				...state,
				heroes: [...state.heroes, action.payload],
			};
		case 'HERO_DELETED':
			return {
				...state,
				heroes: state.heroes.filter((item) => item.id !== action.payload),
			};
		default:
			return state;
	}
};

Тут уже будем хранить логику фильтрации

reducers > filters.js

const initialState = {
	filters: [],
	filtersLoadingStatus: 'idle',
	activeFilter: 'all',
};
 
export const filters = (state = initialState, action) => {
	switch (action.type) {
		case 'FILTERS_FETCHING':
			return {
				...state,
				filtersLoadingStatus: 'loading',
			};
		case 'FILTERS_FETCHED':
			return {
				...state,
				filters: action.payload,
				filtersLoadingStatus: 'idle',
			};
		case 'FILTERS_FETCHING_ERROR':
			return {
				...state,
				filtersLoadingStatus: 'error',
			};
		case 'ACTIVE_FILTER_CHANGED':
			return {
				...state,
				activeFilter: action.payload,
			};
		default:
			return state;
	}
};

И тут через функцию combineReducers объединяем две функции редьюсера в один внутри объекта. Теперь обычный reducer не нужен и его можно будет удалить

store > index.js

import { createStore, combineReducers } from 'redux';
import { heroes } from '../reducers/heroes';
import { filters } from '../reducers/filters';
 
const store = createStore(
	combineReducers({ heroes, filters }),
	window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
);
 
export default store;

И теперь, после манипуляций с объединением редьюсеров, нужно будет вытаскивать нужные объекты из объектов, которые были названы и переданы в combineReducers

И вот уже с таким синтаксисом мы можем импортировать поля из нескольких объектов Однако такой подход приводит к тому, что компонент будет перерисовываться при каждом изменении стейта Такой вариант не стоит использовать в проекте, так как он не оптимизирован

const someState = useSelector((state) => ({
   activeFilter: state.filters.activeFilter,
   heroes: state.heroes.heroes,
}));

Откорректируем логику фильтрации героев. Но тут мы встретимся с такой проблемой, что каждый раз при нажатии кнопки фильтрации, у нас будет воспроизводиться перерендер компонента. Это происходит из-за того, что каждый раз у нас вызывается useSelector() при изменении глобального стейта.

components > heroesList > HeroesList.js

const HeroesList = () => {
   const filteredHeroes = useSelector((state) => {
      if (state.filters.activeFilter === 'all') {
	      console.log('render');
         return state.heroes.heroes;
      } else {
         return state.heroes.heroes.filter(
            (item) => item.element === state.filters.activeFilter,
         );
      }
   });
 
	/// CODE ...

Чтобы решить данную проблему, нужно мемоизировать функцию вызова useSelector()

npm i reselect

Данный модуль позволяет нам вызвать по определённым правилам функцию useSelector. То есть мы создаём массив запросов в селектор первым аргументом, а вторым аргументом берём полученные значения и используем их в функции, которую хотели использовать в селекторе. После вышеописанных манипуляций просто помещаем функцию реселекта внутрь useSelector

components > heroesList > HeroesList.js

import { createSelector } from 'reselect';
 
/// CODE ...
 
// эта функция будет вызвать useSelector по заданным правилам и будет мемоизировать значение
const filteredHeroesSelector = createSelector(
   // вызываем срабатывание двух селекторов
   // получаем сам активный фильтр и массив героев   [(state) => state.filters.activeFilter, (state) => state.heroes.heroes],
   // производим операции над результатами двух вызванных селекторов
   (filter, heroes) => {
      if (filter === 'all') {
         console.log('render');
         return heroes;
      } else {
         return heroes.filter((item) => item.element === filter);
      }
   },
);
 
const filteredHeroes = useSelector(filteredHeroesSelector);
 
/// CODE ...

Теперь рендер вызвается только тогда, когда данные в стейте изменяются

012 Про сложность реальной разработки

Реальные приложения требуют от разработчика большое количество знаний - это сложно, но нужна практика

Спасибо за внимание

013 Store enhancers

Store enhancers - это дополнительный функционал, который упрощает взаимодействие с хранилищем. Зачастую просто используют сторонние npm-пакеты, но так же можно написать и свой функционал улучшителя.

Так же частным случаем энхэнсеров является middleware функции, которые так же передаются в стор.

Конкретно для нашего проекта можно сделать простой энхэнсер, который модифицирует работу диспэтча. Он будет в себя принимать не только объект с определённым действием, но и принимать строку с экшен тайптом.

Тут уже нужно сказать, что самих улучшителей стора может быть большое количество и поэтому их часто передают внутри функции compose, которая объединяет их в один. Однако так же нужно будет соблюдать последовательно передачи функций, так как они будут модифицировать логику последовательно. Конкретно в данном случае, строку с подключением к редакс-девтулзу стоит поместить в конец списка.

import { createStore, combineReducers, compose } from 'redux';
import { heroes } from '../reducers/heroes';
import { filters } from '../reducers/filters';
 
const enhancer =
	(createStore) =>
	// сюда попадают аргументы для функции
	(...args) => {
		// тут мы передаём в функцию стора аргументы и вызваем её
		const store = createStore(...args);
 
		// это старый диспетч, который будет срабатывать, когда мы передаём объект
		const oldDispatch = store.dispatch;
 
		// переопределяем стандартный диспетч, который будет работать с текстом
		store.dispatch = (action) => {
			if (typeof action === 'string') {
				return oldDispatch({ type: action });
			}
 
			// если была передана не строка, то имитируем стандартную работу
			return oldDispatch(action);
		};
 
		return store;
	};
 
const store = createStore(
	combineReducers({ heroes, filters }),
	compose(
		enhancer,
		window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
	),
);
 
export default store;

Приложение так же работает, но теперь у нас есть возможность передавать в диспетч и просто строку с действием

014 Middleware

Middleware - это enhancer, который занимается улучшением только dispatch. Так же зачастую пользуются уже готовыми middleware, которые предоставляет комьюнити npm

Конкретно тут сделаем посредника, который позволит dispatch принимать не только объекты, но и строки

store > index.js

// функция-посредник, которая работает только на dispatch
// сюда автоматически будет попадать две сущности из store - dispatch, getState
const stringMiddleware =
	({ dispatch, getState }) =>
	// потом здесь мы буем принимать dispatch
	(dispatch) =>
	// а это уже по-факту и есть новая функция dispatch с изменением функционала
	(action) => {
		if (typeof action === 'string') {
			return dispatch({ type: action });
		}
 
		return dispatch(action);
	};
  • первым аргументом можно так же ничего не передавать, потому что нам не всегда нужен store
  • обычно, функцию dispatch называют next, так как будет вызываться следующая функция из middleware

store > index.js

const stringMiddleware =
	() =>
	(next) =>
	(action) => {
		if (typeof action === 'string') {
			return next({ type: action });
		}
 
		return next(action);
	};

Чтобы применять middleware в createStore, нужно будет воспользоваться функцией applyMiddleware, которая будет применять посредника.

Чтобы вернуть подключение к редакс-девтулзу, можно опять же обернуть весь второй аргумент createStore в функцию compose()

store > index.js

const store = createStore(
	combineReducers({ heroes, filters }),
	compose(
		applyMiddleware(stringMiddleware),
		window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
	),
);

015 Redux-thunk

Основная задача модуля redux-thunk передавать функцию, которая потом будет производить асинхронную операцию

Устанавливаем пакет в проект.

npm i redux-thunk

И далее, чтобы убедиться, что он работает, можно просто попробовать передать actionCreater функцию в dispatch без вызова:

components > heroesList > HeroesList.js

// функция получения персонажей с сервера
useEffect(() => {
   dispatch(heroesFetching); // передаём функцию экшена без вызова
   request('http://localhost:3001/heroes')
      .then((data) => dispatch(heroesFetched(data)))
      .catch(() => dispatch(heroesFetchingError()));
}, []);

Так же мы можем расширять наши экшены, так как в их вложенную функцию может автоматически поступать dispatch, над которым мы можем проводить различные манипуляции. Конкретно тут будет срабатывать передача данных в стейт через определённый промежуток времени.

actions > index.js

// когда мы вызываем функцию, она возвращает функцию, принимающую в себя dispatch
// dispatch приходит в функцию автоматически, так как мы используем thunk middleware
export const activeFilterChanged = (filter) => (dispatch) => {
   setTimeout(() => {
      dispatch({
         type: 'ACTIVE_FILTER_CHANGED',
         payload: filter,
      });
   }, 1000);
};

Но так же мы можем и упростить себе жизнь тем, что мы можем вызвать логику диспетча прямо внутри самой папки экшенов. Конкретно, мы можем вынести запрос на получение персонажей и занесение их в стейт прямо из экшенов. Там нам не нужно будет импортировать и экспортировать отдельные экшены - можно будет ими просто воспользоваться.

actions > index.js

export const fetchHeroes = (request) => (dispatch) => {
	dispatch(heroesFetching());
	request('http://localhost:3001/heroes')
		.then((data) => dispatch(heroesFetched(data)))
		.catch(() => dispatch(heroesFetchingError()));
};

И тут далее в самом компоненте уже можем воспользоваться одним экшеном, который сам занесёт данные по персонажам в стейт, передав в него функцию совершения реквеста

components > heroesList > HeroesList.js

import { fetchHeroes, heroDeleted } from '../../actions';
 
const HeroesList = () => {
	/// CODE ...
 
	const { request } = useHttp();
 
	useEffect(() => {
		dispatch(fetchHeroes(request));
	}, []);

Redux Toolkit

RTK Redux ReduxToolkit

Проблемы больших проектов на обычном редакса:

  • очень много boilerplates при создании actionCreators и reducers
  • при большом количестве enhancers и middlewares функция по созданию store сильно разрастается

Redux Toolkit включает в себя набор инструментов для более простой и быстрой работы с states и store.

Та же функция createSelector была переэкспортирована из модуля Reselect в RTK

Redux Toolkit configureStore()

Функция configureStore предназначена для того, чтобы удобно автоматически регулировать reducers, подключать middlewares или enhancers и автоматически подключать redux devtools без дополнительных строк кода

В тулкит так же включены изначально самые популярные middlewares:

  • Serializability Middlweware - проверяет, чтобы в стейте были только те значения, которые должны быть в сторе
  • Immutability Middlweware - предназначен для обнаружения мутаций, которые могут быть в сторе
  • Thunk Middlweware - позволяет в экшены автоматически получать dispatch

И уже так будет выглядеть создание нового store с использованием RTK

store > index.js

import { heroes } from '../reducers/heroes';
import { filters } from '../reducers/filters';
import { configureStore } from '@reduxjs/toolkit';
 
const stringMiddleware = () => (next) => (action) => {
	if (typeof action === 'string') {
		return next({ type: action });
	}
 
	return next(action);
};
 
const store = configureStore({
	// подключаем редьюсеры
	reducer: { heroes, filters },
	// подключаем девтулз
	devTools: process.env.NODE_ENV === 'development',
	// подключаем все стандартные middleware (включая thunk) и наши собственные
	middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(stringMiddleware),
});
 
export default store;

Redux Toolkit createAction()

Функция createAction() позволяет автоматически выполнять операцию по созданию экшена

Функция принимает в себя:

  • тип действия
  • вспомогательную функцию

И далее тут нужно сказать, что данная функция автоматически обрабатывает поступающие в неё данные. Т.е. если вызвать heroesFetched и передать в него аргумент, то он автоматически отправится в поле payload

Ниже представлены две реализации экшенов - классическая и через createAction и обе из них работают полностью взаимозаменяемо

export const heroesFetching = () => {
	return {
		type: 'HEROES_FETCHING',
	};
};
 
export const heroesFetched = (heroes) => {
	return {
		type: 'HEROES_FETCHED',
		payload: heroes,
	};
};
 
// ИЛИ ...
 
export const heroesFetching = createAction('HEROES_FETCHING');
export const heroesFetched = createAction('HEROES_FETCHED');

Тут уже стоит отметить, что в reducer стоит передавать только одно поле payload. Таким образом будет проще читать код и воспринимать его. Остальные побочные действия лучше делать вне reducer.

И вот пример, когда мы вторым аргументом передаём дополнительную функцию, которая осуществляет возврат обогащённого payload, который уже будет содержать не просто переданные данные, а ещё и сгенерированные нами

import { createAction, nanoid } from '@reduxjs/toolkit'
 
const addTodo = createAction('todos/add', function prepare(text) {
  return {
    payload: {
      text,
      id: nanoid(),
      createdAt: new Date().toISOString(),
    },
  }
})

Тут так же стоит отметить, что в RTK была добавлена функция nanoid, которая генерирует уникальный идентификатор для объекта

Redux Toolkit createReducer()

Функция reducer зачастую представляет из себя очень много блоков switch-case и много вложенных конструкций, которые нужно редактировать в глубине, что усложняет разработку

Для упрощения создания reducer была добавлена функция createReducer, которая принимает в себя:

  • начальное состояние
  • builder, который позволяет строить reducer за счёт встроенных в него трёх функций

builder использует три функции:

  • addCase - добавляет кейс в свитчер редьюсера
  • addDefaultCase - устанавливает дефолтный кейс выполнения
  • addMatcher - позволяет фильтровать входящие экшены

И так выглядит реализация нашего редьюсера героев через createReducer:

reducers > heroes.js

import { createReducer } from '@reduxjs/toolkit';
 
import {
	heroesFetching,
	heroesFetched,
	heroesFetchingError,
	heroCreated,
	heroDeleted,
} from '../actions';
 
const initialState = {
	heroes: [],
	heroesLoadingStatus: 'idle',
};
 
export const heroes = createReducer(initialState, (builder) => {
	// вызываем объект билдера
	builder
		// создаём отдельный кейс как в switch-case
		.addCase(
			// action кейса
			heroesFetching,
			// reducer
			(state, action) => {
				// меняем состояние напрямую
				state.heroesLoadingStatus = 'loading';
			},
		)
		.addCase(heroesFetched, (state, action) => {
			state.heroes = action.payload;
			state.heroesLoadingStatus = 'idle';
		})
		.addCase(heroesFetchingError, (state, action) => {
			state.heroesLoadingStatus = 'error';
		})
		.addCase(heroCreated, (state, action) => {
			state.heroes.push(action.payload);
		})
		.addCase(heroDeleted, (state, action) => {
			state.heroes = state.heroes.filter((item) => item.id !== action.payload);
		})
		.addDefaultCase(() => {});
});

Так же нужно отметить, что внутри функций builder используется библиотека ImmerJS, которая сама отвечает за сохранение логики иммутабельности в проекте. То есть мы можем писать визуально проект с мутациями, а библиотека сама переведёт код в иммутабельные сущности. Такой подход будет работать ровно до тех пор, пока мы ничего не возвращаем из этих функций через return

Однако функция createReducer требует для работы, чтобы все экшены были написаны с помощью createAction

actions > index.js

import { createAction } from '@reduxjs/toolkit';
 
export const fetchHeroes = (request) => (dispatch) => {
	dispatch(heroesFetching());
	request('http://localhost:3001/heroes')
		.then((data) => dispatch(heroesFetched(data)))
		.catch(() => dispatch(heroesFetchingError()));
};
 
export const heroesFetching = createAction('HEROES_FETCHING');
export const heroesFetched = createAction('HEROES_FETCHED');
export const heroesFetchingError = createAction('HEROES_FETCHING_ERROR');
export const heroCreated = createAction('HERO_CREATED');
export const heroDeleted = createAction('HERO_DELETED');

Так же у нас есть вариант использовать более короткий способ создания редьюсеров через объект. Такой способ уже не работает с TS.

reducers > heroes.js

export const heroes = createReducer(
	// начальное состояние
	initialState,
	// карта действий (кейсы)
	{
		[heroesFetching]: (state) => {
			state.heroesLoadingStatus = 'loading';
		},
		[heroesFetched]: (state, action) => {
			state.heroes = action.payload;
			state.heroesLoadingStatus = 'idle';
		},
		[heroesFetchingError]: (state, action) => {
			state.heroesLoadingStatus = 'error';
		},
		[heroCreated]: (state, action) => {
			state.heroes.push(action.payload);
		},
		[heroDeleted]: (state, action) => {
			state.heroes = state.heroes.filter((item) => item.id !== action.payload);
		},
	},
	// массив функций сравнения
	[],
	// действие по умолчанию
	() => {},
);

Redux Toolkit createSlice()

  • Данная функция объединяет функции createAction и createReducer в одно
  • Обычно она располагается рядом с файлом, к которому она и относится
  • В конец названия файла обычно добавляется суффикс Slice

Функция createSlice принимает в себя 4 аргумента:

  • name - пространство имён создаваемых действий (имя среза). Это имя будет являться префиксом для всех имён экшенов, которые мы будем передавать в качестве ключа внутри объекта reducers
  • initialState - начальное состояние
  • reducers - объект с обработчиками
  • extraReducers - объект с редьюсерами другого среза (обычно используется для обновления объекта, относящегося к другому слайсу)

Конкретно тут был создан срез actionCreators и reducer для героев в одном файле рядом с самим компонентом

components > heroesList > HeroesList.js

import { createSlice } from '@reduxjs/toolkit';
 
const initialState = {
	heroes: [],
	heroesLoadingStatus: 'idle',
};
 
const heroesSlice = createSlice({
	// пространство имён, в котором будут происходить все экшены
	name: 'heroes',
	// начальное состояние
	initialState,
	reducers: {
		// свойство генерирует экшен
		// а значение генерирует действие редьюсера
		heroesFetching: (state) => {
			state.heroesLoadingStatus = 'loading';
		},
		heroesFetched: (state, action) => {
			state.heroes = action.payload;
			state.heroesLoadingStatus = 'idle';
		},
		heroesFetchingError: (state, action) => {
			state.heroesLoadingStatus = 'error';
		},
		heroCreated: (state, action) => {
			state.heroes.push(action.payload);
		},
		heroDeleted: (state, action) => {
			state.heroes = state.heroes.filter((item) => item.id !== action.payload);
		},
	},
});
 
const { actions, reducer } = heroesSlice;
 
export const { heroCreated, heroDeleted, heroesFetched, heroesFetchingError, heroesFetching } =
	actions;
export default reducer;

И далее импортируем наш reducer в store

store > index.js

import heroes from '../components/heroesList/heroesSlice';
 
const store = configureStore({
	reducer: { heroes, filters },
	devTools: process.env.NODE_ENV === 'development',
	middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(stringMiddleware),
});

Теперь всё то, что относится к actionCreators героев можно удалить из файла экшенов и импортировать нужные зависимости для работы функции fetchHeroes

actions > index.js

import {
	heroCreated,
	heroDeleted,
	heroesFetched,
	heroesFetchingError,
	heroesFetching,
} from '../components/heroesList/heroesSlice';
 
export const fetchHeroes = (request) => (dispatch) => {
	dispatch(heroesFetching());
	request('http://localhost:3001/heroes')
		.then((data) => dispatch(heroesFetched(data)))
		.catch(() => dispatch(heroesFetchingError()));
};
 
/// CODE ...

Далее нужно поправить некоторые импорты в HeroesList и в HeroesAddForm

И теперь мы имеем работающее приложение, которое мы переписали на более коротком синтаксисе.

Однако тут стоит сказать, что теперь наши действия были переименованы под образ createSlice, где обозначается пространство выполняемых действий экшеном (heroes) и сам actionCreator (heroesFetching)

Если нам нужно будет не только получить, но и обогатить payload, то можно будет добавить передать в экшен два объекта:

  • reducer - это сам обработчик
  • prepare - обработчик, который обогащает payload
import { createSlice, nanoid } from '@reduxjs/toolkit'
 
const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: {
      // это та стандартная функция, которую мы просто помещаем в экшен
      // тут мы получаем и стейт и экшен для передачи payload
      reducer: (state, action) => {
        state.push(action.payload)
      },
      // а это дополнительное действие для формирования самого payload
      prepare: (text) => {
        const id = nanoid()
        return { payload: { id, text } }
      },
    },
  },
})

Если нам нужно изменить стейт уже в другом компоненте из нашего, то мы можем воспользоваться для этого extraReducers

import { createAction, createSlice } from '@reduxjs/toolkit'
import { incrementBy, decrement } from './actions'
 
function isRejectedAction(action) {
  return action.type.endsWith('rejected')
}
 
createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(incrementBy, (state, action) => {
      })
      .addCase(decrement, (state, action) => {})
      .addMatcher(
        isRejectedAction,
        (state, action) => {}
      )
      .addDefaultCase((state, action) => {})
  },
})

Redux Toolkit createAsyncThunk()

Функция createAsyncThunk() позволяет сделать асинхронный actionCreator, который будет вести себя ровно так же, как и при использовании обычного redux-thunk. Использование данной функции является приоритетным, так как при таком запросе heroes/fetchHeroes функция возвращает нам три экшена, которые поделены на:

  • pending'heroes/fetchHeroes/pending'
  • fulfilled'heroes/fetchHeroes/fulfilled'
  • rejected'heroes/fetchHeroes/rejected' Такой подход позволит нам не обрабатывать три разных состояния функции самостоятельно, а перекладывать это на функционал тулкита.

Тут нужно отметить, что из данной функции мы должны возвращать Promise, который функция сама и обработает по трём состояниям

Сам reducer, который мы создали через createAsyncThunk будет передаваться в основной reducer уже как четвёртый аргумент - объект extraReducers

Тут мы создали функцию fetchHeroes, которая заменит fetchHeroes находящийся в actions. Далее нужно будет обработать три состояния fetchHeroes уже внутри самого heroesSlice, передав внутрь extraReducers

components > heroesList > heroesSlice.js

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { useHttp } from '../../hooks/http.hook';
 
const initialState = {
	heroes: [],
	heroesLoadingStatus: 'idle',
};
 
export const fetchHeroes = createAsyncThunk(
	// название среза / тип действия
	'heroes/fetchHeroes',
	// асинхронная функция
	// 1 арг - то, что приходит при диспетче
	// 2 арг - thunkAPI (dispatch, getState)
	async () => {
		const { request } = useHttp();
		return await request('http://localhost:3001/heroes');
	},
);
 
const heroesSlice = createSlice({
	name: 'heroes',
	initialState,
	reducers: {
		// а тут мы удалим heroesFetching, heroesFetched, heroesFetchingError, так как функционал перенесён в fetchHeroes
		heroCreated: (state, action) => {
			state.heroes.push(action.payload);
		},
		heroDeleted: (state, action) => {
			state.heroes = state.heroes.filter((item) => item.id !== action.payload);
		},
	},
	extraReducers: (builder) => {
		builder
			// добавляем формирование запроса
			.addCase(fetchHeroes.pending, (state) => {
				state.heroesLoadingStatus = 'loading'; // состояние загрузки
			})
			// запрос выполнен
			.addCase(fetchHeroes.fulfilled, (state, action) => {
				state.heroes = action.payload; // данные, полученные с сервера попадут сюда
				state.heroesLoadingStatus = 'idle'; // состояние ожидания
			})
			// запрос отклонён
			.addCase(fetchHeroes.rejected, (state, action) => {
				state.heroesLoadingStatus = 'error'; //
			});
	},
});
 
const { actions, reducer } = heroesSlice;
 
export const { heroCreated, heroDeleted, heroesFetched, heroesFetchingError, heroesFetching } =
	actions;
export default reducer;

Теперь тут меняем импорты

components > heroesList > HeroesList.js

import { heroDeleted, fetchHeroes } from './heroesSlice';

Ну и так же из нашего хука useHttp нужно убрать useCallback, так как это приведёт к ошибке

export const useHttp = () => {
	// убрать useCallback
	const request = async (
		url,
		method = 'GET',
		body = null,
		headers = { 'Content-Type': 'application/json' },
	) => {
		try {
			const response = await fetch(url, { method, body, headers });
 
			if (!response.ok) {
				throw new Error(`Could not fetch ${url}, status: ${response.status}`);
			}
 
			const data = await response.json();
 
			return data;
		} catch (e) {
			throw e;
		}
	};
 
	return {
		request,
	};
};

И теперь всё работает и функция за нас реализовала сразу три состояния стейта

Redux Toolkit createEntityAdapter()

Функция createEntityAdapter() позволит создавать готовый объект с часто-выполняемыми CRUD-операциями в reducer

В самом начале в файле со слайсом нужно создать сам адаптер и переписать создание initialState под адаптер

Так же мы можем внутрь адаптера вложить свойства, которые мы не хотим обрабатывать через него (heroesLoadingStatus), а хотим обработать самостоятельно

components > heroesList > heroesSlice.js

// создаём адаптер
const heroesAdapter = createEntityAdapter();
 
// создаём начальное состояние
const initialState = heroesAdapter.getInitialState({
	heroesLoadingStatus: 'idle',
});

Если вывести адаптер в консоль, то он будет иметь в себе объект, который будет хранить все попадающие внутрь него сущности и идентификаторы. Так же он будет отображать все те поля, которые мы передали как объект внутрь адаптера - уже с ними можно будет работать отдельно без круд-функций адаптера

Так же нужно сказать, что функция createEntityAdapter принимает в себя объект с переопределением начальных функций

const booksAdapter = createEntityAdapter({
  // тут мы указываем, что будем брать id не из book.id, а из book.bookId
  selectId: (book) => book.bookId,
  // тут мы производим сортировку всех книг по тайтлам
  sortComparer: (a, b) => a.title.localeCompare(b.title),
})

CRUD-операции, которые предоставляет createEntityAdapter:

  • addOne: принимает один объект и добавляет его, если он еще не присутствует.
  • addMany: принимает массив сущностей или объект в форме Record<EntityId, T> и добавляет их, если они еще не присутствуют.
  • setOne: принимает отдельный объект и добавляет или заменяет его
  • setMany: принимает массив сущностей или объект в форме Record<EntityId, T> и добавляет или заменяет их.
  • setAll: принимает массив сущностей или объект в форме Record<EntityId, T> и заменяет все существующие сущности значениями в массиве.
  • removeOne: принимает единственное значение идентификатора объекта и удаляет объект с этим идентификатором, если он существует.
  • removeMany: принимает массив значений идентификатора объекта и удаляет каждый объект с этими идентификаторами, если они существуют.
  • removeAll: удаляет все объекты из объекта состояния сущности.
  • updateOne: принимает “объект обновления”, содержащий идентификатор объекта, и объект, содержащий одно или несколько новых значений поля для обновления внутри changes поля, и выполняет поверхностное обновление соответствующего объекта.
  • updateMany: принимает массив объектов обновления и выполняет мелкие обновления для всех соответствующих объектов.
  • upsertOne: принимает единую сущность. Если объект с таким идентификатором существует, он выполнит поверхностное обновление, и указанные поля будут объединены в существующий объект, а любые совпадающие поля будут перезаписывать существующие значения. Если объект не существует, он будет добавлен.
  • upsertMany: принимает массив объектов или объект в формеRecord<EntityId, T>, который будет слегка изменен.

Все вышеописанные методы следуют принципу нормализации данных. Они производят действия над данными по определённым условиям, если они существуют/не существуют

Реализуем добавление персонажей в массив через адаптер. Для этого нам может подойти функция setAll, в которая будет являться местом, куда помещаем все данные, а вторым аргументом данные для помещения.

components > heroesList > heroesSlice.js

const heroesSlice = createSlice({
	name: 'heroes',
	initialState,
	reducers: {
		heroCreated: (state, action) => {
			state.heroes.push(action.payload);
		},
		heroDeleted: (state, action) => {
			state.heroes = state.heroes.filter((item) => item.id !== action.payload);
		},
	},
	extraReducers: (builder) => {
		builder
			.addCase(fetchHeroes.pending, (state) => {
				state.heroesLoadingStatus = 'loading';
			})
			.addCase(fetchHeroes.fulfilled, (state, action) => {
				state.heroesLoadingStatus = 'idle';
				// устанавливаем все полученные данные в стейт
				// первый аргумент - место, куда помещаем все данные
				// второй - что помещаем
				heroesAdapter.setAll(state, action.payload); // state.heroes = action.payload;
			})
			.addCase(fetchHeroes.rejected, (state, action) => {
				state.heroesLoadingStatus = 'error';
			});
	},
});

Все данные, которые мы помещаем в стейт, отправляются в объект entities

Чтобы работать с данным объектом и получать из него нужные сущности, нужно воспользоваться функциями выбора. Адаптер выбранной сущности содержит метод getSelectors(), которая предоставляет функционал селекторов уже знающих как считывать содержимое этой сущности:

  • selectIds: возвращает массив с идентификаторами state.ids.
  • selectEntities: возвращает объект state.entities.
  • selectAll: возвращает массив объектов с идентификаторами state.ids.
  • selectTotal: возвращает общее количество объектов, сохраняемых в этом состоянии.
  • selectById: учитывая состояние и идентификатор объекта, возвращает объект с этим идентификатором или undefined.

Если мы использем селекторы в глобальной областивидимости, то нам нужно будет самостоятельно указывать, с чем именно должна работать данная команда

const store = configureStore({
  reducer: {
    books: booksReducer,
  },
})
 
const simpleSelectors = booksAdapter.getSelectors()
const globalizedSelectors = booksAdapter.getSelectors((state) => state.books)
 
// указываем конкретный объект, с которым будет работать селектор
const bookIds = simpleSelectors.selectIds(store.getState().books)
 
// уже этот селектор знает, с каким объектом в стейте он имеет дело
const allBooks = globalizedSelectors.selectAll(store.getState())

И теперь нам нужно добавить функционал по вытаскиванию всех элементов из стейта. Сделать это легко - мы просто из файла со слайсом будем экспортировать функцию selectAll, которую привяжем к state.heroes

components > heroesList > heroesSlice.js

// и теперь все функции для получения значений из стейта, которые мы используем, будут обращаться к героям
export const { selectAll } = heroesAdapter.getSelectors((state) => state.heroes);

Вторым аргументом в листе мы возвращали с помощью отдельной функции список всех персонажей. Теперь же можно вернуть всё с помощью функции-селектора

И теперь приложение работает, так как на фронт попадает тот массив, который нам и был нужен

Если мы попытаемся вывести массив с логами о героях, то тут можно увидеть, что в первые две смены состояния были пустые, но дальше мы получили массив с объектами

И теперь можно переписать все операции модификации стейта на круд-операции из самого адаптера.

Тут нужно сказать, что данные по reducer, действия над которыми происходят в пространстве имён heroes, будут помещаться в state.entities.heroes. Однако напрямую с ними взаимодействовать не придётся, так как мы их можем автоматически достать через селекторы

Ну и так же можно оптимизировать код и создавать селекторы (библиотека Reselect) уже внутри самого слайса

Вышеописанный подход с использованием Redux позволяет нам скрывать логическую часть работы с данными от самого компонента, который эти данные отображает. Теперь View работает отдельно и занимается только отображением данных без какого-либо их преобразования.

024 Redux Toolkit RTK Query

RTK Qeury и React Query концептуально меняют подход к использованию данных в приложении. Они предлагают не изменять глобальные состояния, а оперировать загруженными данными

Сейчас наше взаимодействие выглядит так:

  • мы отправляем запрос на сервер
  • мы получаем данные с сервера
  • отправляем изменение состояния в стейт

Далее потребуются две основные функции для работы с Query:

  • createApi - полностью описывает поведение RTK Query
  • fetchBaseQuery - модифицированная функция fetch()

Чтобы начать работать с данной библиотекой, нужно будет написать будущее АПИ общения с RTK Query:

  • Пишем функцию createApi, которая описывает взаимодействие с библиотекой и передаём в неё объект
    • reducerPath будет указывать то пространство имён, в котором происходят все запросы
    • baseQuery описывает полностью базовые параметры запроса на сервер
      • функция fetchBaseQuery выполняет функцию фетча, но хранит дополнительные параметры для ртк
      • baseUrl принимает строку для обращения к серверу
    • endpoints хранит функцию, которая возвращает объект с теми запросами и изменениями, что мы можем вызвать
      • свойство объекта будет входить в имя хука, который будет сгенерирован. Если мы имеем имя getHeroes, то библиотека сформирует хук useGetHeroes[Query/Mutation] (суффикс уже будет зависеть от типа того, что делает хук - просто запрос или мутация данных)

api > apiSlice.js

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
 
// эта функция генерирует хуки (эндпоинты) на каждое наше действие
// так же она генерирует и редьюсер (как createSlice)
export const apiSlice = createApi({
	// путь к редьюсеру
	reducerPath: 'api',
	// формирование базового запроса
	baseQuery: fetchBaseQuery({
		// тут указываем ссылку до сервера
		baseUrl: 'http://localhost:3001',
	}),
	// тут указываем те операции, которые будем проводить по базовому адресу (получение, отправка, удаление данных)
	// query - запросы, которые получают данные и сохраняют их
	// mutation - запросы на изменение данных на сервере
	endpoints: (builder) => ({
		// тут мы просто хотим получить героев с сервера
		getHeroes: builder.query({
			query: () => '/heroes',
		}),
	}),
});
 
export const { useGetHeroesQuery } = apiSlice;

Далее нужно сконфигурировать хранилище:

  • чтобы добавить новый reduce, нужно в качестве свойства указать динамическую строку apiSlice.reducerPath и указать значение переменной самого редьюсера apiSlice.reducer
  • далее добавляем middleware для обработки специфических запросов RTK Query

store > index.js

import { apiSlice } from '../api/apiSlice';
 
const store = configureStore({
	reducer: {
		heroes,
		filters,
		// добавляем reducer, сформированный через RTK Query
		[apiSlice.reducerPath]: apiSlice.reducer
	},
	devTools: process.env.NODE_ENV === 'development',
	middleware: (getDefaultMiddleware) =>
		getDefaultMiddleware().concat(
			stringMiddleware,
			// передаём сюда middleware для обработки запросов RTK Query
			apiSlice.middleware
		),
});

И уже тут мы можем воспользоваться хуком, который сгенерировал Query. Через хук useGetHeroesQuery мы получаем все те промежуточные состояния, которые могут быть присвоены запросы, который приходит с сервера

Так же нужно упомянуть, что все те данные, что мы получили с сервера будут кешироваться в браузере на определённое время

components > heroesList > HeroesList.js

import { heroDeleted, fetchHeroes, filteredHeroesSelector } from './heroesSlice';
import { useGetHeroesQuery } from '../../api/apiSlice';
 
const HeroesList = () => {
	const {
		// тут нужно установить значение по умолчанию, так как это асинхронный код
		data: heroes = [], // получаем данные, которые запишем в переменную heroes
		isUninitialized, // если true, то запрос вообще не был отправлен
		isFetching, // состояние отправленного запроса
		isLoading, // состояние загрузки
		isError, // состояние ошибки
		error, // переменная с ошибкой
	} = useGetHeroesQuery();
 
	// получаем доступ к выбранному пользователем фильтру
	const activeFilter = useSelector((state) => state.filters.activeFilter);
 
	// это фильтр героев, которых мы получили с сервера
	const filteredHeroes = useMemo(() => {
		// создаём копию массива персонажей
		const filteredHeroes = heroes.slice();
 
		if (activeFilter === 'all') {
			return filteredHeroes;
		} else {
			return filteredHeroes.filter((item) => item.element === activeFilter);
		}
	}, [heroes, activeFilter]);
 
	/// CODE ...
 
	if (isLoading) {
		return <Spinner />;
	} else if (isError) {
		return <h5 className='text-center mt-5'>Ошибка загрузки</h5>;
	}
 
	/// CODE ...
 
	// и сюда подставляем отсортированных персонажей
	const elements = renderHeroesList(filteredHeroes);
	return <TransitionGroup component='ul'>{elements}</TransitionGroup>;
};
 
export default HeroesList;

И наше приложение работает теперь так же, как и до изменений - список героев нормально получается с сервера

Далее добавим запрос на мутацию стейта, который будет отправлять на сервер запрос на добавление персонажа в список

api > apiSlice.js

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
 
export const apiSlice = createApi({
	reducerPath: 'api',
	baseQuery: fetchBaseQuery({
		baseUrl: 'http://localhost:3001',
	}),
	endpoints: (builder) => ({
		getHeroes: builder.query({
			query: () => '/heroes',
		}),
		createHero: builder.mutation({
			query: (hero) => ({
				url: '/heroes',
				method: 'POST',
				body: hero,
			}),
		}),
	}),
});
 
export const { useGetHeroesQuery, useCreateHeroMutation } = apiSlice;

И далее можно будет применить данный хук мутации в коде:

  • хук возвращает массив из двух объектов:
    • функция отправки мутации данных
    • объект со статусом обработки запроса (тот же объект, что и у query)
  • далее можно будет применить функцию отправки героя на сервер и передать в него нового героя
  • и для нормальной работы всех обработчиков (объект из второго аргумента) используется функция unwrap()

Однако после отправки запроса на сервер, мы не получаем на главной странице нового списка персонажей с нашим созданным героем.

Чтобы исправить данную ситуацию, нам нужно будет использовать наш стейт api и обновлять стейт на фронте, когда мы получаем актуальные данные с сервера

Чтобы подвязать выполнение одних запросов под другие, нужно использовать теги в createApi

И теперь тут правим ситуацию:

  • объявляем глобально в АПИ поле tagTypes, которое принимает в себя массив тегов, которые будут использоваться для общения между методами
  • добавляем в первый запрос providesTags и тег, по которому будет оповещаться данный метод, чтобы он сработал при изменении данных
  • добавляем в запрос мутации invalidatesTags, который будет отправлять в хранилище тегов запрос, откуда на все подписанные методы с подходящими тегами будет приходить уведомление о переиспользовании

api > apiSlice.js

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
 
export const apiSlice = createApi({
	reducerPath: 'api',
	baseQuery: fetchBaseQuery({
		baseUrl: 'http://localhost:3001',
	}),
	// тут мы задаём, какие метки (теги) существуют
	tagTypes: ['Heroes'],
	endpoints: (builder) => ({
		getHeroes: builder.query({
			query: () => '/heroes',
			// указываем, когда данные запрашиваются при помощи обычного запроса
			providesTags: ['Heroes'], // а тут мы подцепляемся к тегам - функция триггерится от тегов
		}),
		createHero: builder.mutation({
			query: (hero) => ({
				url: '/heroes',
				method: 'POST',
				body: hero,
			}),
			// если мы мутировали эти данные, то по какой метке мы должны получить эти данные
			invalidatesTags: ['Heroes'], // а тут мы указываем, что именно нужно обновить повторно, когда данные изменились
		}),
	}),
});
 
export const { useGetHeroesQuery, useCreateHeroMutation } = apiSlice;

И теперь всё работает - при создании нового персонажа триггерится функция обновления списка персонажей на фронте

api > apiSlice.js

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
 
export const apiSlice = createApi({
	reducerPath: 'api',
	baseQuery: fetchBaseQuery({
		baseUrl: 'http://localhost:3001',
	}),
	tagTypes: ['Heroes'],
	endpoints: (builder) => ({
		getHeroes: builder.query({
			query: () => '/heroes',
			providesTags: ['Heroes'],
		}),
		createHero: builder.mutation({
			query: (hero) => ({
				url: '/heroes',
				method: 'POST',
				body: hero,
			}),
			invalidatesTags: ['Heroes'],
		}),
		deleteHero: builder.mutation({
			query: (id) => ({
				url: `/heroes/${id}`,
				method: 'DELETE',
			}),
			invalidatesTags: ['Heroes'],
		}),
	}),
});
 
export const {
	useGetHeroesQuery,
	useCreateHeroMutation,
	useDeleteHeroMutation
} = apiSlice;

И по итогу мы теперь можем удалить весь heroesSlice.js, который использовался для реализации управления состояниями

И теперь список персонажей выглядит таком образом:

components > heroesList > HeroesList.js

import { useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
 
import HeroesListItem from '../heroesListItem/HeroesListItem';
import Spinner from '../spinner/Spinner';
 
import './heroesList.scss';
 
import { useGetHeroesQuery, useDeleteHeroMutation } from '../../api/apiSlice';
 
const HeroesList = () => {
	const {
		data: heroes = [],
		isUninitialized,
		isFetching,
		isLoading,
		isError,
		error,
	} = useGetHeroesQuery();
 
	const activeFilter = useSelector((state) => state.filters.activeFilter);
 
	const filteredHeroes = useMemo(() => {
		const filteredHeroes = heroes.slice();
 
		if (activeFilter === 'all') {
			return filteredHeroes;
		} else {
			return filteredHeroes.filter((item) => item.element === activeFilter);
		}
	}, [heroes, activeFilter]);
 
	const [deleteHero] = useDeleteHeroMutation();
 
	const onDelete = useCallback((id) => {
		deleteHero(id);
	}, []);
 
	if (isLoading) {
		return <Spinner />;
	} else if (isError) {
		return <h5 className='text-center mt-5'>Ошибка загрузки</h5>;
	}
 
	const renderHeroesList = (arr) => {
		if (arr.length === 0) {
			return (
				<CSSTransition timeout={0} classNames='hero'>
					<h5 className='text-center mt-5'>Героев пока нет</h5>
				</CSSTransition>
			);
		}
 
		return arr.map(({ id, ...props }) => {
			return (
				<CSSTransition key={id} timeout={500} classNames='hero'>
					<HeroesListItem {...props} onDelete={() => onDelete(id)} />
				</CSSTransition>
			);
		});
	};
 
	const elements = renderHeroesList(filteredHeroes);
	return <TransitionGroup component='ul'>{elements}</TransitionGroup>;
};
 
export default HeroesList;

И сейчас можно сделать следующие выводы:

  • RTK Query предлагает нам не пользоваться каким-либо единственным хранилищем состояния, а пользоваться активным взаимодействием с сервером для актуализации данных
  • В браузере же данные хранятся только в кешированном формате (то есть тех данных, что хранится просто в нашем стейте просто нет - они в памяти браузера)

Ссылки

002 -Github https://github.com/AlariCode/top-app-demo

002 -Figma https://www.figma.com/file/eHIyKZXUUtMf1BQiuv6tTA/%D0%9A%D1%83%D1%80%D1%81-2-NextJS?node-id=0%3A1

002 -Telegram https://t.me/purple_code

002 - Telegram https://t.me/purple_code_channel

004 VSCode https://code.visualstudio.com

004 Insomnia https://insomnia.rest/download

004 NodeJS https://nodejs.org/en/

004 NVM https://github.com/nvm-sh/nvm

007 TypeScript-Deep-Dive https://basarat.gitbook.io/typescript/

029 - https://www.figma.com/file/eHIyKZXUUtMf1BQiuv6tTA/%D0%9A%D1%83%D1%80%D1%81-2-NextJS?node-id=1%3A2

114 AI-ARIA-Authoring-Practices https://www.w3.org/TR/wai-aria-practices-1.1/

118 MDN-Web-Docs-ARIA https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques