Rundown of React Hooks и их применение в симуляции чата в реальном времени

Хуки в React вызвали изменение в том, как разработчики React структурируют свои проекты; катализатор принятия функций вместо классов. В этой статье будут рассмотрены 2 крючка, useState и useEffect, для моделирования среды чата в реальном времени с использованием socket.io и Express с целью демонстрации того, как работают эти хуки.

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

Не забегая вперед, давайте рассмотрим первую демонстрацию, где мы увидим, как useState и useEffect можно использовать с веб-сокетом для управления процессом монтирования, повторного рендеринга и размонтирования. Конечный результат следующий:

Вверху слева: Express Server. Внизу слева: консоль Javascript. Справа: приложение React

Обзор хуков

В приведенной выше демонстрации мы используем хуки следующим образом:

useState

useState используется для установки переменных состояния наших компонентов. Мы определяем эти хуки, как только открываем объявление нашей функции. В этой демонстрации не используются классы, и нам не нужно использовать setState() для обновления состояния:

function App() {
   const [messageCount, setMessageCount] = useState(0);
   const [theme, setTheme] = useState('dark');
   const [inRoom, setInRoom] = useState(false);
 
   ...

Давайте посмотрим, как определяется useState хук, а затем сравним его со стандартным синтаксисом React.

  • useState использует синтаксис деструктурирующего присваивания (подробнее об этом здесь). Этот синтаксис позволяет нам распаковывать значения из массива в отдельные переменные. Для каждого описанного выше хука мы определяем 2 переменные в его массиве, которые затем можем использовать в нашей функции App для ссылки и обновления значения состояния.
  • Первая переменная массива - это имя переменной, которая относится к нашему значению состояния.
  • Вторая переменная массива относится к функции, вызываемой для обновления значения нашего состояния.
  • Значения, переданные в useState(), являются значениями состояния по умолчанию. Состояние по умолчанию для messageCount - 0, а значение по умолчанию для theme - dark.

Рассмотрим messageCount. Вот как мы можем определить значение нашего состояния и функцию обновления в стандартном классе React:

class App extends React.component {
   state = {
      messageCount: 0
   };
   const messageCount = this.state.messageCount;
   setMessageCount = count => (
      setState({messageCount: count});
   )
   ...
}

По сути, это то же самое, что объявление хука useState:

function App() {
   const [messageCount, setMessageCount] = useState(0);
   ...
}

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

Это useState. Теперь давайте посмотрим на второй крючок, useEffect.

useEffect

Хук useEffect служит совсем другой цели, чем useState. Он имеет дело с побочными эффектами компонентов - например, материал, который обрабатывается в процессе монтажа и демонтажа компонента.

Другими словами, useEffect - альтернатива использованию методов жизненного цикла класса React; в частности, три из них: componentDidMount, componentDidUpdate и componentWillUnmount.

Как это выглядит с точки зрения синтаксиса?

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

Эти 3 характеристики useEffect можно заключить в следующий шаблон:

function App() {
   useEffect(() => {
      //stuff that happens upon initial render
      ///and subsequent re-renders
      //e.g. make a fetch request, open a socket connection
      return () => {
         //stuff that happens when the component unmounts
         //e.g. close socket connection
      }
   }, [messageCount]);
  
   ...
}

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

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

В любом случае, давайте посмотрим, как это делается в нашей следующей демонстрации.

Демо-версия App.js

Давайте посетим демонстрационное приложение React, чтобы укрепить наше понимание этих хуков. Демонстрация в основном состоит из стандартного create-react-app стандартного кода по умолчанию, с некоторыми добавленными хуками и кнопками, а также с нашим socket.io клиентом для обработки соединения через веб-сокет:

В довольно удобочитаемой форме компонент состоит из следующего:

Подключение через сокет, подключение к localhost: 3011. Номер вашего порта не имеет значения - просто убедитесь, что он соответствует вашему внутреннему интерфейсу socket.io. Мы обратимся к коду на стороне сервера позже в статье для полноты демонстрации.

useState крючки. Как обсуждалось выше, наши 3 перехватчика состояния определяются сразу после открытия функции, безусловно.

Первая useEffect ловушка
Наша первая useEffect отвечает за присоединение и выход из комнаты. На этапе монтирования клиенту внутреннего веб-сокета отправляется событие join room, предлагающее присоединиться к нашему test-room. Здесь мы также обрабатываем входящие сообщения от test-room, прослушивая receive message события, которые транслируются с нашего сервера веб-сокетов. Такое обновление события увеличивает наше messageCount значение состояния.

Блок возврата, вызываемый при отключении компонента, генерирует событие leave room, чтобы покинуть test-room.

Обратите внимание, что эти события происходят только тогда, когда значение нашего inRoom состояния равно true, что по умолчанию равно false при первоначальном рендеринге. Далее по компоненту у нас есть кнопка, которая переключает это значение состояния. Это позволяет нам входить в комнату и выходить из нее, а не автоматически присоединяться, как только страница загружается.

useEffect, как и все хуки, нужно определять безоговорочно, поэтому мы не можем сделать что-то вроде определения хука внутри if(inRoom), например. Вместо этого мы проверяем условия в хуке.

Вторая useEffect ловушка
Эта useEffect ловушка обновляет заголовок вкладки в браузере с нашим messageCount значением состояния.

Обратите внимание, что ловушка обрабатывается только при повторном рендеринге, если messageCount изменяется. Здесь мы использовали второй аргумент useEffect, который принимает массив значений состояния, которые должны измениться, чтобы этот эффект обрабатывался при повторном рендеринге.

handleSetTheme, handleInRoom и handleNewMessage

Следующие 3 функции обрабатывают наши нажатия кнопок:

  • handleSetTheme переключает нашу тему между светом и темнотой, что просто отражается как className при рендеринге.
  • handleInRoom переключает нашу переменную состояния inRoom между истиной и ложью.
  • handleNewMessage отправляет событие new message клиенту веб-сокета нашего сервера, который, в свою очередь, передает событие receive message всем клиентам в нашем test-room. Но разве мы еще не обработали событие receive message в нашей ловушке useEffect? Да, но события, отправленные отправителем, не отправляются обратно тому же отправителю. Поэтому вместо этого мы увеличиваем здесь нашу переменную состояния messageCount.

возвращение

Наш блок возврата отображает домашнюю страницу приложения create-response-app по умолчанию. Были добавлены несколько заголовков и кнопок, чтобы отразить состояние нашей комнаты чата, а также класс темы, который переключает цвет фона страницы между светлым и темным.

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

Экспресс-сервер socket.io

Чтобы завершить это объяснение, давайте кратко посетим сервер Express, на котором размещена серверная логика socket.io. В app.js наша конфигурация socket.io определяется под стандартным кодом Express:

Как видите, здесь мы определили наши события socket.io, соответствующие нашей конфигурации React.

Настройка проекта

Чтобы запустить проект самостоятельно, настройте приложение React и сервер Node с парой дополнительных установок. Давайте быстро пробежимся по ним.

Настройка React

Для стороны React сгенерируйте новое приложение с помощью create-response-app перед обновлением React до последней версии, чтобы обеспечить реализацию хуков.

Примечание. Если вы хотите использовать исключительно пряжу, сделайте это с помощью yarn add ‹package›.

create-react-app websockets
cd websockets && yarn
npm i react@latest
npm i react-dom@latest
npm i socket.io-client
yarn start

У меня появляется привычка переводить новые проекты React в параллельный режим, как только они запускаются. Для этого откройте index.js и измените свой ReactDOM.render() на:

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

Наконец, скопируйте код App.js из приведенного выше Gist. Не забудьте указать цвета своей темы в App.css:

.Theme-light {
  background-color: #aaa;
}
.Theme-dark {
  background-color: #282c34;
}

Экспресс установка

На стороне Node JS запустите новый сервер Express с установкой socket.io:

express backend_websockets
cd backend_websockets && yarn
npm i socket.io
node bin/www

Наконец, скопируйте код app.js сверху. (строчные буквы app.js для стороны узла! App.js в верхнем регистре для стороны React.)

Теперь, когда оба приложения работают на localhost, вы можете приступить к тестированию.

Изучение console.log

Посмотрите, что происходит, когда мы переключаем кнопки.

  • Канал на localhost: 3011 сохраняется при начальной загрузке страницы.
  • На данном этапе мы не находимся в test-room. Следовательно, если мы продолжим и нажмем «Переключить тему», ни одно из наших присоединений к комнате или выход из нее не произойдет при повторном рендеринге компонента.
  • Теперь нажмите "Войти в комнату". Наше состояние inRoom переключается, после чего обрабатывается первая ловушка useEffect(), и мы присоединяемся к нашему test-room, генерируя событие join room.
  • Теперь, когда мы переключаем значение состояния приложения theme, повторно запускается повторная визуализация компонента, но теперь мы покидаем и снова присоединяемся к test-room. Обратите внимание, как наш второй useEffect() игнорирует этот повторный рендеринг, поскольку состояние messageCount не изменилось.
  • Нажатие на «Создать новое сообщение» также имеет тот же эффект, вызывая повторный рендеринг, а также выход из комнаты и присоединение к ней снова.

Дополнительная проблема и решение

В качестве дополнительной задачи рассмотрите возможность сделать следующее для улучшения демонстрации:

Изолируйте переменные состояния theme и newMessage в отдельный компонент, который НЕ заставит нас уйти и повторно ввести test-room после обновления этих переменных состояния.

Решение:

Решение здесь состоит в том, чтобы разделить состояние <Room /> и состояние <Message /> на отдельные компоненты, чтобы присоединение и вход в нашу комнату происходило только тогда, когда мы переключаем переменную состояния inRoom (и если мы полностью размонтируем приложение).

Однако за это приходится платить, поскольку элемент управления темой теперь применяется только к нашему пользовательскому интерфейсу сообщений, а не ко всему приложению (хотя вы могли бы обойтись без этого с помощью некоторого умного CSS). Обновленное полное решение под названием App2.js выглядит следующим образом:

Я также изменил следующий CSS для улучшения представления в App.css:

body {
  background-color: #282c34;
  color: #fff;
  text-align: center;
  padding: 20px;
}
.App-header {
  min-height: 50vh;
}

Куда пойти отсюда

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

Чтобы узнать больше о встраивании Websocket в проекты React, я опубликовал другую статью, посвященную интеграции событий Websocket с React Context и Redux: