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

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

Что происходит в том случае, когда зависимости лгут 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 всегда нужно честно сообщать о зависимостях эффектов, и в том, чтобы указывать все эти зависимости. Если вы хотите получить поддержку линтера в выполнении этого правила — мы приготовили кое-что для вас и для вашей команды.

Faq про хостинг


— В чём прикол хоститься у российской компании за рубежом?

Рассмотрим размещение в Амстердаме или Лондоне. Из-за другой юрисдикции не будет вопросов от российского МВД, например, потому, что на практике будет очень сложно дотянуться и разобраться. С другой стороны, точно так же вас почти точно не будет трогать и местное МВД, поскольку при запросе данных через кросс-юрисдикцию нужно будет делать это через Интерпол, что довольно сложно. В итоге нужно реально сильно накосячить, чтобы за вами пришли. У нас запросов через Интерпол было только два за историю хостинга.

— Погодите, то есть это лучше, чем у английской компании в Англии?

С точки зрения приватности — да. Местные не могут получить доступа к вашим установочным данным без запроса в Интерпол, а в России никто на практике не интересуется другой юрисдикцией. Если при этом выбрать ещё не Англию, а Голландию, где законы разрешают почти всё с точки зрения информационной свободы, то получится флеш-рояль.

— Это защитит от блокировок РКН?

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

— Что означает 30 % отклонённых запросов МВД в transparency report?

Обычно то, что около трети запросов было написано неверно и на следующий день прислано ещё раз с правильным указанием юрлица и правильной шапкой. То есть это не количество неисполненных требований, а примерное распределение запросов по ведомствам, потому что опытные люди знают: в МВД ошибаются часто, а в погранслужбе ФСБ — почти никогда.

— Где про всё это можно почитать больше?

Вот пост про разные запросы органов, которые приходят к нам, и то, как мы их обрабатываем.

— Можно ли дидосить себя с вашего VDS-хостинга?

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

— А как вы поймёте, что это DDoS, если не должны лезть внутрь виртуальной машины?

Мы не видим ничего дальше гостевой ОС, кроме одного исключения. Ну и опытный пользователь может полностью зашифровать свою ВМ и не хранить ключи на ней, если не верит этому утверждению. Поэтому формулировки «не топите остальных клиентов хостинга в нашей коммуналке» в оферте достаточно широки и с явной перестраховкой. На практике защита от майнинга может заблокировать тяжёлые вычисления, защита от спама — рассылку по клиентской базе, защита от DDoS — какой-то тяжёлый парсинг. Если вы знаете, что такое может случиться либо если такое уже случилось, то поддержка реагирует в течение 15 минут. Обратите внимание, что иногда это требует объяснения цели деятельности или её технической реализации, что может не очень вязаться с желаемой приватностью. Но таковы особенности всех VDS-шар.

— А что это за исключение, когда можно лезть внутрь ВМ клиента?

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

— Могут ли быть другие причины залезть на ВМ клиента?

Могут: это аудит Microsoft, но снова через запрос доступа уже от нас у клиента. По лицензионному соглашению с MS она имеет право в любой момент иметь доступ ко всем машинам, где стоят её продукты. В случае юрисдикции США речь идёт про постоянный доступ в онлайн-режиме без запроса. В нашем случае это реализовано через систему аудитов по запросу. То есть кто-то стучит на вас, что у вас пиратский офис, MS отправляет запрос нам, мы отправляем запрос вам, вы имеете пару дней на удаление конфиденциальных данных с ВМ, затем передаёте доступы, затем там проводится аудит в поисках пиратского офиса и другого пиратского софта MS. С учётом, что мы живём в реальном мире, обычно MS застают там девственно чистую машину. Все всё понимают, но иногда попадаются пользователи, которые не знали, что нельзя использовать десктопные ключи на ВМ без специальной конвертации по SPLA. Альтернатива аудиту — отказ в обслуживании.

— А разве можно так зашифровать ВМ, чтобы, имея доступ к памяти виртуальной машины, нельзя было достать ключ шифрования?

Короткий ответ: практически можно. Более сложный ответ: как и везде в юриспруденции или ИБ, 100-процентной вероятности в принципе нет. Есть вопрос затраченных усилий и навыка против затраченных усилий на получение данных. На практике для доступа к оперативной памяти машины и прослушивания всего её трафика нужно установить специальное устройство в разрыв между сервером и свитчем стойки. Получение разрешения на такое устройство — очень долгая и неприятная для органов процедура. У нас такое было один раз, и там шла речь о возможной массовой дистрибуции наркотиков в особо крупных размерах. В меньших случаях органы предпочитают другие, скажем так, неайтишные способы. Пост с этой историей.

— Пытаются ли майнить Chia или другие крипты, завязанные не на CPU, а на диски, например? Это вы тоже детектируете и пресекаете автоматически?

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

— Если арендовать сервер с большим диском и сразу же начать заливать туда большой бэкап, это будет штатным использованием, или системы защиты на него стриггерятся и всё ограничат?

Стриггерятся не сразу. Скажем, первые 15 Гб пройдут нормально, далее порежется полоса, а если агрессивная w/r 90/10 продолжится, то тогда уже стриггерится. Если предупредить поддержку заранее или уже после применения защиты — можно будет продолжать. Напоминаю, что смысл всех этих действий не в том, чтобы не дать клиенту хостинга ресурсы, а в том, чтобы клиент хостинга не поаффектил остальных. Поскольку мы можем ориентироваться только на косвенные признаки, это достаточно трудный вопрос. Более подробно — в посте про разбор оферты.

— А это вообще нормально — в ЦОДах патч-корды ручками обжимать?

Да, мы обжимали первые патч-корды вручную ещё для своего первого ЦОДа, а потом привезли их во второй и третий. На фото с ЦОДа в бомбоубежище вы их и увидели. Те золотые времена давно прошли, и мы больше так не делаем. В других ЦОДах в России мы нередко видим обжатые вручную, но их обычно менее 10 %, то есть, скорее всего, примерно так же, как у нас. Работающий патч-корд может служить лет 15, что ему сделается.

Читайте также:  8.4.5. Добавление, включение и отключение репозитория Yum

— Можно ли подключаться к одобренному РКН VPN (который блокирует по списку), а через него подключиться на другой VPN (который даже не предоставляет услуг в России, и поэтому РКН про него даже не в курсе) и уже через тот, второй VPN, смотреть запрещённые сайты?

Логика в том, что одобренные РКН VPN должны поддерживать тот же чёрный список, что и провайдеры без VPN. Если с этого VPN подключаться в другой неизвестный РКН, то непонятна логика всего действия, если можно сразу подключиться к неизвестному VPN. Разве что только не спалить его адреса.

— Можно ли смотреть порно с конями с работы через VDS, чтобы админ не заметил?

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

Ещё про внутряки хостинга:

Как мы возим железо по России и Европе и насколько часто его бьют и ломают
Коснётся ли цензура нас
Какие бывают незаконные использования хостинга (с чем мы сталкивались)
Управление хостингом: вид из головы тактика
Странные управленческие решения внутри хостинга
Управлять VPS-бизнесом в 10 городах — это же просто
Наш первый ЦОД в советском бомбоубежище: особенности вычислений в бункере
Как мы переучивали поддержку разговаривать по-человечески, и что получилось
Делаем поддержку дешевле, стараясь не растерять качество

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

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

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

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, они никуда не деваются.

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

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

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

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

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

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

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

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

Читайте также:  Демо-версия HostCMS

Так как возможности 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]);

  // ...

}

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

Способы обеспечения «непробиваемости»

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

Использование особенностей законодательства

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

Криминальные активности и их степень приемлемости в разных странах. Y (yes) означает, что указанные действия, по отзывам пользователей «подпольных» хостинг-услуг, возможны с использованием серверов, расположенных в отмеченной стране, N (no)— для указанных действий серверы в этой стране решительно не рекомендуются, M (maybe) — использование серверов возможно, но с некоторыми ограничениями

Одна из популярных стран для размещения BPH-инфраструктуры — Украина. Причина этого в том, что государственное регулирование и правоприменение этой страны во многих случаях значительно менее жёсткие по сравнению с соседними странами. В то же время действия правоохранителей Украины менее предсказуемы: известны случаи, когда СБУ проводили самостоятельные расследования и арестовывала владельцев пуленепробиваемых хостингов.

Швейцария и Нидерланды также популярны у владельцев BPH как страны, подходящие для создания компаний-прокладок для проведения прокси-хостинга. Это обусловлено тем, что в соответствии с законодательством этих стран хостинг заблаговременно получает уведомление о жалобе, что даёт возможность переместить системы в безопасные локации.

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

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

В США считается допустимым размещение порнографического контента за исключением, разумеется, детского порно, однако рассылка спама, сетевое сканирование и брутфорс приводят к многочисленным жалобам. Помимо этого, правоохранители США нетерпимо относятся к нарушениям законодательства о копирайте (DMCA и другие подобные акты).

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

Сейшельские острова, Белиз, Доминикана и Панама крайне популярны как место для размещения BPH из-за хороших интернет-каналов и лояльных законов, которые допускают весьма неторопливое реагирование на любые жалобы. Для таких BPH-сервисов даже появился термин «офшорный хостинг».

Анонимность

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

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


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

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

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 ${0} times`;
    }
  );
  // ...

}

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

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

}

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

  useEffect(
    // Функция эффекта из третьего рендера
    () => {
      document.title = `You clicked ${2} 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 не может заглянуть в функцию, но он может сравнить зависимости.

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

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

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