Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера Хостинг

Что происходит в том случае, когда зависимости лгут react

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

  useEffect(() => {
    document.title = 'Hello, '   name;
  }, [name]);

Так как зависимости различаются — эффект перезапускается

Но если мы для этого эффекта укажем, в качестве зависимостей, пустой массив, [], тогда, при обновлении данных, используемых в эффекте, он перезапущен не будет:

  useEffect(() => {
    document.title = 'Hello, '   name;
  }, []); // Неправильно: в зависимостях нет name

Зависимости выглядят одинаково — эффект повторно не вызывается

В данном случае проблема может показаться очевидной и интуитивно понятной. Но интуиция может подвести в других случаях, когда из памяти «всплывают» идеи, навеянные работой с компонентами, основанными на классах.

Например, предположим, мы создаём счётчик, который увеличивается каждую секунду. Если использовать для его реализации класс, то внутреннее чутьё подскажет нам следующее: «Один раз настроить setInterval для запуска счётчика и один раз использовать clearInterval для его остановки».

Вот пример реализации этого механизма. Когда мы, в голове, переносим подобный подход, планируя воспользоваться useEffect, то мы, инстинктивно, указываем в качестве зависимостей []. Запустить-то счётчик нам нужно лишь один раз, верно?

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count   1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

Однако, вот незадача, в таком случае счётчик обновится лишь

Если в голове у вас имеется модель, в соответствии с которой «зависимости позволяют мне указывать на то, когда я хочу повторно вызывать эффект», то этот пример может довести вас до экзистенциального кризиса. Ведь вам нужно, чтобы эффект был вызван лишь один раз, так как в его коде вы, используя setInterval, запускаете счётчик. Почему же код работает не так, как ожидается?

Но если вы знаете о том, что зависимости — это наша подсказка для React обо всём том, что эффект использует из области видимости рендера, то такое поведение этой программы вас не удивит. А именно, эффект использует count, но мы не сообщили React правду об этом, указав, в качестве списка зависимостей, пустой массив. И когда эта ложь приведёт к проблемам — лишь вопрос времени.

В первой операции рендеринга count равняется 0. В результате setCount(count 1) в эффекте первого рендера означает setCount(0 1). Так как мы никогда этот эффект повторно не вызываем, причиной чему — зависимости в виде [], каждую секунду будет вызываться setCount(0 1):

// Первый рендеринг, состояние равно 0
function Counter() {
  // ...

  useEffect(
    // Эффект из первого рендера
    () => {
      const id = setInterval(() => {
        setCount(0   1); // Всегда setCount(1)
      }, 1000);
      return () => clearInterval(id);
    },
    [] // Никогда не перезапускается
  );
  // ...

}

// В каждом следующем рендере состояние равно 1
function Counter() {
  // ...

  useEffect(
    // Этот эффект всегда игнорируется из-за того, что
    // мы солгали React о зависимостях, передав пустой массив.

    () => {
      const id = setInterval(() => {
        setCount(1   1);
      }, 1000);
      return () => clearInterval(id);
    },
    []
  );
  // ...

}

Мы солгали React, сообщив о том, что наш эффект не зависит от значений из компонента, хотя на самом деле — зависит.

Наш эффект использует count — значение, находящееся внутри компонента (но за пределами эффекта):

  const count = // ...


  useEffect(() => {
    const id = setInterval(() => {
      setCount(count   1);
    }, 1000);
    return () => clearInterval(id);
  }, []);


В результате указание пустого массива в качестве списка зависимостей приводит к ошибке. React сравнит зависимости и не станет повторно вызывать эффект.

Зависимости не меняются, поэтому вызов эффекта можно пропустить

Непросто искать решения проблем такого рода в уме. Поэтому я советую вам жёстко придерживаться правила, которое заключается в том, что React всегда нужно честно сообщать о зависимостях эффектов, и в том, чтобы указывать все эти зависимости. Если вы хотите получить поддержку линтера в выполнении этого правила — мы приготовили кое-что для вас и для вашей команды.

Бесплатная подборка из 40 эффектов css

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

Эти часы созданы с использованием одного из основных инструментов CSS3 – rotate и с подключением библиотеки JQuery.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Более классические, аналоговые часы. Они созданы с помощью webkit transition и свойства transform CSS. А вот для того, чтобы время соответствовало текущему, понадобиться JavaScript.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Вращение и перемещение по сторонам куба будет производиться стандартными клавишами «вверх», «вниз», «влево» и «вправо». Сама 3D-фигура построена с использованием webkit-perspective, -webkit-transform и -webkit-transition.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Здесь представлено уже несколько 3D-кубов, использующих CSS3 и непосредственно свойства transform и transition. Наведение курсора на куб заставляет его отъехать в сторону, открывая текст, который находиться на другой стороне фигуры.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Эффект «меню-гармошки» на чистом CSS, где клик по каждой из строк открывает дополнительное окно в теле самого списка. Собственная анимация в браузерах на базе WebKit.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Это анимированный автоматический параллакс-скроллинг с использованием CSS переходов на основе WebKit. При наведении на текстовое окно, звезды на фоне начинают плавно двигаться в сторону. Создается эффект полета.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Культовый фильм «Матрица» — одна из лучших фантастических кинокартин. На примере показано, как воссоздать примерно такую же удивительную анимацию (черный экран с бегущими цифрами) в CSS3.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

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

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

В этом примере картинки просто увеличиваются при наведении. Незамысловатый, но порой весьма полезный эффект.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

В качестве альтернативы JavaScript, в посте предложено семь эффектов на CSS3: различные блоки, которые вращаются, исчезают, выезжают, увеличиваются и т.д.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

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

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Эффект скольжения виниловых пластинок создается с помощью переходов CSS3 и HTML. Подобная анимация оживляет веб-страницу, добавляет оригинальности стандартной обложке альбома и т.д.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

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

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

При нажатии на треугольник, он начинает вращаться.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Целое космическое пространство, умещенное в CSS. Выступает примером наложения вращающихся слоев (более заметно при уменьшении масштаба изображения в браузере).

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Интересный эффект CSS, с помощью которого известная картина Диего Веласкеса «Менины» начинает казаться трехмерной.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

В нижней части экрана представлен набор основных иконок Mac OS Х, которые при наведении увеличиваются. Эффект придает динамики сайту.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

CSS3 эффекты и свойств Drop-In Modals помогут в создании быстрого, анимированного и простого изменения модальных окон.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Трансформация изменяет внешний вид элемента в браузере. Показано на примере ракеты, которая «летит» из одного конца экрана в другой. Могут использоваться инструменты перемещения, вращения и т.д.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Цветные часы созданы на основе jQuery и CSS3. Подобный эффект придется кстати в контексте ожидания времени завершения какого-нибудь конкурса, голосования и тому подобного.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Это замечательная галерея, которая позволяет сортировать и раскладывать изображения в выборочном порядке. Для интерактивности галерея использует JQuery, JQuery UI и JQuery плагин FancyBox. Lightbox поддерживает название и описание картинок, группирует их и автоматически выстраивает слайды в ряд.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

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

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Этот пример представляет собой динамический набор карточек с использованием функций HTML и CSS3.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Выдвижное меню из примера создано комбинацией CSS3 и JQuery. При наведении на картинку появляется всплывающее окно с текстом.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

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

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

На примере продемонстрировано, как с помощью анимации CSS и SVG создать Fisheye меню. В качестве дополнительного бонуса используется демо-SVG в тэге IMG.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

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

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Знаменитые титры из «Звездных войн». Для их запуска будет достаточно HTML и CSS.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Опять таки, увеличивающиеся при наведении значки.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Возможны несколько вариантов демонстрации.

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

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

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

И снова «Звездные войны» — этот движущийся шагоход AT-AT сделан с помощью CSS3.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

При клике на строку, таблица раскладывается.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

При наведении секции меняют цвет и выдвигаются.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Показательная подборка разнообразных формаций эффектов. Достаточно кликнуть по клавишам «magic», «swap» и т.д. для демонстрации эффекта.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Анимационный прогресс бар на CSS.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

В примере показано, как создать салют из кругов на JQuery и CSS.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

На примере показано, каким образом можно создать анимированную кнопку on/off с помощью CSS.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

Пример оригинальной разноцветной анимации загрузки на CSS.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

40. Выпадающее меню
Еще один вариант простого и симпатичного выпадающего меню на CSS.

Безопасность хостинга: как это устроено и на что обращать внимание при выборе провайдера

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

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

Первая стратегия заключается в исправлении массива зависимостей, во внесении в него всех значений, находящихся в компоненте, которые используются внутри эффекта. Добавим в массив зависимостей count:

useEffect(() => {
  const id = setInterval(() => {
    setCount(count   1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);


Теперь массив зависимостей исправлен. Возможно, такое решение не идеально, но это — первая проблема, которую нам нужно решить. Теперь изменение

count

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

count

из его рендера, выполняя операцию

setCount(count 1)

// Первый рендеринг, состояние равно 0
function Counter() {
  // ...

  useEffect(
    // Эффект из первого рендера
    () => {
      const id = setInterval(() => {
        setCount(0   1); // setCount(count   1)
      }, 1000);
      return () => clearInterval(id);
    },
    [0] // [count]
  );
  // ...

}

// Второй рендер, состояние равно 1
function Counter() {
  // ...

  useEffect(
    // Эффект из второго рендера
    () => {
      const id = setInterval(() => {
        setCount(1   1); // setCount(count   1)
      }, 1000);
      return () => clearInterval(id);
    },
    [1] // [count]
  );
  // ...

}

Такой подход позволяет решить проблему, но

setInterval

будет, при каждом изменении

count

, очищаться и запускаться снова. Вероятно, нас это не устроит.

Зависимости различаются, поэтому эффект мы перезапускаем

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

Рассмотрим несколько распространённых подходов избавления от зависимостей.

Делаем эффект самодостаточным

Итак, мы хотим избавиться от зависимости

count

в эффекте.

useEffect(() => {
    const id = setInterval(() => {
      setCount(count   1);
    }, 1000);
    return () => clearInterval(id);
  }, [count]);


Для того чтобы это сделать, зададимся вопросом о том, для чего мы используем

count

. Возникает такое ощущение, что мы используем

count

только в вызове

setCount

. В таком случае нам, на самом деле, совершенно не нужно иметь

count

в области видимости. Когда мы хотим обновить состояние, основываясь на предыдущем состоянии, мы можем использовать

setState

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c   1);
    }, 1000);
    return () => clearInterval(id);
  }, []);


Я предпочитаю рассматривать подобные случаи как «ненастоящие зависимости». Да, значение

count

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

setCount(count 1)

. Однако

count

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

count 1

и «вернуть» его React. Но React уже знает о текущем значении

count

. Всё, что нам нужно сообщить React — это сведения о том, что соответствующее значение состояния, в его текущем виде, нужно увеличить на единицу.

Именно эту задачу и решает конструкция setCount(c => c 1). Её можно воспринимать как «отправку React инструкции», описывающей то, как должно изменяться состояние. Такая «форма обновления» оказывается полезной и в других случаях, например, если выполняется объединение множества обновлений.

Обратите внимание на то, что мы, на самом деле, избавились от зависимости. И мы при этом не обманываем React. Наш эффект больше не выполняет чтение значения count из области видимости рендера:

Зависимости не меняются, поэтому эффект повторно не вызывается

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

Даже хотя этот эффект вызывается лишь один раз, коллбэк setInterval, который принадлежит первому рендеру, прекрасно справляется с отправкой инструкции c => c 1 при каждом срабатывании таймера. Ему не нужно знать текущее значение count. React уже известно это значение.

Каждому рендеру принадлежит… всё


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

Попробуем выполнить мысленный эксперимент. Рассмотрим следующий код:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count   1)}>
        Click me
      </button>
    </div>
  );
}

Что будет выведено в консоль в том случае, если быстро щёлкнуть по кнопке несколько раз?

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

Щелчки по кнопке и вывод данных в консоль

Тут вы можете подумать: «Конечно, именно так это и работает! Да и может ли эта программа вести себя иначе?».

Ну, на самом деле, this.setState в компонентах, основанных на классах, работает не так. Поэтому легко допустить ошибку, если полагать, что следующий вариант примера, в котором используется компонент, основанный на классе, эквивалентен предыдущему:

  componentDidUpdate() {
    setTimeout(() => {
      console.log(`You clicked ${this.state.count} times`);
    }, 3000);
  }

Дело в том, что

this.state.count

всегда указывает на самое свежее значение

count

, а не на значение, принадлежащее конкретному рендеру. В результате, вместо последовательности сообщений с разными числами, мы, быстро щёлкнув по кнопке 5 раз, увидим 5 одинаковых сообщений.

Щелчки по кнопке и вывод данных в консоль

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

Замыкания — это отличный инструмент в том случае, если значение, которое «запирают» в замыкании, никогда не меняется. Это облегчает их использование и размышления о них, так как, в сущности, речь идёт о константах. И, как мы уже говорили, свойства и состояние никогда не меняются в конкретном рендере.

Как насчёт очистки?

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

Рассмотрим этот код:

useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
    };
  });

Предположим,

props

— это объект

{id: 10}

в первой операции рендеринга, и

{id: 20}

— во второй. Можно подумать, что тут происходит примерно следующее:

(Но это, на самом деле, не совсем так.)

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

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

Тут вы можете задаться вопросом о том, как операция очистки предыдущего эффекта всё ещё может видеть «старое» значение

props

, содержащее

{id: 10}

, после того, как в

props

записано

{id: 20}

Надо отметить, что мы уже здесь были…

А может это — та же самая кошка?

Приведём цитату из предыдущего раздела: «каждая функция внутри механизма рендеринга компонента (включая обработчики событий, эффекты, тайм-ауты или вызовы API внутри них) захватывает свойства и состояние вызова рендера, который их определил».

Теперь ответ очевиден! В ходе операции очистки эффекта не производится чтение «самых свежих» свойств, что бы это ни значило. Эта операция читает свойства, которые принадлежат рендеру, в котором они определены:

// Первый рендер, в props записано {id: 10}
function Example() {
  // ...

  useEffect(
    // Эффект из первого рендера
    () => {
      ChatAPI.subscribeToFriendStatus(10, handleStatusChange);
      // Очистка для эффекта из первого рендера
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(10, handleStatusChange);
      };
    }
  );
  // ...

}

// Следующий рендер, в props записано {id: 20}
function Example() {
  // ...

  useEffect(
    // Эффект из второго рендера
    () => {
      ChatAPI.subscribeToFriendStatus(20, handleStatusChange);
      // Очистка для эффекта из второго рендера
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(20, handleStatusChange);
      };
    }
  );
  // ...

}

Королевства будут расти и превращаться в пепел, Солнце сбросит внешние оболочки и станет белым карликом, последняя цивилизация исчезнет… Но ничто не заставит свойства, которые «увидела» операция очистки эффекта из первого рендеринга, превратиться во что-то, отличающееся от

{id: 10}

Именно это позволяет React работать с эффектами сразу после вывода изображения на экран. Это, без дополнительных усилий со стороны программиста, делает его приложения быстрее. Если нашему коду понадобятся старые значения props, они никуда не деваются.

Плывём против течения

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

В результате следующие два компонента эквивалентны:

function Example(props) {
  useEffect(() => {
    setTimeout(() => {
      console.log(props.counter);
    }, 1000);
  });
  // ...

}
function Example(props) {
  const counter = props.counter;
  useEffect(() => {
    setTimeout(() => {
      console.log(counter);
    }, 1000);
  });
  // ...

}

При этом неважно, выполняется ли внутри компонента «заблаговременное» чтение из свойств или состояния. Они не изменятся! Внутри области видимости отдельно взятого рендера свойства и состояния не изменяются. Надо отметить, что деструктурирование свойств делает это более очевидным.

Конечно, иногда, внутри какого-нибудь коллбэка, объявленного в эффекте, нужно прочитать самое свежее значение, а не то, что было захвачено. Легче всего это сделать, используя ссылки ref, почитать об этом можно в последнем разделе этой статьи.

Учитывайте то, что когда вам нужно прочитать будущие свойства или состояние из функции, принадлежащей ранее выполненной операции рендеринга, то вы, так сказать, пытаетесь плыть против течения. Нельзя сказать, что это неправильно (и иногда это просто необходимо), но менее «чистым» решением может показаться выход за рамки традиционной парадигмы React-разработки.

Вот версия нашего примера со счётчиком щелчков, основанная на функции, которая воспроизводит поведение той его версии, которая основана на классе:

Поднимаем планку

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

Если смотреть на вещи с точки зрения useEffect, то всё, по умолчанию, является синхронизированным. Побочные эффекты стали частью потока данных React. Если сделать всё правильно, то при каждом вызове useEffect компонент гораздо лучше обрабатывает пограничные случаи.

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

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

Я видел, как в различных приложениях создаются их собственные хуки, наподобие useFetch, который инкапсулирует некоторую логику аутентификации таких приложений, или useTheme, который использует контекст темы. После того, как вы освоитесь с этими инструментами, вы не особенно часто будете прибегать к использованию useEffect. Но гибкость, предоставляемая этим механизмом, идёт на пользу каждому хуку, построенному на его основе.

До сих пор, например, useEffect наиболее часто используется для загрузки данных. Но загрузка данных — это не совсем то, что относится к проблеме синхронизации. Это особенно очевидно по той причине, что зависимости в таких случаях обычно представлены пустым массивом. Что мы вообще синхронизируем с их помощью?

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

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

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

Синхронизация, а не жизненный цикл


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

программ.

Предположим, мой компонент выглядит так:

function Greeting({ name }) {
  return (
    <h1 className="Greeting">
      Hello, {name}
    </h1>
  );
}


При его использовании совершенно неважно, будет ли сначала отрендерено

, а потом —

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

. И в том и в другом случаях в итоге мы увидим текст

Hello, Yuzhi

Говорят, что важен путь, а не цель. Если говорить о React, то справедливым окажется обратное утверждение. Здесь важна цель, а не то, каким путём к ней идут. В этом и заключается разница между вызовами вида $.addClass и $.removeClass в jQuery-коде (это — то, что мы называем «путём»), и указание того, каким должен быть CSS-класс в React (то есть — того, какой должна быть «цель»).

React синхронизирует DOM с тем, что имеется в текущих свойствах и состоянии. При рендеринге нет разницы между «монтированием» и «обновлением».

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

function Greeting({ name }) {
  useEffect(() => {
    document.title = 'Hello, '   name;
  });
  return (
    <h1 className="Greeting">
      Hello, {name}
    </h1>
  );
}

В этом состоит незначительное отличие восприятия

useEffect

от привычной ментальной модели, в которую входят понятия монтирования, обновления и размонтирования компонентов. Если вы пытаетесь создать эффект, который ведёт себя по-особому при первом рендеринге компонента, то вы пытаетесь плыть против течения! Синхронизация не удастся в том случае, если наш результат зависит от «пути», а не от «цели».

Не должно быть разницы между тем, выполняем ли мы рендеринг компонента сначала со свойством A, потом с B, а потом — со свойством C, и той ситуацией, когда мы сразу же рендерим его со свойством C. Хотя в процессе работы этих двух вариантов кода и могут быть некоторые временные различия (например, возникающие при загрузке каких-либо данных), в итоге конечный результат должен быть тем же самым.

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

Как с этим бороться?

Состояние гонки

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

class Article extends Component {
  state = {
    article: null
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  async fetchData(id) {
    const article = await API.fetchArticle(id);
    this.setState({ article });
  }
  // ...

}


Как вы, возможно, знаете, этот код содержит ошибки. Он не поддерживает обновления. А вот — ещё один подобный пример, который можно найти в интернете:

class Article extends Component {
  state = {
    article: null
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      this.fetchData(this.props.id);
    }
  }
  async fetchData(id) {
    const article = await API.fetchArticle(id);
    this.setState({ article });
  }
  // ...

}

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

{id: 10}

, потом перехожу на статью с

{id: 20}

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

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

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

Если используемые вами асинхронные механизмы поддерживают отмену операций, то надо отметить, что это замечательно! Это позволяет отменить асинхронный запрос прямо в функции очистки.

Кроме того, простейшим временным решением этой проблемы является контроль асинхронных операций с помощью логических переменных:

function Article({ id }) {
  const [article, setArticle] = useState(null);

  useEffect(() => {
    let didCancel = false;

    async function fetchData() {
      const article = await API.fetchArticle(id);
      if (!didCancel) {
        setArticle(article);
      }
    }

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [id]);

  // ...

}

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

У каждого рендера есть собственные свойства и состояние


Прежде чем мы сможем обсуждать эффекты, нам надо поговорить о рендеринге.

Вот функциональный компонент-счётчик.

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count   1)}>
        Click me
      </button>
    </div>
  );
}

Внимательно присмотритесь к строке

. Что она означает? «Наблюдает» ли каким-то образом константа

count

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

происходящего.

В нашем примере count — это просто число. Это не некая магическая «привязка данных», не некий «объект-наблюдатель» или «прокси», или что угодно другое. Перед нами — старое доброе число, вроде этого:

const count = 42;
// ...

<p>You clicked {count} times</p>
// ...

Во время первого вывода компонента значение

count

, получаемое из

useState()

, равняется 0. Когда мы вызываем

setCount(1)

, React снова вызывает компонент. В этот раз

count

будет равно 1. И так далее:

// Во время первого рендеринга
function Counter() {
  const count = 0; // Возвращено useState()
  // ...

  <p>You clicked {count} times</p>
  // ...

}

// После щелчка наша функция вызывается снова
function Counter() {
  const count = 1; // Возвращено useState()
  // ...

  <p>You clicked {count} times</p>
  // ...

}

// После ещё одного щелчка функция вызывается снова
function Counter() {
  const count = 2; // Возвращено useState()
  // ...

  <p>You clicked {count} times</p>
  // ...

}


React вызывает компонент всякий раз, когда мы обновляем состояние. В результате каждая операция рендеринга «видит» собственное значение состояния

counter

, которое, внутри функции, является константой.

В результате эта строка не выполняет какую-то особую операцию привязки данных:

Она лишь встраивает числовое значение в код, формируемый при рендеринге. Это число предоставляется средствами React. Когда мы вызываем

setCount

, React снова вызывает компонент с другим значением

count

. Затем React обновляет DOM для того чтобы объектная модель документа соответствовала бы самым свежим данным, выведенным в ходе рендеринга компонента.

Самый главный вывод, который можно из этого сделать, заключается в том, что count является константой внутри любого конкретного рендера и со временем не меняется. Меняется компонент, который вызывается снова и снова. Каждый рендер «видит» собственное значение count, которое оказывается изолированным для каждой из операций рендеринга.

В этом материале можно найти подробности о данном процессе.

У каждого рендера есть собственные эффекты

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

Рассмотрим пример из документации, который очень похож на тот, который мы уже разбирали:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count   1)}>
        Click me
      </button>
    </div>
  );
}

Теперь у меня к вам вопрос. Как эффект считывает самое свежее значение

count

Может быть, тут используется некая «привязка данных», или «объект-наблюдатель», который обновляет значение count внутри функции эффекта? Может быть count — это мутабельная переменная, значение которой React устанавливает внутри нашего компонента, в результате чего эффект всегда видит её самую свежую версию?

Нет.

Мы уже знаем, что в рендере конкретного компонента count представляет собой константу. Даже обработчики событий «видят» значение count из рендера, которому они «принадлежат» из-за того, что count — это константа, находящаяся в определённой области видимости. То же самое справедливо и для эффектов!

И надо отметить, что это не переменная count каким-то образом меняется внутри «неизменного» эффекта. Перед нами — сама функция эффекта, различная в каждой операции рендеринга.

Каждая версия «видит» значение count из рендера, к которому она «принадлежит»:

// Во время первого рендеринга
function Counter() {
  // ...

  useEffect(
    // Функция эффекта из первого рендера
    () => {
      document.title = `You clicked 
// Во время первого рендеринга
function Counter() {
  // ...

  useEffect(
    // Функция эффекта из первого рендера
    () => {
      document.title = `You clicked ${0} times`;
    }
  );
  // ...

}

// После щелчка наша функция вызывается снова
function Counter() {
  // ...

  useEffect(
    // Функция эффекта из второго рендера
    () => {
      document.title = `You clicked ${1} times`;
    }
  );
  // ...

}

// После ещё одного щелчка функция вызывается снова
function Counter() {
  // ...

  useEffect(
    // Функция эффекта из третьего рендера
    () => {
      document.title = `You clicked ${2} times`;
    }
  );
  // ..

}
times`;    }  );  // ... } // После щелчка наша функция вызывается снова function Counter() {  // ...  useEffect(    // Функция эффекта из второго рендера    () => {      document.title = `You clicked times`;    }  );  // ... } // После ещё одного щелчка функция вызывается снова function Counter() {  // ...  useEffect(    // Функция эффекта из третьего рендера    () => {      document.title = `You clicked times`;    }  );  // .. }

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

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

Эффект, концептуально, можно представить в качестве части результатов рендеринга.

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

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

React:


Компонент:

React:

Браузер:


React:

А теперь давайте разберём то, что происходит после щелчка по кнопке. На самом деле, многое тут повторяет предыдущий разбор, но кое-что здесь выглядит иначе:

Компонент:

React:


Компонент:

React:

Браузер:


React:

У каждого рендера имеются собственные обработчики событий

До сих пор всё понятно. А что можно сказать об обработчиках событий?

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

count

function Counter() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: '   count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count   1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
}

Предположим, я выполню следующую последовательность действий:

Увеличение значения count после щелчка по кнопке Show alert

Как вы думаете, что выведется в окне сообщения? Будет ли там выведено 5, что соответствует значению count на момент срабатывания таймера, или 3 — то есть значение count в момент нажатия на кнопку?

Сейчас вы узнаете ответ на этот вопрос, но, если хотите выяснить всё сами — вот рабочая версия этого примера.

Если то, что вы увидели, кажется вам непонятным — вот вам пример, который ближе к реальности. Представьте себе приложение-чат, в котором, в состоянии, хранится ID текущего получателя сообщения, и имеется кнопка Send. В этом материале происходящее рассматривается в подробностях. Собственно говоря, правильным ответом на вопрос о том, что появится в окне сообщения, является 3.

Механизм вывода окна сообщения «захватил» состояние в момент щелчка по кнопке.

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

Как же всё это работает?

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

Подобное поведение функций не является чем-то особенным для React — обычные функции ведут себя похожим образом:

function sayHi(person) {
  const name = person.name;
  setTimeout(() => {
    alert('Hello, '   name);
  }, 3000);
}

let someone = {name: 'Dan'};
sayHi(someone);

someone = {name: 'Yuzhi'};
sayHi(someone);

someone = {name: 'Dominic'};
sayHi(someone);

примере внешняя переменная

someone

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

sayHi

имеется локальная константа

name

, которая связана с

person

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

name

Это объясняет то, как наш обработчик события захватывает значение count в момент щелчка по кнопке. Если мы, работая с компонентами, применим тот же принцип, то окажется, что каждый рендер «видит» собственное значение count:

// Во время первого рендеринга
function Counter() {
  const count = 0; // Возвращено useState()
  // ...

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: '   count);
    }, 3000);
  }
  // ...

}

// После щелчка наша функция вызывается снова
function Counter() {
  const count = 1; // Возвращено useState()
  // ...

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: '   count);
    }, 3000);
  }
  // ...

}

// После ещё одного щелчка функция вызывается снова
function Counter() {
  const count = 2; // Возвращено useState()
  // ...

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: '   count);
    }, 3000);
  }
  // ...

}

В результате каждый рендер, фактически, возвращает собственную «версию»

handleAlertClick

. Каждая из таких версий «помнит» собственное значение

count

// Во время первого рендеринга
function Counter() {
  // ...

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: '   0);
    }, 3000);
  }
  // ...

  <button onClick={handleAlertClick} /> // Версия, хранящая значение 0
  // ...

}

// После щелчка наша функция вызывается снова
function Counter() {
  // ...

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: '   1);
    }, 3000);
  }
  // ...

  <button onClick={handleAlertClick} /> // Версия, хранящая значение 1
  // ...

}

// После ещё одного щелчка функция вызывается снова
function Counter() {
  // ...

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: '   2);
    }, 3000);
  }
  // ...

  <button onClick={handleAlertClick} /> // Версия, хранящая значение 2
  // ...

}


Именно поэтому в

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

count

из этих рендеров.

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

Надо отметить, что в вышеприведённом примере я встроил конкретные значения count прямо в функции handleAlertClick. Эта «мысленная» замена нам не повредит, так как константа count не может изменяться в пределах конкретного рендера.

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

Учим react различать эффекты

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

Предположим, у нас есть такой код:

Мы хотим обновить его до такого состояния:

React видит два объекта:

const oldProps = {className: 'Greeting', children: 'Hello, Dan'};
const newProps = {className: 'Greeting', children: 'Hello, Yuzhi'};


React просматривает свойства этих объектов и выясняет, что значение

children

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

className

осталось неизменным. Поэтому можно просто поступить так:

domNode.innerText = 'Hello, Yuzhi';
// domNode.className трогать не нужно


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

Например, возможно, компонент выполняет повторный рендеринг из-за изменения состояния:

function Greeting({ name }) {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    document.title = 'Hello, '   name;
  });

  return (
    <h1 className="Greeting">
      Hello, {name}
      <button onClick={() => setCounter(counter   1)}>
        Increment
      </button>
    </h1>
  );
}

Но эффект не использует значение

counter

из состояния. Эффект синхронизирует

document.title

со свойством

name

, но свойство

name

тут не меняется. Перезапись

document.title

при каждом изменении

counter

кажется решением, далёким от идеального.

Может ли React просто… сравнить эффекты?

let oldEffect = () => { document.title = 'Hello, Dan'; };
let newEffect = () => { document.title = 'Hello, Dan'; };
// Может ли React увидеть то, что эти функции делают одно и то же?

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

name

Именно поэтому, если нужно избежать ненужных перезапусков эффектов, эффекту можно передать массив зависимостей (такие массивы ещё называют deps), выглядящий как аргумент useEffect:

  useEffect(() => {
    document.title = 'Hello, '   name;
  }, [name]); // Наши зависимости

Это похоже на то, как если бы мы сказали React: «Слушай, я понимаю, что внутрь этой функции ты заглянуть не можешь, но я обещаю, что я будут использовать только

name

и ничего другого из области видимости рендера».

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

const oldEffect = () => { document.title = 'Hello, Dan'; };
const oldDeps = ['Dan'];

const newEffect = () => { document.title = 'Hello, Dan'; };
const newDeps = ['Dan'];

// React не может заглянуть в функцию, но он может сравнить зависимости.

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

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

Выводы

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

Широкий диапазон просмотра сервисов позволил нам осмотреть разные виды хостингов: от профессиональных до, скажем, недоработанных. На примере последних видно, что «проколы» появляются в самых неожиданных местах, и только постоянная работа над ошибками, своими и чужими, позволит вывести хостинг в ряд профессиональных, а значит, обеспечит организатору в будущем какой-то приток посетителей и доход.

Список в последней трети вполне можно использовать как хрестоматию правил серии «Как не надо вести бизнес». Даже если какой-то сервис из приведённых — любительский, он не предупреждает об этом посетителей, а значит, они имеют шансы пострадать от его ошибок.

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

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

UPD: у кого есть идеи, как промониторить эту массу хостингов (30 штук), чтобы получить логи в течение месяцев 2-3 и сделать вывод о надёжности?

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

Оцените статью
Хостинги