How to Load and Render External HTML Files in React: Blog App Tutorial with Redux

React has revolutionized front-end development with its component-based architecture and JSX, which allows developers to write HTML-like code directly within JavaScript. However, there are scenarios where you might need to render external HTML files in a React app—for example, dynamic blog posts, user-generated content, or legacy HTML templates.

In this tutorial, we’ll build a fully functional blog application that loads and renders external HTML files using React, Redux for state management, and best practices for security. You’ll learn how to fetch HTML files, manage loading/error states with Redux, sanitize HTML to prevent XSS attacks, and render content dynamically.

Table of Contents#

  1. Prerequisites
  2. Project Setup
  3. Setting Up Redux for State Management
  4. Storing External HTML Files
  5. Loading External HTML with Redux Thunks
  6. Rendering HTML Safely in React
  7. Building Blog App Components
  8. Adding Routing with React Router
  9. Testing the Application
  10. Conclusion
  11. References

Prerequisites#

Before starting, ensure you have:

  • Basic knowledge of React (components, hooks, props)
  • Familiarity with Redux (actions, reducers, store)
  • Node.js (v14+) and npm/yarn installed
  • Code editor (VS Code recommended)

Project Setup#

Let’s start by creating a new React project and installing dependencies.

Step 1: Create a React App#

npx create-react-app blog-app-with-redux
cd blog-app-with-redux

Step 2: Install Dependencies#

We’ll need:

  • react-redux and @reduxjs/toolkit: For Redux state management
  • react-router-dom: For navigation between blog posts
  • axios: For fetching HTML files (alternative to native fetch)
  • dompurify: To sanitize HTML and prevent XSS attacks
npm install @reduxjs/toolkit react-redux react-router-dom axios dompurify

Setting Up Redux for State Management#

Redux will help us manage the state of our blog app, including loading status, errors, and the fetched HTML content. We’ll use Redux Toolkit for simplified Redux setup.

Step 1: Create a Redux Slice#

Create a src/redux/blogSlice.js file to define our blog state, actions, and reducers:

// src/redux/blogSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import DOMPurify from 'dompurify';
 
// Async thunk to fetch external HTML file
export const fetchBlogPost = createAsyncThunk(
  'blog/fetchBlogPost',
  async (postId, { rejectWithValue }) => {
    try {
      // Fetch HTML file from public/posts directory
      const response = await axios.get(`/posts/${postId}.html`);
      // Sanitize HTML to prevent XSS attacks
      const sanitizedHTML = DOMPurify.sanitize(response.data);
      return { id: postId, content: sanitizedHTML };
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);
 
const initialState = {
  posts: {}, // Stores fetched posts by ID: { '1': { id, content }, ... }
  loading: false,
  error: null,
  currentPostId: null,
};
 
const blogSlice = createSlice({
  name: 'blog',
  initialState,
  reducers: {
    setCurrentPostId: (state, action) => {
      state.currentPostId = action.payload;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchBlogPost.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchBlogPost.fulfilled, (state, action) => {
        state.loading = false;
        // Store the sanitized HTML in the posts object
        state.posts[action.payload.id] = action.payload;
      })
      .addCase(fetchBlogPost.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload || 'Failed to load blog post';
      });
  },
});
 
export const { setCurrentPostId } = blogSlice.actions;
export default blogSlice.reducer;

Explanation:#

  • Async Thunk: fetchBlogPost fetches the HTML file for a given postId using Axios, sanitizes it with DOMPurify, and returns the sanitized content.
  • State Structure: posts stores HTML content by post ID, loading tracks fetch status, error holds error messages, and currentPostId tracks the active post.
  • Reducers: Handle pending/fulfilled/rejected states for the async thunk, and update the state accordingly.

Step 2: Configure the Redux Store#

Create src/redux/store.js to configure the Redux store:

// src/redux/store.js
import { configureStore } from '@reduxjs/toolkit';
import blogReducer from './blogSlice';
 
export const store = configureStore({
  reducer: {
    blog: blogReducer,
  },
});

Storing External HTML Files#

To make HTML files accessible, place them in the public/posts directory (create the posts folder inside public). For example:

public/
  posts/
    1.html  // "Getting Started with React"
    2.html  // "Redux Toolkit Basics"
    3.html  // "Secure HTML Rendering in React"

Example content for public/posts/1.html:

<h1>Getting Started with React</h1>
<p>React is a JavaScript library for building user interfaces...</p>
<ul>
  <li>Component-based architecture</li>
  <li>Virtual DOM</li>
  <li>JSX syntax</li>
</ul>

Loading External HTML with Redux#

The fetchBlogPost thunk (defined earlier) handles loading HTML files. Let’s verify how it works:

  • It fetches public/posts/${postId}.html (e.g., postId=1 loads 1.html).
  • Axios returns the HTML as a string, which is sanitized with DOMPurify to remove malicious scripts.

Rendering HTML Safely in React#

React doesn’t render raw HTML by default for security reasons. To render HTML, use dangerouslySetInnerHTML—but always sanitize the HTML first to prevent XSS attacks.

Why Sanitize?#

Malicious HTML could contain <script> tags or event handlers (e.g., onclick="stealData()"). DOMPurify removes these threats by sanitizing the HTML string.

Building Blog App Components#

Let’s build the core components for our blog app.

1. Blog List Component#

Create src/components/BlogList.js to display a list of blog posts:

// src/components/BlogList.js
import { Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { setCurrentPostId } from '../redux/blogSlice';
 
const BlogList = () => {
  const dispatch = useDispatch();
  const posts = [
    { id: 1, title: 'Getting Started with React' },
    { id: 2, title: 'Redux Toolkit Basics' },
    { id: 3, title: 'Secure HTML Rendering in React' },
  ];
 
  return (
    <div className="blog-list">
      <h2>Blog Posts</h2>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Link 
              to={`/posts/${post.id}`} 
              onClick={() => dispatch(setCurrentPostId(post.id))}
            >
              {post.title}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
};
 
export default BlogList;

2. Blog Post Component#

Create src/components/BlogPost.js to render the fetched HTML:

// src/components/BlogPost.js
import { useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { fetchBlogPost } from '../redux/blogSlice';
 
const BlogPost = () => {
  const { id } = useParams(); // Get post ID from URL
  const dispatch = useDispatch();
  const { posts, loading, error } = useSelector((state) => state.blog);
  const post = posts[id];
 
  // Fetch post when component mounts or ID changes
  useEffect(() => {
    if (id) dispatch(fetchBlogPost(id));
  }, [id, dispatch]);
 
  if (loading) return <div className="loading">Loading...</div>;
  if (error) return <div className="error">Error: {error}</div>;
  if (!post) return <div>Post not found</div>;
 
  return (
    <div className="blog-post">
      <Link to="/" className="back-link">← Back to Posts</Link>
      <div 
        className="post-content" 
        dangerouslySetInnerHTML={{ __html: post.content }} 
      />
    </div>
  );
};
 
export default BlogPost;

Explanation:#

  • useParams from React Router gets the id from the URL (e.g., /posts/1id=1).
  • useEffect triggers fetchBlogPost when the component mounts or id changes.
  • dangerouslySetInnerHTML renders the sanitized HTML.

3. Layout and Routing#

Update src/App.js to add routing and wrap components with Redux/React Router providers:

// src/App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store } from './redux/store';
import BlogList from './components/BlogList';
import BlogPost from './components/BlogPost';
import './App.css';
 
function App() {
  return (
    <Provider store={store}>
      <Router>
        <div className="app">
          <header>
            <h1>My Redux Blog</h1>
          </header>
          <main>
            <Routes>
              <Route path="/" element={<BlogList />} />
              <Route path="/posts/:id" element={<BlogPost />} />
            </Routes>
          </main>
        </div>
      </Router>
    </Provider>
  );
}
 
export default App;

4. Add Styling (Optional)#

Add basic styling in src/App.css:

/* src/App.css */
.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}
 
header {
  border-bottom: 2px solid #333;
  padding-bottom: 10px;
  margin-bottom: 20px;
}
 
.blog-list ul {
  list-style: none;
  padding: 0;
}
 
.blog-list li {
  margin: 10px 0;
}
 
.blog-list a {
  text-decoration: none;
  color: #007bff;
  font-size: 1.2rem;
}
 
.blog-list a:hover {
  text-decoration: underline;
}
 
.loading, .error {
  text-align: center;
  padding: 20px;
  font-size: 1.1rem;
}
 
.error {
  color: #dc3545;
}
 
.back-link {
  display: inline-block;
  margin-bottom: 20px;
  color: #007bff;
  text-decoration: none;
}
 
.post-content {
  line-height: 1.6;
}
 
.post-content h1 {
  color: #2d3436;
}
 
.post-content p {
  margin: 15px 0;
}

Testing the Application#

Start the development server:

npm start

Visit http://localhost:3000 to see the app:

  • Click a blog post link (e.g., "Getting Started with React") to load its HTML content.
  • Observe loading spinners during fetching and error messages if a post is missing.

Conclusion#

In this tutorial, you learned how to:

  • Load external HTML files in React using Redux Toolkit and Axios.
  • Safely render HTML with dangerouslySetInnerHTML and DOMPurify to prevent XSS.
  • Manage loading/error states with Redux async thunks.
  • Build a blog app with React Router for navigation.

Next Steps:

  • Add a search feature to filter blog posts.
  • Implement server-side rendering (SSR) for better SEO.
  • Add user authentication to restrict access to HTML files.

References#