使用 React Hooks 构建聊天应用,一个实用的例子

2025-06-04

使用 React Hooks 构建聊天应用,一个实用的例子

Hooks 是 React 16.8 中的新增功能,它使我们能够使用状态和其他 React 功能,而无需编写类。

“我可以不用类来构建一个功能齐全的应用程序吗?”我听到你问了。是的,你可以!在本教程中,我将向你展示如何操作。

虽然有些教程会通过“虚构”的例子来单独关注钩子,但在本教程中,我想向您展示如何构建一个真实的应用程序。

最后,你会得到类似这样的结果:

react_hooks_img.gif

随着您的学习,您将学习如何使用新引入的useStateuseEffect钩子,这使我们能够更干净地管理状态和生命周期功能。

当然,如果您希望直接进入代码,您可以在GitHub上查看完整的存储库。

CometChat 概览

我们不会构建自己的聊天后端,而是利用CometChat 的沙盒帐户

简而言之,CometChat 是一个 API,它使我们能够轻松构建实时聊天等通信功能。在我们的例子中,我们将利用 npm 模块进行连接并开始实时传输消息。

综上所述,在连接到 CometChat 之前,我们必须首先创建一个 CometChat 应用程序(请注册一个永久免费的 CometChat 帐户来开始创建该应用程序)。

现在,前往仪表盘并输入应用名称——我将其命名为“react-chat-hooks”。点击 + 即可创建你的应用:

图像预览.png使用 CometChat 创建应用程序

创建完成后,进入新创建的应用程序并点击API 密钥。从这里复制自动生成的authOnly 密钥

图像预览 (1).png获取 CometChat API

下一步我们将需要它。

设置 React

使用我们的 CometChat 应用程序,打开命令行并使用 和 初始化npxReact create-react-app

npx create-react-app cometchat-react-hooks
Enter fullscreen mode Exit fullscreen mode

旋转完成后create-react-app,打开新创建的文件夹并安装以下模块:

cd cometchat-react-hooks
npm install @cometchat-pro/chat bootstrap react-md-spinner react-notifications
Enter fullscreen mode Exit fullscreen mode

我们需要这些依赖项来完成我们的应用程序。

当我们在这里时,我们还应该删除src目录中的所有文件:

rm src
Enter fullscreen mode Exit fullscreen mode

有时这个样板很有用,但今天我希望我们从头开始。

因此,本着从头开始的精神,创建一个名为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;
Enter fullscreen mode Exit fullscreen mode

通过这个文件,我们可以方便的全局访问我们的凭证。

接下来,编写一个新的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'));
Enter fullscreen mode Exit fullscreen mode

这是我们 React 应用的入口点。加载后,我们首先初始化 CometChat,然后再渲染我们的App组件(稍后我们将对其进行定义)。

设置我们的组件

我们的应用程序将有三个值得注意的组件,即App,,LoginChat

为了容纳我们的组件,创建一个名为components的文件夹,并在其中放置组件本身:

mkdir components && cd components
touch App.js Login.js Chat.js
Enter fullscreen mode Exit fullscreen mode

App.js:

import React from 'react';

const App = () => {
  return (
    <div> This is the App component</div>
  );
};
export default App;
Enter fullscreen mode Exit fullscreen mode

登录.js:

import React from 'react';

const Login = () => {
  return (
    <div> This is the Login component</div>
  );
};
export default App;
Enter fullscreen mode Exit fullscreen mode

Chat.js

import React from 'react';

const Chat = () => {
  return (
    <div> This is the Chat component</div>
  );
};
export default App;
Enter fullscreen mode Exit fullscreen mode

如果您愿意,您可以运行该应用程序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;
Enter fullscreen mode Exit fullscreen mode

我建议你先看一眼代码,看看你理解了多少。如果你熟悉 React,应该会觉得它看起来很熟悉,但是useStatehook 呢?

如您所见,我们首先导入新引入的useState钩子,它是一个函数:

import React, {useState} from 'react';
Enter fullscreen mode Exit fullscreen mode

useState可用于创建状态属性。

为了给你一个想法,在useState钩子之前,你可能已经写了类似的东西:

this.state = { user: null };

setState({ user: { name: "Joe" }})
Enter fullscreen mode Exit fullscreen mode

使用钩子后,(或多或少)等效的代码如下所示:

const [user, setUser] = useState(null);

setUser({ user: { name: "Joe" }})
Enter fullscreen mode Exit fullscreen mode

这里的一个重要区别是,使用this.state和时setState,您处理的是整个状态对象。使用useState钩子时,您处理的是单个状态属性。这通常会使代码更简洁。

useState接受一个参数,即初始状态,并立即返回两个值,即相同的初始状态(在本例中为user)和一个可用于更新状态的函数(在本例中为setUser)。在这里,我们传递初始状态,null但任何数据类型都可以。

如果这一切听起来足够简单,那也确实如此!

没有必要过度思考,useState因为它只是一个更新状态的不同界面——我相信你熟悉这个基本概念。

有了初始状态,我们就可以根据用户是否已登录(换句话说,是否已设置)renderApp有条件地渲染Chat或:Loginuser

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

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;
}
Enter fullscreen mode Exit fullscreen mode

创建登录组件

提醒一下,我们的登录组件将如下所示:

为了继续操作,将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;
Enter fullscreen mode Exit fullscreen mode

这里我们利用useState来创建两个状态属性:uidValueisSubmitting

在 hooks 出现之前,我们可能会写类似这样的代码:

this.setState({
  uidValue: '',
  isSubmitting: false
})
Enter fullscreen mode Exit fullscreen mode

但是,那样就需要一个类了。这里,我们使用了一个函数式组件——太棒了!

在同一个函数中(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);
    }
  );
};
Enter fullscreen mode Exit fullscreen mode

这里我们利用了setIsSubmitting返回的函数useState。设置后,表单将被禁用。

然后,我们调用CometChat.login该方法使用我们的密钥对用户进行身份验证。在生产应用中,CometChat 建议您执行自己的身份验证逻辑。

如果登录成功,我们就调用props.setUser

最终,组件props.setUser的 的值会被更新——正如在 React 中更新状态时所预期的那样——应用会被重新渲染。这一次, 的值会是 true,因此我们之前检查过的函数会渲染组件。userAppuserApp.renderAppChat

创建聊天组件

我们的Chat组件承担着很多职责。事实上,它是我们应用中最重要的组件!

Chat组件来看,用户需要:

  • 选择要聊天的好友
  • 查看他们最近的消息历史记录
  • 发送新消息
  • 实时接收回复

你可能已经想到了,这需要我们处理大量的状态。就我个人而言,我想不出还有什么比这更好的地方来练习我们新学到的useState钩子知识了!但正如我在介绍中提到的,useState这只是我们今天要讨论的一个钩子。在本节中,我们还将探索useEffect钩子本身。

我现在可以告诉你,useEffect它取代了你可能已经认识到的componentDidMountcomponentDidUpdate和生命周期函数。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;
Enter fullscreen mode Exit fullscreen mode

Chat组件安装完成后,我们必须首先获取可聊天的用户。为此,我们可以利用useEffect

在无状态组件中ChatuseEffect像这样调用:

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();
    };

}, []);
Enter fullscreen mode Exit fullscreen mode

如上所述,当使用空数组调用时,useEffect仅在组件初始安装时调用一次。

我还没提到的是,你可以返回一个函数,useEffect当组件卸载时,React 会自动调用这个函数。换句话说,这就是你的componentWillUnmount函数。

在我们的componentWillUnmount等价函数中,我们调用removeMessageListenerlogout

接下来我们来编写组件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>
);
Enter fullscreen mode Exit fullscreen mode

如果这看起来像是很多代码,那么确实如此!但​​我们在这里所做的只是渲染我们的好友列表(FriendsList)和聊天框(ChatBox),并使用 Bootstrap 设置样式。

我们实际上还没有定义我们的FriendsListChatBox组件,所以现在让我们来做。

在同一个文件中,创建名为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>
    );
  }
};
Enter fullscreen mode Exit fullscreen mode

有了FriendsListChatBox组件,我们的 UI 或多或少已经完成,但我们仍然需要一种实时发送和接收消息的方式。

创建 selectFriend 函数

在上面的FriendsList组件中,我们引用了一个名为的函数,selectFriend当用户点击列表中的某个名称时会调用该函数,但我们尚未定义它。

我们可以在Chat组件中(在 之前)编写此函数并将其作为 propreturn传递下去:FriendList

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

当选择好友时,我们会更新状态:

  • 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]);
Enter fullscreen mode Exit fullscreen mode

通过将[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('');
};
Enter fullscreen mode Exit fullscreen mode

这已经从您之前复制的 JSX 中引用了。

当新消息发送成功时,我们调用并用最新消息setChat更新状态的值。chat

创建 scrollToBottom 函数

我们的Chat组件看起来很漂亮,除了一件事:当中有一堆消息时Chatbox,用户必须手动滚动到底部才能看到最新消息。

为了自动将用户滚动到底部,我们可以定义一个漂亮的函数以编程方式滚动到消息的底部:

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

然后,当先前的消息设置为状态时运行此函数:

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

结论

如果你做到了这一步,那么你已经成功创建了一个由 CometChat 和 Hooks 驱动的聊天应用。击掌👋🏻!

有了这些经验,我相信你会开始欣赏 Hooks 的“炒作”。

Hooks 使我们能够使用函数式组件,以更优雅的方式构建同样强大的 React 组件。总而言之,Hooks 使我们能够编写更易于理解和维护的 React 组件。

说实话,我们只是触及了皮毛。在官方文档的指导下,你甚至可以创建自己的钩子!

附言:如果你正在努力学习 React,你可能会发现 React Distilled 是一个很棒的辅助工具。点击此处查看
cta-react-distilled-d1a3dc470cbfafb1c7d56c72f262649e.jpg


最初发布于https://www.cometchat.com

文章来源:https://dev.to/codewithnathan/building-a-chat-app-with-react-hooks-a-pragmatic-example-m46
PREV
如何制作交互式 ReactJS 表单
NEXT
6 个适合你的作品集的 HTML 和 CSS 项目目录