Frontend Development 14 min read

Implementing Front‑End File Preview for Various Formats Using React

This article explains how to preview a wide range of file types—including images, audio, video, markdown, plain text, Office documents, PDFs and external websites—in a React front‑end by using native HTML tags, embed/iframe techniques, and libraries such as marked and highlight.js, while also supporting page or time positioning.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing Front‑End File Preview for Various Formats Using React

Introduction

A recent requirement asked the front‑end to upload files with specific extensions and provide in‑page preview functionality. The article walks through the solution, covering preview strategies for different file types and how to locate specific pages, timestamps, or text positions.

Supported File Types

The preview must handle the following formats: png、jpg、jpeg、docx、xlsx、ppt、pdf、md、txt、audio、video . For some formats (e.g., pdf , txt , markdown ) additional positioning features are required.

Implementation Overview

Self‑contained media files: png、jpg、jpeg、audio、video

Plain‑text files: markdown & txt

Office files: docx、xlsx、ppt

Embedded PDFs: pdf

External websites via iframe

Self‑Contained Media Files

Images ( png、jpg、jpeg )

Use the native <img> tag:

<img src={url} key={docId} alt={name} width="100%" />

Audio ( audio )

Render with the <audio> element and disable download:

<audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
  <track kind="captions" />
  <source src={url} type="audio/mpeg" />
</audio>

Video ( video )

Render with the <video> element and mute by default:

<video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
  <track kind="captions" />
  <source src={url} type="video/mp4" />
</video>

Full component for audio/video positioning (time in seconds):

import React, { useRef, useEffect } from 'react';

interface IProps {
  type: 'audio' | 'video';
  url: string;
  timeInSeconds: number;
}

function AudioAndVideo(props: IProps) {
  const { type, url, timeInSeconds } = props;
  const videoRef = useRef<HTMLVideoElement>(null);
  const audioRef = useRef<HTMLAudioElement>(null);

  useEffect(() => {
    const secondsTime = timeInSeconds / 1000;
    if (type === 'audio' && audioRef.current) {
      audioRef.current.currentTime = secondsTime;
    }
    if (type === 'video' && videoRef.current) {
      videoRef.current.currentTime = secondsTime;
    }
  }, [type, timeInSeconds]);

  return (
    <div>
      {type === 'audio' ? (
        <audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
          <track kind="captions" />
          <source src={url} type="audio/mpeg" />
        </audio>
      ) : (
        <video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
          <track kind="captions" />
          <source src={url} type="video/mp4" />
        </video>
      )}
    </div>
  );
}

export default AudioAndVideo;

Plain‑Text Files ( markdown & txt )

When only a URL is available, the file must be fetched first. For markdown, the component uses marked to convert to HTML and highlight.js for code highlighting. Links open in a new tab, and the first match of the search text is scrolled into view.

Markdown Viewer

Key helper to highlight the first occurrence:

const highlightAndMarkFirst = (text: string, highlightText: string) => {
  let firstMatchDone = false;
  const regex = new RegExp(`(${highlightText})`, 'gi');
  return text.replace(regex, (match) => {
    if (!firstMatchDone) {
      firstMatchDone = true;
      return `<span id='first-match' style="color: red;">${match}</span>`;
    }
    return `<span style="color: red;">${match}</span>`;
  });
};

Configuration of marked for code highlighting:

marked.use({
  renderer: {
    code(code, infostring) {
      const validLang = !!(infostring && hljs.getLanguage(infostring));
      const highlighted = validLang
        ? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
        : code;
      return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
    },
    link(href, title, text) {
      const isExternal = !href.startsWith('/') && !href.startsWith('#');
      if (isExternal) {
        return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
      }
      return `<a href="${href}" title="${title}">${text}</a>`;
    },
  },
});

Full React component:

import React, { useEffect, useState, useRef } from 'react';
import { marked } from 'marked';
import hljs from 'highlight.js';

const preStyle = {
  width: '100%',
  maxHeight: '64vh',
  minHeight: '64vh',
  overflow: 'auto',
};

function MarkdownViewer({ docUrl, searchText }: { docUrl: string; searchText: string }) {
  const [markdown, setMarkdown] = useState('');
  const markdownRef = useRef<HTMLDivElement | null>(null);

  const highlightAndMarkFirst = (text: string, highlightText: string) => {
    let firstMatchDone = false;
    const regex = new RegExp(`(${highlightText})`, 'gi');
    return text.replace(regex, (match) => {
      if (!firstMatchDone) {
        firstMatchDone = true;
        return `<span id='first-match' style="color: red;">${match}</span>`;
      }
      return `<span style="color: red;">${match}</span>`;
    });
  };

  useEffect(() => {
    fetch(docUrl)
      .then((res) => res.text())
      .then((text) => {
        const highlighted = searchText ? highlightAndMarkFirst(text, searchText) : text;
        setMarkdown(highlighted);
      })
      .catch((err) => console.error('Failed to load markdown:', err));
  }, [docUrl, searchText]);

  useEffect(() => {
    if (markdownRef.current) {
      marked.use({
        renderer: {
          code(code, infostring) {
            const validLang = !!(infostring && hljs.getLanguage(infostring));
            const highlighted = validLang
              ? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
              : code;
            return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
          },
          link(href, title, text) {
            const isExternal = !href.startsWith('/') && !href.startsWith('#');
            if (isExternal) {
              return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
            }
            return `<a href="${href}" title="${title}">${text}</a>`;
          },
        },
      });
      const html = marked.parse(markdown);
      markdownRef.current.innerHTML = html as string;
      const firstMatch = document.getElementById('first-match');
      if (firstMatch) {
        firstMatch.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }
  }, [markdown]);

  return (
    <div style={preStyle}>
      <div ref={markdownRef} />
    </div>
  );
}

export default MarkdownViewer;

Text File Viewer

Highlights search text and scrolls to the first matching paragraph:

function highlightText(text: string) {
  if (!searchText.trim()) return text;
  const regex = new RegExp(`(${searchText})`, 'gi');
  return text.replace(regex, `<span style="color: red">$1</span>`);
}
import React, { useEffect, useState, useRef } from 'react';
import { preStyle } from './config';

function TextFileViewer({ docurl, searchText }: { docurl: string; searchText: string }) {
  const [paragraphs, setParagraphs] = useState<string[]>([]);
  const targetRef = useRef<HTMLDivElement | null>(null);

  function highlightText(text: string) {
    if (!searchText.trim()) return text;
    const regex = new RegExp(`(${searchText})`, 'gi');
    return text.replace(regex, `<span style="color: red">$1</span>`);
  }

  useEffect(() => {
    fetch(docurl)
      .then((res) => res.text())
      .then((text) => {
        const highlighted = highlightText(text);
        const paras = highlighted.split('\n').map(p => p.trim()).filter(p => p);
        setParagraphs(paras);
      })
      .catch((err) => console.error('Failed to load text file:', err));
  }, [docurl, searchText]);

  useEffect(() => {
    const timer = setTimeout(() => {
      if (targetRef.current) {
        targetRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }, 100);
    return () => clearTimeout(timer);
  }, [paragraphs]);

  return (
    <div style={preStyle}>
      {paragraphs.map((para, idx) => {
        const isTarget = para.includes(`>${searchText}<`);
        return (
          <p key={idx} ref={isTarget && !targetRef.current ? targetRef : null}>
            <div dangerouslySetInnerHTML={{ __html: para }} />
          </p>
        );
      })}
    </div>
  );
}

export default TextFileViewer;

Office Files ( docx、xlsx、ppt )

Preview using Microsoft Office Online embed URL:

<iframe
    src={`https://view.officeapps.live.com/op/view.aspx?src=${url}`}
    width="100%"
    height="500px"
    frameBorder="0"
></iframe>

Embedded PDF ( pdf )

Use the <embed> tag; page number can be appended to the URL:

<embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />
const httpsUrl = sourcePage
  ? `${doc.url}#page=${sourcePage}`
  : doc.url;

External Websites ( iframe )

Render any external URL inside an <iframe> :

<iframe
    title="Website"
    width="100%"
    height="100%"
    src={doc.url}
    allow="microphone;camera;midi;encrypted-media;"
/>

Conclusion

The article covered preview implementations for all required file types, including positioning logic for PDFs, markdown, and plain text, and provided complete React code snippets for each scenario.

frontendJavaScriptreacthtmlMediaFile Preview
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.