使用 React Hooks 构建聊天应用,一个实用的例子
Hooks 是 React 16.8 中的新增功能,它使我们能够使用状态和其他 React 功能,而无需编写类。
“我可以不用类来构建一个功能齐全的应用程序吗?”我听到你问了。是的,你可以!在本教程中,我将向你展示如何操作。
虽然有些教程会通过“虚构”的例子来单独关注钩子,但在本教程中,我想向您展示如何构建一个真实的应用程序。
最后,你会得到类似这样的结果:
随着您的学习,您将学习如何使用新引入的useState
和useEffect
钩子,这使我们能够更干净地管理状态和生命周期功能。
当然,如果您希望直接进入代码,您可以在GitHub上查看完整的存储库。
CometChat 概览
我们不会构建自己的聊天后端,而是利用CometChat 的沙盒帐户。
简而言之,CometChat 是一个 API,它使我们能够轻松构建实时聊天等通信功能。在我们的例子中,我们将利用 npm 模块进行连接并开始实时传输消息。
综上所述,在连接到 CometChat 之前,我们必须首先创建一个 CometChat 应用程序(请注册一个永久免费的 CometChat 帐户来开始创建该应用程序)。
现在,前往仪表盘并输入应用名称——我将其命名为“react-chat-hooks”。点击 + 即可创建你的应用:
创建完成后,进入新创建的应用程序并点击API 密钥。从这里复制自动生成的authOnly 密钥:
下一步我们将需要它。
设置 React
使用我们的 CometChat 应用程序,打开命令行并使用 和 初始化npx
React 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
我们需要这些依赖项来完成我们的应用程序。
当我们在这里时,我们还应该删除src目录中的所有文件:
rm src
有时这个样板很有用,但今天我希望我们从头开始。
因此,本着从头开始的精神,创建一个名为src/config.js的新文件并填写您的 CometChat 凭据:
// src/config.js
const config = {
appID: '{Your CometChat Pro App ID here}',
apiKey: '{Your CometChat Pro Api Key here}',
};
export default config;
通过这个文件,我们可以方便的全局访问我们的凭证。
接下来,编写一个新的src/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
。
为了容纳我们的组件,创建一个名为components的文件夹,并在其中放置组件本身:
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;
Chat.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
hook 呢?
如您所见,我们首先导入新引入的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
。
如果你够敏锐,可能已经注意到我们导入了一个名为 App.css 的 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;
}
创建登录组件
提醒一下,我们的登录组件将如下所示:
为了继续操作,将Login.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
。
在 hooks 出现之前,我们可能会写类似这样的代码:
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
的 的值会被更新——正如在 React 中更新状态时所预期的那样——应用会被重新渲染。这一次, 的值会是 true,因此我们之前检查过的函数会渲染组件。user
App
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
并更新Chat.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
组件,我们的 UI 或多或少已经完成,但我们仍然需要一种实时发送和接收消息的方式。
创建 selectFriend 函数
在上面的FriendsList
组件中,我们引用了一个名为的函数,selectFriend
当用户点击列表中的某个名称时会调用该函数,但我们尚未定义它。
我们可以在Chat
组件中(在 之前)编写此函数并将其作为 propreturn
传递下去:FriendList
const selectFriend = uid => {
setSelectedFriend(uid);
setChat([]);
setChatIsLoading(true);
};
当选择好友时,我们会更新状态:
selectedFriend
使用新朋友的 uid 进行更新。chat
再次设置为空,这样之前好友的消息就不会与新好友的消息混淆。chatIsLoading
设置为 true,这样微调器将取代空的聊天框
在 selectedFriend 状态更新时运行 useEffect
当选择新的转化时,我们需要初始化该转化。这意味着实时获取旧消息并订阅新消息。
为此,我们使用 use 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
创建 scrollToBottom 函数
我们的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 驱动的聊天应用。击掌👋🏻!
有了这些经验,我相信你会开始欣赏 Hooks 的“炒作”。
Hooks 使我们能够使用函数式组件,以更优雅的方式构建同样强大的 React 组件。总而言之,Hooks 使我们能够编写更易于理解和维护的 React 组件。
说实话,我们只是触及了皮毛。在官方文档的指导下,你甚至可以创建自己的钩子!
附言:如果你正在努力学习 React,你可能会发现 React Distilled 是一个很棒的辅助工具。点击此处查看!
最初发布于https://www.cometchat.com
文章来源:https://dev.to/codewithnathan/building-a-chat-app-with-react-hooks-a-pragmatic-example-m46