React Express View is a template engine for Express that allows rendering React components as client-side views. It supports both JavaScript (.jsx) and TypeScript (.tsx).
For example projects, check the example directory:
npm install react-express-viewTo ensure compatibility and prevent "Invalid Hook Call" errors (due to multiple React instances), you should inject your application's React and ReactDOMServer instances into the engine.
const express = require("express");
const path = require("path");
const reactExpressView = require("react-express-view");
const app = express();
// Configure the engine and views folder
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jsx");
app.engine("jsx", reactExpressView.createEngine());
// Serve static files for client-side JavaScript
app.use(express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
res.render("Home", {
title: "Home Page",
message: "Welcome to the Home Page!",
});
});
app.listen(3000, () => {
console.log("Server is running at http://localhost:3000");
});import express from "express";
import path from "path";
import { createEngine } from "react-express-view";
const app = express();
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "tsx");
app.engine(
"tsx",
createEngine({
transformViews: true,
React: require("react"),
ReactDOMServer: require("react-dom/server"),
})
);
app.use(express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
res.render("Home", {
title: "TypeScript App",
message: "Welcome to the TypeScript Example!",
});
});
app.listen(3000, () => {
console.log("Server is running at http://localhost:3000");
});Inside the views folder, create a file Home.jsx (for JavaScript) or Home.tsx (for TypeScript).
const React = require("react");
const { useState } = require("react");
function Home(props) {
const [count, setCount] = useState(0);
return (
<div>
<h1>{props.title}</h1>
<p>{props.message}</p>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
module.exports = Home; // or export default Home;import React, { useState } from "react";
interface HomeProps {
title: string;
message: string;
}
function Home(props: HomeProps) {
const [count, setCount] = useState(0);
return (
<div>
<h1>{props.title}</h1>
<p>{props.message}</p>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Home;To make the React components interactive on the client side, you need a client entry point.
Inside the client folder, create index.jsx (for JavaScript) or index.tsx (for TypeScript).
import React from "react";
import { hydrateRoot } from "react-dom/client";
// Dynamic import to support CommonJS and ES Modules
// Automatically register all components in the views directory
const requireComponent = require.context(
"../views",
true,
/\.(js|jsx|ts|tsx)$/
);
const components = {};
requireComponent.keys().forEach((fileName) => {
// Extract component name from filename (e.g., ./Home.jsx -> Home)
const componentName = fileName
.split("/")
.pop()
.replace(/\.\w+$/, "");
// Handle default exports and named exports
components[componentName] =
requireComponent(fileName).default || requireComponent(fileName);
});
// Retrieve the component name sent from the server
const componentName = window.__INITIAL_COMPONENT__;
const Component = components[componentName];
if (!Component) {
throw new Error(`Component "${componentName}" not found.`);
}
// Target root element
const rootElement = document.getElementById("root");
// Perform rehydration
if (rootElement) {
hydrateRoot(rootElement, <Component {...window.__INITIAL_PROPS__} />);
}import React from "react";
import { hydrateRoot } from "react-dom/client";
declare global {
interface Window {
__INITIAL_COMPONENT__: string;
__INITIAL_PROPS__: any;
}
}
// Dynamic import to support CommonJS and ES Modules
// Automatically register all components in the views directory
const requireComponent = require.context(
"../views",
true,
/\.(js|jsx|ts|tsx)$/
);
const components: { [key: string]: any } = {};
requireComponent.keys().forEach((fileName: string) => {
// Extract component name from filename (e.g., ./Home.tsx -> Home)
const componentName = fileName
.split("/")
.pop()
?.replace(/\.\w+$/, "");
if (componentName) {
components[componentName] = requireComponent(fileName);
}
});
// Retrieve the component name sent from the server
const componentName = window.__INITIAL_COMPONENT__;
const Component =
components[componentName]?.default || components[componentName];
if (!Component) {
throw new Error(`Component "${componentName}" not found.`);
}
// Target root element
const rootElement = document.getElementById("root");
if (rootElement) {
// Perform rehydration
hydrateRoot(rootElement, <Component {...window.__INITIAL_PROPS__} />);
}You will need a bundler like Webpack to compile your client-side code.
Ensure you have the necessary presets:
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
]
}Note:
@babel/preset-typescriptis only required if you are using TypeScript.
createEngine(options) accepts the following options:
doctype: Defines the doctype for the output HTML (default:<!DOCTYPE html>).transformViews: Enables runtime transpilation of views using@babel/register(default:true).React: Pass your application's React instance (fixes Hook errors).ReactDOMServer: Pass your application's ReactDOMServer instance.babel: Custom Babel configuration object passed to@babel/register.
This project is licensed under the MIT License.