You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中botChat数组更新时如何避免聊天记录重复触发动画?

问题:React聊天组件历史消息打字动画重复触发的修复方案

原组件代码

import React, { useState, useEffect, useMemo } from 'react';
import axios from 'axios';
import { ChatLog } from './ChatLog.jsx';
import { GoArrowUp } from "react-icons/go";

export function QueryBar() {
    const [searchInput, setSearchInput] = useState("");
    const [botChat, setBotChat] = useState([]);
    const [userChat, setUserChat] = useState([]);

    const handleKeyDown = (e) => {
        if (e.key === 'Enter') {
            e.preventDefault();
            handleSubmit();
        }
    }
    const handleSubmit = () => {
        const params = {
            prompt: searchInput
        }
        axios.get("http://localhost:5555/chat", { params })
            .then((response) => {
                setBotChat(botChat => [...botChat, response.data.result]);
                setSearchInput("");
            })
            .catch((error) => {
                alert('Something bad happened while getting API response');
                console.log(error);
            })
    }
    useEffect(() => {
        setSearchInput(searchInput);
    }, [searchInput]);

    const chat = useMemo(() => botChat.map(chat => {
        return (
            <>
                <React.Fragment>
                    <h4 className="pl-7 pt-2">Bot: </h4>
                    <div className="pl-24 before:absolute before:inset-0 before:animate-typewriter before:bg-white after:absolute after:inset-0 after:w-[0.125em] after:animate-caret after:bg-black">{chat}</div>
                </React.Fragment>
            </>
        )
    }), [botChat])
    return (
        <>
        <div className="h-screen">
            <div>{chat}</div>
            <div className="p-4">
                <div className="w-screen absolute bottom-0 flex justify-center mb-8">
                    <form className="w-1/2 rounded-lg outline">
                        <input className="w-full placeholder:text-gray-500 pl-[8px] outline-none" type="text" placeholder='Talk to Bot'
                        onChange={(e) => setSearchInput(e.target.value)} onKeyDown={handleKeyDown} value={searchInput}/>
                    </form>
                    <button className="mx-4 h-6 w-6 inline-block rounded-full align-center outline items-center" onClick={handleSubmit}>
                        <div className='flex items-center justify-center'>
                            <GoArrowUp/>
                        </div>
                    </button>
                </div>
            </div>
        </div>
        </>
    )
    }

export default QueryBar

问题分析

原组件中,每次botChat数组更新时,useMemo会重新生成整个聊天日志的JSX数组,导致所有历史消息的DOM元素被重新创建,进而重复触发打字动画。此外,代码中存在一个无效的useEffect,会导致searchInput无限循环更新。

修复方案

1. 拆分独立消息组件,隔离渲染逻辑

将单条机器人消息抽成独立组件,并用React.memo包裹,确保只有当组件的props变化时才会重新渲染。

新建BotMessage.jsx组件:

import React from 'react';

// React.memo 让组件仅在props变化时重渲染
const BotMessage = React.memo(({ content }) => {
  return (
    <div>
      <h4 className="pl-7 pt-2">Bot: </h4>
      <div className="pl-24 before:absolute before:inset-0 before:animate-typewriter before:bg-white after:absolute after:inset-0 after:w-[0.125em] after:animate-caret after:bg-black">
        {content}
      </div>
    </div>
  );
});

export default BotMessage;

2. 优化主组件代码

  • 删除无效的useEffect
  • 使用拆分后的BotMessage组件渲染消息,避免整组重渲染
  • 为列表元素添加唯一key,帮助React优化DOM更新

修改后的QueryBar.jsx:

import React, { useState } from 'react';
import axios from 'axios';
import { GoArrowUp } from "react-icons/go";
import BotMessage from './BotMessage';

export function QueryBar() {
    const [searchInput, setSearchInput] = useState("");
    const [botChat, setBotChat] = useState([]);
    const [userChat, setUserChat] = useState([]);

    const handleKeyDown = (e) => {
        if (e.key === 'Enter') {
            e.preventDefault();
            handleSubmit();
        }
    }

    const handleSubmit = () => {
        const params = {
            prompt: searchInput
        }
        axios.get("http://localhost:5555/chat", { params })
            .then((response) => {
                // 存储用户消息,完善聊天逻辑
                setUserChat(prev => [...prev, searchInput]);
                setBotChat(prev => [...prev, response.data.result]);
                setSearchInput("");
            })
            .catch((error) => {
                alert('获取API响应时出错');
                console.log(error);
            })
    }

    return (
        <div className="h-screen">
            <div>
                {/* 渲染用户消息 */}
                {userChat.map((msg, index) => (
                    <div key={index} className="pl-7 pt-2">
                        <h4>You: </h4>
                        <div className="pl-24">{msg}</div>
                    </div>
                ))}
                {/* 渲染机器人消息,仅新增消息会触发重渲染 */}
                {botChat.map((msg, index) => (
                    <BotMessage key={index} content={msg} />
                ))}
            </div>
            <div className="p-4">
                <div className="w-screen absolute bottom-0 flex justify-center mb-8">
                    <form className="w-1/2 rounded-lg outline">
                        <input 
                            className="w-full placeholder:text-gray-500 pl-[8px] outline-none" 
                            type="text" 
                            placeholder='和机器人聊天'
                            onChange={(e) => setSearchInput(e.target.value)} 
                            onKeyDown={handleKeyDown} 
                            value={searchInput}
                        />
                    </form>
                    <button 
                        className="mx-4 h-6 w-6 inline-block rounded-full align-center outline items-center" 
                        onClick={handleSubmit}
                    >
                        <div className='flex items-center justify-center'>
                            <GoArrowUp/>
                        </div>
                    </button>
                </div>
            </div>
        </div>
    )
}

export default QueryBar

核心修复点说明

  • React.memo的作用:BotMessage组件只有在content props变化时才会重新渲染,历史消息的content不会改变,因此不会重复触发打字动画。
  • 组件拆分:将单条消息的渲染逻辑独立后,父组件更新时仅会创建并渲染新增的BotMessage实例,历史消息的DOM不会被重新创建。
  • 移除无效useEffect:原代码中监听searchInput并调用setSearchInput的逻辑会导致无限循环,直接删除即可。

内容的提问来源于stack exchange,提问作者Kyle Lee

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 20:33:13