Создание приложения для чата с помощью React Hooks, практичный пример
Хуки — это новое дополнение в React 16.8, которое позволяет нам использовать состояние и другие функции React без написания класса.
«Я могу создать полнофункциональное приложение без классов?» Я слышу, как ты спрашиваешь. Да, ты можешь! И в этом уроке я покажу вам, как это сделать.
В то время как некоторые руководства будут посвящены хукам отдельно с «придуманными» примерами, в этом руководстве я хочу показать вам, как создать реальное приложение.
В итоге у вас будет что-то вроде этого:
По мере прохождения вы узнаете, как использовать недавно представленный useState
а также useEffect
хуки, которые позволяют нам более четко управлять функциями состояния и жизненного цикла.
Конечно, если вы предпочитаете сразу переходить к коду, вы можете увидеть полный репозиторий на Гитхаб.
CometChat с первого взгляда
Вместо того, чтобы создавать собственный сервер чата, мы будем использовать Учетная запись песочницы CometChat.
В двух словах, CometChat — это API, который позволяет нам с легкостью создавать функции общения, такие как чат в реальном времени. В нашем случае мы будем использовать модуль npm для подключения и начала передачи сообщений в режиме реального времени.
С учетом всего сказанного, прежде чем подключаться к CometChat, мы должны сначала создать приложение CometChat (пожалуйста, зарегистрируйтесь для навсегда бесплатный аккаунт CometChat чтобы приступить к созданию приложения).
Теперь перейдите на панель инструментов и введите имя приложения — я назвал свое «реагировать на чат-хуки». Нажмите +, чтобы создать приложение:
Создание приложения с CometChat
После создания перейдите к своему только что созданному приложению и нажмите Ключи API. Отсюда скопируйте автоматически сгенерированный ключ authOnly :
Получите 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
— чтобы указать, когда приложение загружает предыдущие чаты с сервера CometChatfriendIsLoading
— чтобы указать, когда приложение получает всех друзей, доступных для чата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. Проверьте это здесь!