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#
- Prerequisites
- Project Setup
- Setting Up Redux for State Management
- Storing External HTML Files
- Loading External HTML with Redux Thunks
- Rendering HTML Safely in React
- Building Blog App Components
- Adding Routing with React Router
- Testing the Application
- Conclusion
- 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-reduxStep 2: Install Dependencies#
We’ll need:
react-reduxand@reduxjs/toolkit: For Redux state managementreact-router-dom: For navigation between blog postsaxios: For fetching HTML files (alternative to nativefetch)dompurify: To sanitize HTML and prevent XSS attacks
npm install @reduxjs/toolkit react-redux react-router-dom axios dompurifySetting 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:
fetchBlogPostfetches the HTML file for a givenpostIdusing Axios, sanitizes it withDOMPurify, and returns the sanitized content. - State Structure:
postsstores HTML content by post ID,loadingtracks fetch status,errorholds error messages, andcurrentPostIdtracks 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=1loads1.html). - Axios returns the HTML as a string, which is sanitized with
DOMPurifyto 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:#
useParamsfrom React Router gets theidfrom the URL (e.g.,/posts/1→id=1).useEffecttriggersfetchBlogPostwhen the component mounts oridchanges.dangerouslySetInnerHTMLrenders 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 startVisit 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
dangerouslySetInnerHTMLandDOMPurifyto 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.