React
Before a React application gets the Document Editor and the Document Viewer, one decision comes first: the editor package requires React 19.2 and the viewer package React 18, so the two components go into separate projects that share one ASP.NET Core backend. From there, both components work like other React components with props and callbacks, and documents travel between them and the backend with fetch.
Document Editor: React and Document Viewer: React create a first project for each component. Once the projects exist, they need a working import in current Vite versions, backend addresses per environment, a development proxy, loading and saving, a clean removal of the editor, access tokens and a hosting setup.
How the Applications Connect to the Backend
React application with the editor (Vite, http://localhost:5173)
|-- DocumentEditor -------- WebSocket ----> /api/TXWebSocket
|-- fetch ----------------- HTTP ---------> /Document
React application with the viewer
|-- DocumentViewer -------- HTTP ---------> /TextControl (controller of the viewer)
|-- fetch ----------------- HTTP ---------> /Document
v
ASP.NET Core backend with TX Text Control .NET Server, for example https://localhost:7118
Set up the backend as described in Configure Program.cs for Separate Frontends and add the controller of Add a Document Controller. Place a template.docx in the Documents folder of the backend. During development, the Vite development server can forward all requests to the backend, so the applications call their own origin and the backend needs no CORS policy for them.
Plan for the React Versions
The editor package 34.0.0 declares React ^19.2.0 and the viewer package 34.0.0 React ^18.2.0. No React version satisfies both ranges, so npm refuses to install the second package into the same project. Put the editor and the viewer into separate projects, each with the React version its package declares, and connect both to the same backend. Installing both packages into one project while ignoring peer dependencies is possible, but untested; npm Packages: React describes both options.
Import the Components in Vite 8
Both packages are CommonJS modules. Since Vite 8, the default import of a CommonJS module returns the complete module object when the package.json of your project sets "type": "module", which the Vite templates do. The plain import import DocumentEditor from '@txtextcontrol/tx-react-document-editor' then fails with the error "Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object." Take the component from the default property instead:
import DocumentEditorPackage from '@txtextcontrol/tx-react-document-editor';
// Vite 8 returns the CommonJS module object for the default import
const DocumentEditor = DocumentEditorPackage.default ?? DocumentEditorPackage;
The same applies to the viewer package:
import DocumentViewerPackage from '@txtextcontrol/tx-react-document-viewer';
const DocumentViewer = DocumentViewerPackage.default ?? DocumentViewerPackage;
With ??, the code also works with earlier Vite versions, in which the default import returns the component directly. The section "Consistent CommonJS Interop" of the Vite migration guide explains the change.
Set the Backend Address per Environment
Vite exposes variables whose names start with VITE_ from .env files as import.meta.env. The development server loads .env.development, and vite build loads .env.production. During development, the applications use the address of the Vite development server, which forwards the requests. Create .env.development in the project folder:
VITE_BACKEND_URL=http://localhost:5173
VITE_WEBSOCKET_URL=ws://localhost:5173/api/TXWebSocket
For the production build, create .env.production with the public address of the backend:
VITE_BACKEND_URL=https://backend.example.com
VITE_WEBSOCKET_URL=wss://backend.example.com/api/TXWebSocket
The values become part of the application that the browser loads, so they must not contain secrets. Restart the development server after changing an .env file.
Proxy the Backend During Development
Forward the paths of the editor, the viewer and the document controller to the backend in vite.config.js:
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api/TXWebSocket': { target: 'https://localhost:7118', secure: false, ws: true },
'/TextControl': { target: 'https://localhost:7118', secure: false },
'/Document': { target: 'https://localhost:7118', secure: false }
}
}
})
The code works as follows:
/api/TXWebSocketcovers the script of the editor and its WebSocket connection;ws: trueforwards the WebSocket connection as well./TextControlcovers the requests of the viewer, and/Documentthe document controller.secure: falseaccepts the development certificate of the backend. Replace the port with the HTTPS port of your backend.
Vite doesn't check the origin of WebSocket requests before it forwards them. Without the proxy, set the backend address in .env.development directly and allow the origin of the development server, http://localhost:5173, in the CORS policy of the backend.
Load, Save and Remove the Editor
The onLoad function of the DocumentEditor component runs when the global TXTextControl object exists. The following src/Editor.jsx loads the template once the editor is ready, saves it back on request and removes the editor when React unmounts the component:
import { useEffect } from 'react';
import DocumentEditorPackage from '@txtextcontrol/tx-react-document-editor';
const DocumentEditor = DocumentEditorPackage.default ?? DocumentEditorPackage;
const backendUrl = import.meta.env.VITE_BACKEND_URL;
const webSocketUrl = import.meta.env.VITE_WEBSOCKET_URL;
function Editor() {
useEffect(() => {
// Close the connection and remove the editor when the component unmounts
return () => {
if (typeof TXTextControl !== "undefined") {
TXTextControl.removeFromDom();
}
};
}, []);
function handleLoad() {
// Wait until the editor is ready
TXTextControl.addEventListener("textControlLoaded", async function () {
const response = await fetch(backendUrl + "/Document/Load?name=template.docx");
const stored = await response.json();
TXTextControl.loadDocument(TXTextControl.StreamType.WordprocessingML, stored.data);
});
}
function save() {
TXTextControl.saveDocument(TXTextControl.StreamType.WordprocessingML, function (result) {
fetch(backendUrl + "/Document/Save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "template.docx", data: result.data })
});
});
}
return (
<>
<button onClick={save}>Save</button>
<DocumentEditor
style={{ width: "1000px", height: "600px" }}
webSocketURL={webSocketUrl}
onLoad={handleLoad}>
</DocumentEditor>
</>
);
}
export default Editor;
The code works as follows:
- addEventListener waits for the
textControlLoadedevent, after which the editor accepts API calls. The component throws an error ifwebSocketURLis missing. fetchrequests the document from the document controller, and loadDocument replaces the content of the editor with the base64-encoded DOCX.- saveDocument passes the document to its callback in SaveDocumentResult.data, and
fetchposts it to the controller. - The cleanup function of
useEffectcalls TXTextControl.removeFromDom, which closes the WebSocket connection gracefully and removes the editor, its event handlers and its CSS references. TheDocumentEditorcomponent doesn't do this by itself. Without the cleanup, showing the editor again after React unmounted it fails with the error "The editor is already initialized." With it, the editor connects again and loads the document; this also works whenStrictModemounts the component twice during development.
Save the document before the component unmounts; unsaved content is lost. Loading and Saving Documents describes the formats and callbacks of both methods.
Load and Save Documents in the Viewer
The TXDocumentViewer object doesn't exist when the page loads; the viewer fires documentViewerLoaded on window when it's ready. Register the handler with the Effect Hook, remove it in the cleanup function and request the document inside it. The following src/Viewer.jsx belongs to the project with the viewer package:
import { useEffect } from 'react';
import DocumentViewerPackage from '@txtextcontrol/tx-react-document-viewer';
const DocumentViewer = DocumentViewerPackage.default ?? DocumentViewerPackage;
const backendUrl = import.meta.env.VITE_BACKEND_URL;
function Viewer() {
useEffect(() => {
const handleLoaded = async (event) => {
const response = await fetch(backendUrl + "/Document/Load?name=template.docx");
const stored = await response.json();
event.srcElement['TXDocumentViewer'].loadDocument(stored.data, stored.name);
};
window.addEventListener('documentViewerLoaded', handleLoaded);
return () => window.removeEventListener('documentViewerLoaded', handleLoaded);
}, []);
const saveAsPdf = async () => {
const result = await window.TXDocumentViewer.saveDocument(
window.TXDocumentViewer.StreamType.AdobePDF,
{ mergeFormFields: true, embedAnnotations: true });
const data = await result.base64();
await fetch(backendUrl + "/Document/Save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "template.pdf", data: data })
});
};
return (
<>
<button onClick={saveAsPdf}>Save as PDF</button>
<DocumentViewer
width="1000px"
height="600px"
basePath={backendUrl}>
</DocumentViewer>
</>
);
}
export default Viewer;
The code works as follows:
- The handler takes the viewer object from the source of the event, the
windowobject, and calls TXDocumentViewer.loadDocument with the base64-encoded document and its name. The cleanup function removes the handler, so a component that React mounts again doesn't register it twice. - TXDocumentViewer.saveDocument returns a promise. The DocumentSaveSettings merge the form field values and embed the annotations, and base64 returns the saved PDF.
basePathis the address of the backend, during development the address of the development server. When the application calls the backend on another origin, the backend's CORS policy must allow it.
Declare the Global Objects in TypeScript
Neither package ships type definitions, and the TXTextControl and TXDocumentViewer objects are globals that the components load at run time. In .tsx files, declare them before you use them:
declare const TXTextControl: any;
declare var TXDocumentViewer: any;
Pass Access Tokens
When the backend protects its endpoints with access tokens, the application requests a token first. The React components pass no additional WebSocket sub-protocols, so a token for the editor travels in the query string of webSocketURL, for example wss://backend.example.com/api/TXWebSocket?access_token=.... The viewer component has no setting for a token: it splits a query string off basePath and sends it only with the request for the viewer script, so serve the application from the origin of the backend and protect the viewer with the sign-in cookie of the user, as Frontends on Another Origin describes. Tokens in URLs can appear in the logs of web servers and proxies. Access Tokens and WebSocket Security describe the server side and the alternatives. When users sign in to the backend with a cookie instead, Require Signed-In Users protects the endpoints without tokens in the URL.
Build and Host the Application
vite build compiles the application with the values of .env.production into the dist folder. If the application doesn't run at the root of its server, set the base option of Vite. Two setups are common:
- Separate hosting. The React applications and the backend run on different servers or origins. .env.production contains the public address of the backend, and the CORS policy of the backend allows the origins of the applications.
- One ASP.NET Core project serves both. The Visual Studio template React and ASP.NET Core creates a client project and a server project. The server serves the built React application with
app.UseDefaultFiles(),app.UseStaticFiles()andapp.MapFallbackToFile("/index.html"), next toapp.UseWebSockets()andapp.UseTXWebSocketMiddleware(). The application and the backend then share one origin. The blog article Using the Document Editor in an ASP.NET Core React Application walks through this setup; install the 34.x packages as described in ASP.NET Core.
Behind a load balancer, keep every WebSocket connection of an editor on the same server, as Scaling and Load Balancing explains. Host the Connection describes proxies and hosting platforms.
Next Steps
- WebSocket Architecture: the connection between the editor and the backend in detail.
- ASP.NET Core: the backend for the React applications.
- JavaScript API: program the editor from React.
- REST Services: call document services from React.