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

React+TS中如何为dangerouslySetInnerHTML内容添加阅读更多按钮

React+TS实现HTML内容截断+阅读更多功能

不需要依赖无TS类型声明的第三方库,原生实现逻辑轻量、类型安全,还能避免直接截断字符串导致的HTML标签未闭合、页面样式错乱问题。

核心实现代码

import { useState, useMemo } from 'react';

interface TruncatedHtmlProps {
  htmlContent: string;
  limit?: number;
  readMoreText?: string;
}

const TruncatedHtml = ({
  htmlContent,
  limit = 200,
  readMoreText = 'Lire la suite +'
}: TruncatedHtmlProps) => {
  const [isExpanded, setIsExpanded] = useState(false);

  // 计算是否需要展示阅读更多按钮
  const showReadMore = useMemo(() => {
    if (typeof window === 'undefined') return false;
    const tempDom = document.createElement('div');
    tempDom.innerHTML = htmlContent;
    return (tempDom.textContent || '').length > limit;
  }, [htmlContent, limit]);

  // 处理截断后的内容,自动保留完整HTML标签
  const displayContent = useMemo(() => {
    if (isExpanded || !showReadMore) return htmlContent;
    const tempDom = document.createElement('div');
    tempDom.innerHTML = htmlContent;

    let currentTextLength = 0;
    // 递归遍历节点截取内容,保证标签完整
    const processNode = (node: Node): Node | null => {
      // 文本节点:按长度截取
      if (node.nodeType === Node.TEXT_NODE) {
        const nodeText = node.textContent || '';
        if (currentTextLength + nodeText.length <= limit) {
          currentTextLength += nodeText.length;
          return node.cloneNode(true);
        }
        const remainLength = limit - currentTextLength;
        currentTextLength = limit;
        return document.createTextNode(nodeText.slice(0, remainLength) + '...');
      }
      // 元素节点:递归处理子节点
      if (node.nodeType === Node.ELEMENT_NODE) {
        const clonedEl = (node as Element).cloneNode(false) as Element;
        node.childNodes.forEach(child => {
          if (currentTextLength >= limit) return;
          const processedChild = processNode(child);
          if (processedChild) clonedEl.appendChild(processedChild);
        });
        return clonedEl;
      }
      return null;
    };

    const truncatedContainer = document.createElement('div');
    tempDom.childNodes.forEach(node => {
      if (currentTextLength >= limit) return;
      const processed = processNode(node);
      if (processed) truncatedContainer.appendChild(processed);
    });

    return truncatedContainer.innerHTML;
  }, [htmlContent, isExpanded, limit, showReadMore]);

  return (
    <>
      <p
        dangerouslySetInnerHTML={{ __html: displayContent }}
        className="my-4"
      />
      {showReadMore && !isExpanded && (
        <button
          className="underline"
          onClick={() => setIsExpanded(true)}
        >
          {readMoreText}
        </button>
      )}
    </>
  );
};

export default TruncatedHtml;

使用方法

直接替换你原有代码即可,默认按200字符截断,按钮文案和你原来的保持一致:

<TruncatedHtml htmlContent={template.content} />

如果需要自定义截断长度、按钮文案,传入对应props即可:

<TruncatedHtml
  htmlContent={template.content}
  limit={150}
  readMoreText="阅读更多"
/>

实现说明

  • 无任何第三方依赖,不需要额外安装类型声明包,TS类型完整
  • 截断逻辑通过DOM节点遍历实现,不会出现标签截断导致的样式错乱问题
  • 字符计数按纯文本长度计算,不会把HTML标签字符计入长度,符合日常阅读计数逻辑
  • 已做SSR兼容判断,在Next.js等服务端渲染框架中也可正常使用

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 04:09:25