Создание приложения для чата с помощью React Hooks, практичный пример

Хуки — это новое дополнение в React 16.8, которое позволяет нам использовать состояние и другие функции React без написания класса.

«Я могу создать полнофункциональное приложение без классов?» Я слышу, как ты спрашиваешь. Да, ты можешь! И в этом уроке я покажу вам, как это сделать.

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

В итоге у вас будет что-то вроде этого:

react_hooks_img.gif

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

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

CometChat с первого взгляда

Вместо того, чтобы создавать собственный сервер чата, мы будем использовать Учетная запись песочницы CometChat.

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

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

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

image_preview.pngСоздание приложения с CometChat

После создания перейдите к своему только что созданному приложению и нажмите Ключи API. Отсюда скопируйте автоматически сгенерированный ключ authOnly :

image_preview (1).pngПолучите API-интерфейс CometChat

Это понадобится нам на следующем шаге.

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

Установив наше приложение CometChat, откройте командную строку и инициализируйте React с помощью npx а также create-react-app:

npx create-react-app cometchat-react-hooks

Один раз create-react-app завершит вращение, откройте только что созданную папку и установите следующие модули:

cd cometchat-react-hooks
npm install @cometchat-pro/chat bootstrap react-md-spinner react-notifications

Нам понадобятся эти зависимости для завершения нашего приложения.

Пока мы здесь, мы также должны удалить все файлы внутри источник каталог:

rm src

Иногда этот шаблон полезен, но сегодня я хочу, чтобы мы начали с нуля.

Итак, в духе начала с нуля создайте новый файл с именем источник/config.js файл и введите свои учетные данные CometChat:


    
const config = {
  appID: '{Your CometChat Pro App ID here}',
  apiKey: '{Your CometChat Pro Api Key here}',
};
    
export default config;

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

Далее напишите новый источник/index.js файл:

import React from 'react';
import ReactDOM from 'react-dom';
import {CometChat} from '@cometchat-pro/chat';
import App from './components/App';
import config from './config';
    
CometChat.init(config.appID);
    
ReactDOM.render(, document.getElementById('root'));

Это точка входа для нашего приложения React. При загрузке мы сначала инициализируем CometChat перед рендерингом нашего App компонент, который мы определим в данный момент.

Настраиваем наши компоненты

Наше приложение будет состоять из трех примечательных компонентов, а именно: App, Loginа также Chat.

Для размещения наших компонентов создайте изящную папку с именем составные части а внутри него сами компоненты:

mkdir components && cd components
touch App.js Login.js Chat.js

App.js:

import React from 'react';
    
const App = () => {
  return (
    <div> This is the App component</div>
  );
};
export default App;

Логин.js:

import React from 'react';
    
const Login = () => {
  return (
    <div> This is the Login component</div>
  );
};
export default App;

Чат.js

import React from 'react';
    
const Chat = () => {
  return (
    <div> This is the Chat component</div>
  );
};
export default App;

Если вы хотите, вы можете запустить приложение с помощью npm start и обратите внимание на текст «Это компонент приложениятекст.

Конечно, это всего лишь заполнитель. Строительство App компонент является предметом нашего следующего раздела.

Создание компонента приложения

Ладно, пора серьезно заняться крючками.

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

Для начала замените App.js на:

import React, {useState} from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import 'react-notifications/lib/notifications.css';
import './App.css';
import {NotificationContainer} from 'react-notifications';
import Login from './Login';
import Chat from './Chat';
    
const App = () => {
  const [user, setUser] = useState(null);
  const renderApp = () => {
    // Render Chat component when user state is not null
    if (user) {
      return <Chat user={user} />;
    } else {
      return <Login setUser={setUser} />;
    }
  };
  return (
    <div className="container">
          
      {renderApp()}
    </div>
  );
};
export default App;

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

Как видите, сначала мы импортируем только что представленный useState ловушка, которая является функцией:

import React, {useState} from 'react';

useState может использоваться для создания свойства состояния.

Чтобы дать вам представление, прежде чем useState ловушка, вы могли бы написать что-то вроде:

this.state = { user: null };
    
setState({ user: { name: "Joe" }})

С хуками (более или менее) эквивалентный код выглядит так:

const [user, setUser] = useState(null);
    
setUser({ user: { name: "Joe" }})

Важным отличием здесь является то, что при работе с this.state а также setState, вы работаете со всем состоянием объекта. С useState крючок, вы работаете с отдельным государственным имуществом. Это часто приводит к более чистому коду.

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

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

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

С нашим начальным состоянием, от renderApp мы можем условно отобразить Chat или же Login в зависимости от того, вошел ли пользователь в систему (другими словами, если user был установлен):

const renderApp = () => {
  // Render Chat component when user state is not null
  if (user) {
    return ;
  } else {
    return ;
  }
};

renderApp вызывается из render функция, в которой мы также отображаем наш NotifcationContainer.

Если вы сообразительны, то могли заметить, что мы импортировали файл CSS с именем App.css, но еще не создали его. Давайте сделаем это дальше.

Создайте новый файл с именем App.css:

.container {
  margin-top: 5%;
  margin-bottom: 5%;
}
    
.login-form {
  padding: 5%;
  box-shadow: 0 5px 8px 0 rgba(0, 0, 0, 0.2), 0 9px 26px 0 rgba(0, 0, 0, 0.19);
}
    
.login-form h3 {
  text-align: center;
  color: #333;
}
    
.login-container form {
  padding: 10%;
}
    
.message {
  overflow: hidden;
}
    
.balon1 {
  float: right;
  background: #35cce6;
  border-radius: 10px;
}
    
.balon2 {
  float: left;
  background: #f4f7f9;
  border-radius: 10px;
}

.container {
  margin-top: 5%;
  margin-bottom: 5%;
}
    
.login-form {
  padding: 5%;
  box-shadow: 0 5px 8px 0 rgba(0, 0, 0, 0.2), 0 9px 26px 0 rgba(0, 0, 0, 0.19);
}
    
.login-form h3 {
  text-align: center;
  color: #333;
}
    
.login-container form {
  padding: 10%;
}
    
.message {
  overflow: hidden;
}
    
.balon1 {
  float: right;
  background: #35cce6;
  border-radius: 10px;
}
    
.balon2 {
  float: left;
  background: #f4f7f9;
  border-radius: 10px;
}

Создание компонента входа

Напоминаем, что наш компонент входа в систему будет выглядеть так:

Чтобы продолжить, замените Логин.js с:

import React, {useState} from 'react';
import {NotificationManager} from 'react-notifications';
import {CometChat} from '@cometchat-pro/chat';
import config from '../config';
    
const Login = props => {
  const [uidValue, setUidValue] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
    
  return (
    <div className="row">
      <div className="col-md-6 login-form mx-auto">
        <h3>Login to Awesome Chat</h3>
        <form className="mt-5" onSubmit={handleSubmit}>
          <div className="form-group">
            <input
              type="text"
              name="username"
              className="form-control"
              placeholder="Your Username"
              value={uidValue}
              onChange={event => setUidValue(event.target.value)}
            />
          </div>
          <div className="form-group">
            <input
              type="submit"
              className="btn btn-primary btn-block"
              value={`${isSubmitting ? 'Loading...' : 'Login'}`}
              disabled={isSubmitting}
            />
          </div>
        </form>
      </div>
    </div>
  );
};
    
export default Login;

Здесь мы используем useState для создания двух свойств состояния: uidValue а также isSubmitting.

До хуков мы могли бы написать что-то вроде:

this.setState({
  uidValue: '',
  isSubmitting: false
})

Однако для этого потребовался бы класс. Здесь мы используем функциональную составляющую — аккуратно!

В той же функции (перед return заявление), создать handleSubmit функция, которая будет вызываться при отправке формы:

const handleSubmit = event => {
  event.preventDefault();
  setIsSubmitting(true);
  CometChat.login(uidValue, config.apiKey).then(
    User => {
      NotificationManager.success('You are now logged in', 'Login Success');
      console.log('Login Successful:', {User});
      props.setUser(User);
    },
    error => {
      NotificationManager.error('Please try again', 'Login Failed');
      console.log('Login failed with exception:', {error});
      setIsSubmitting(false);
    }
  );
};

Здесь мы используем setIsSubmitting функция, возвращаемая useState. После установки форма будет отключена.

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

Если вход успешен, мы вызываем props.setUser.

В конечном счете, props.setUser обновляет значение user в нашем App компонент и, как и следовало ожидать, когда вы обновляете состояние в React, приложение повторно отображается. В это время, user будет правдивым, а значит, App.renderApp функция, которую мы проверили ранее, отобразит Chat составная часть.

Создание компонента чата

Наш Chat Компонент несет большую ответственность. На самом деле, это самый важный компонент в нашем приложении!

От Chat компонент, пользователю необходимо:

  • Выберите друга, с которым можно пообщаться
  • Смотрите их недавнюю историю сообщений
  • Отправить новые сообщения
  • Получайте ответы в режиме реального времени

Как вы можете себе представить, это потребует от нас обработки большого количества состояний. Я, например, не могу придумать лучшего места, чтобы практиковать наши новые знания о useState крюк! Но, как упоминалось в моем введении, useState это всего лишь один крючок, который мы рассмотрим сегодня. В этом разделе мы также рассмотрим useEffect крюк.

Я могу сказать вам сейчас, useEffect заменяет componentDidMount, componentDidUpdateа также componentWillUnmount функции жизненного цикла, которые вы, вероятно, узнали.

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

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

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

Начнем с отображения необходимого состояния. Этот компонент будет иметь 6 свойств состояния:

  • friends сохранить список пользователей, доступных для чата
  • selectedFriend — чтобы сохранить выбранного друга для общения в чате
  • chat — для сохранения массива сообщений чата, отправляемых и получаемых между друзьями
  • chatIsLoading — чтобы указать, когда приложение загружает предыдущие чаты с сервера CometChat
  • friendIsLoading — чтобы указать, когда приложение получает всех друзей, доступных для чата
  • message — для нашего компонента, управляемого вводом сообщений

Возможно, лучший способ освоить useEffect это увидеть его в действии. Не забудьте импортировать useEffect и обновить Чат.js :

import React, {useState, useEffect} from 'react';
import MDSpinner from 'react-md-spinner';
import {CometChat} from '@cometchat-pro/chat';
    
const MESSAGE_LISTENER_KEY = 'listener-key';
const limit = 30;
    
const Chat = ({user}) => {
  const [friends, setFriends] = useState([]);
  const [selectedFriend, setSelectedFriend] = useState(null);
  const [chat, setChat] = useState([]);
  const [chatIsLoading, setChatIsLoading] = useState(false);
  const [friendisLoading, setFriendisLoading] = useState(true);
  const [message, setMessage] = useState('');  
};
    
export default Chat;

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

В рамках Chat компонент без гражданства, вызов useEffect как это:

useEffect(() => {
  // this useEffect will fetch all users available for chat
  // only run on mount
    
  let usersRequest = new CometChat.UsersRequestBuilder()
    .setLimit(limit)
    .build();
    usersRequest.fetchNext().then(
      userList => {
        console.log('User list received:', userList);
        setFriends(userList);
        setFriendisLoading(false);
      },
      error => {
        console.log('User list fetching failed with error:', error);
      }
    );
    
    return () => {
      CometChat.removeMessageListener(MESSAGE_LISTENER_KEY);
      CometChat.logout();
    };
    
}, []);

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

Я еще не упомянул, что вы можете вернуть функцию из useEffect для автоматического вызова React при размонтировании компонента. Другими словами, это ваш componentWillUnmount функция.

В нашем componentWillUnmount -эквивалентную функцию, мы называем removeMessageListener а также logout.

Далее напишем return заявление Chat составная часть:

return (
  <div className="container-fluid">
    <div className="row">
      <div className="col-md-2" />
      <div className="col-md-8 h-100pr border rounded">
        <div className="row">
          <div className="col-lg-4 col-xs-12 bg-light" style={{height: 658}}>
            <div className="row p-3">
              <h2>Friend List</h2>
            </div>
            <div
              className="row ml-0 mr-0 h-75 bg-white border rounded"
              style={{height: '100%', overflow: 'auto'}}>
              <FriendList
                friends={friends}
                friendisLoading={friendisLoading}
                selectedFriend={selectedFriend}
                selectFriend={selectFriend}
              />
            </div>
          </div>
          <div className="col-lg-8 col-xs-12 bg-light" style={{height: 658}}>
            <div className="row p-3 bg-white">
              <h2>Who you gonna chat with?</h2>
            </div>
            <div
              className="row pt-5 bg-white"
              style={{height: 530, overflow: 'auto'}}>
              <ChatBox
                chat={chat}
                chatIsLoading={chatIsLoading}
                user={user}
              />
            </div>
            <div className="row bg-light" style={{bottom: 0, width: '100%'}}>
              <form className="row m-0 p-0 w-100" onSubmit={handleSubmit}>
                <div className="col-9 m-0 p-1">
                  <input
                    id='text'
                    className="mw-100 border rounded form-control"
                    type="text"
                    onChange={event => {
                      setMessage(event.target.value);
                    }}
                    value={message}
                    placeholder="Type a message..."
                  />
                </div>
                <div className="col-3 m-0 p-1">
                  <button
                    className="btn btn-outline-secondary rounded border w-100"
                    title="Send"
                    style={{paddingRight: 16}}>
                    Send
                  </button>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
);

Если это выглядит как много кода, что ж, так оно и есть! Но все, что мы здесь делаем, это рендерим наш список друзей (FriendsList) и окно чата (ChatBox), оформленный с помощью Bootstrap.

Мы на самом деле не определили наш FriendsList или же ChatBox компоненты, так что давайте сделаем это сейчас.

В этом же файле создайте компоненты с именем ChatBox а также FriendsList:

const ChatBox = props => {
  const {chat, chatIsLoading, user} = props;
  if (chatIsLoading) {
    return (
      <div className="col-xl-12 my-auto text-center">
        <MDSpinner size="72" />
      </div>
    );
  } else {
    return (
      <div className="col-xl-12">
        {chat.map(chat => (
          <div key={chat.id} className="message">
            <div
              className={`${
                chat.receiver !== user.uid ? 'balon1' : 'balon2'
              } p-3 m-1`}>
              {chat.text}
            </div>
          </div>
        ))}
        <div id='ccChatBoxEnd' />
      </div>
    );
  }
};
    
const FriendList = props => {
  const {friends, friendisLoading, selectedFriend} = props;
  if (friendisLoading) {
    return (
      <div className="col-xl-12 my-auto text-center">
        <MDSpinner size="72" />
      </div>
    );
  } else {
    return (
      <ul className="list-group list-group-flush w-100">
        {friends.map(friend => (
          <li
            key={friend.uid}
            c;assName={`list-group-item ${
              friend.uid === selectedFriend ? 'active' : ''
            }`}
            onClick={() => props.selectFriend(friend.uid)}>
            {friend.name}
          </li>
        ))}
      </ul>
    );
  }
};

С нашим FriendsList а также ChatBox компоненты на месте, наш пользовательский интерфейс более или менее завершен, но нам все еще нужен способ отправки и получения сообщений в режиме реального времени.

Создание функции selectFriend

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

Мы можем написать эту функцию в Chat компонент (перед return) и передать его вниз FriendList в качестве реквизита:

const selectFriend = uid => {
  setSelectedFriend(uid);
  setChat([]);
  setChatIsLoading(true);
};

Когда друг выбран, мы обновляем наше состояние:

  • selectedFriend обновляется с помощью uid нового друга.
  • chat снова пуст, чтобы сообщения от предыдущего друга не смешивались с новым.
  • chatIsLoading установлено значение true, так что счетчик заменит пустое окно чата

Запуск useEffect для обновления состояния selectedFriend

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

Для этого используем использование useEffect. в Chat компонента (и, как обычно, перед return):

useEffect(() => {
  // will run when selectedFriend variable value is updated
  // fetch previous messages, remove listener if any
  // create new listener for incoming message
    
  if (selectedFriend) {
    let messagesRequest = new CometChat.MessagesRequestBuilder()
      .setUID(selectedFriend)
      .setLimit(limit)
      .build();
    
    messagesRequest.fetchPrevious().then(
      messages => {
        setChat(messages);
        setChatIsLoading(false);
        scrollToBottom();
      },
      error => {
        console.log('Message fetching failed with error:', error);
      }
    );
    
    CometChat.removeMessageListener(MESSAGE_LISTENER_KEY);
    
    CometChat.addMessageListener(
      MESSAGE_LISTENER_KEY,
      new CometChat.MessageListener({
        onTextMessageReceived: message => {
          console.log('Incoming Message Log', {message});
          if (selectedFriend === message.sender.uid) {
            setChat(prevState => [...prevState, message]);
          }
        },
      })
    );
  }
}, [selectedFriend]);

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

Поскольку у нас есть прослушиватель, который прослушивает входящие сообщения и обновляет состояние чата, когда новое сообщение поступает из текущего selectedFriendнам нужно добавить новый прослушиватель сообщений, который берет новое значение из selectedFriend в своем if утверждение. Мы также позвоним removeMessageListener чтобы удалить любой неиспользуемый прослушиватель и избежать утечек памяти.

Обработчик отправки нового сообщения

Чтобы отправлять новые сообщения, мы можем подключить нашу форму к CometChat.sendMessage функция. В Chatbox функцию, создайте функцию с именем handleSubmit:

const handleSubmit = event => {
  event.preventDefault();
  let textMessage = new CometChat.TextMessage(
    selectedFriend,
    message,
    CometChat.MESSAGE_TYPE.TEXT,
    CometChat.RECEIVER_TYPE.USER
  );
  CometChat.sendMessage(textMessage).then(
    message => {
      console.log('Message sent successfully:', message);
      setChat([...chat, message]);
    },
    error => {
      console.log('Message sending failed with error:', error);
    }
  );
  setMessage('');
};

На это уже есть ссылка из JSX, который вы скопировали ранее.

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

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

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

const scrollToBottom = () => {
  let node = document.getElementById('ccChatBoxEnd');
  node.scrollIntoView();
};

Затем запустите эту функцию, когда предыдущие сообщения будут установлены в состояние:

messagesRequest.fetchPrevious().then(
  messages => {
    setChat(messages);
    setChatIsLoading(false);
    scrollToBottom();
  },
  error => {
    console.log('Message fetching failed with error:', error);
  }
);

Вывод

Если вы дошли до этого момента, значит, вы успешно создали чат-приложение на базе CometChat и Hooks. Дай пять 👋🏻!

Имея за плечами этот опыт, я уверен, что вы начнете ценить «ажиотаж» вокруг хуков.

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

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

PS: Если вы изо всех сил пытаетесь изучить React, вам может пригодиться React Distilled. Проверьте это здесь!
cta-реагировать-дистиллированный-d1a3dc470cbfafb1c7d56c72f262649e.jpg


Первоначально опубликовано на

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *