# React

> Add the Document Editor and Document Viewer to React apps: React versions, Vite imports, environment variables, a dev proxy, unmounting and hosting.

- **Product:** TX Text Control .NET Server
- **Section:** Documentation
- **Kind:** Topic
- **Path:** TX Text Control .NET Server > Integration Guide > React
- **4483 words**
- **Web URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/react/
- **LLMs URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/react/llms.txt
- **LLMs-Full URL:** https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/react/llms-full.txt

---

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](https://docs.textcontrol.com/tx-text-control-dotnet-server/getting-started/document-editor/react/llms-full.txt) and [Document Viewer: React](https://docs.textcontrol.com/tx-text-control-dotnet-server/getting-started/document-viewer/react/llms-full.txt) 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

```text
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](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/aspnet-core/#configure-program-cs-for-separate-frontends) and add the controller of [Add a Document Controller](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/aspnet-core/#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](https://docs.textcontrol.com/tx-text-control-dotnet-server/installation/npm/react/#using-both-packages-in-one-project) 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:

```jsx
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:

```jsx
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:

```text
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:

```text
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*:

```javascript
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/TXWebSocket` covers the script of the editor and its WebSocket connection; `ws: true` forwards the WebSocket connection as well.
- `/TextControl` covers the requests of the viewer, and `/Document` the document controller.
- `secure: false` accepts 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:

```jsx
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](https://docs.textcontrol.com/tx-text-control-dotnet-server/reference/javascript/txtextcontrol/addeventlistener-method/llms-full.txt) waits for the `textControlLoaded` event, after which the editor accepts API calls. The component throws an error if `webSocketURL` is missing.
- `fetch` requests the document from the document controller, and [loadDocument](https://docs.textcontrol.com/tx-text-control-dotnet-server/reference/javascript/txtextcontrol/loaddocument-method/llms-full.txt) replaces the content of the editor with the base64-encoded DOCX.
- [saveDocument](https://docs.textcontrol.com/tx-text-control-dotnet-server/reference/javascript/txtextcontrol/savedocument-method/llms-full.txt) passes the document to its callback in [SaveDocumentResult.data](https://docs.textcontrol.com/tx-text-control-dotnet-server/reference/javascript/savedocumentresult/data-property/llms-full.txt), and `fetch` posts it to the controller.
- The cleanup function of `useEffect` calls [TXTextControl.removeFromDom](https://docs.textcontrol.com/tx-text-control-dotnet-server/reference/javascript/txtextcontrol/removefromdom-method/llms-full.txt), which closes the WebSocket connection gracefully and removes the editor, its event handlers and its CSS references. The `DocumentEditor` component 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 when `StrictMode` mounts the component twice during development.

Save the document before the component unmounts; unsaved content is lost. [Loading and Saving Documents](https://docs.textcontrol.com/tx-text-control-dotnet-server/document-editor/load-save/llms-full.txt) 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:

```jsx
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 `window` object, and calls [TXDocumentViewer.loadDocument](https://docs.textcontrol.com/tx-text-control-dotnet-server/reference/javascript-document-viewer/documentviewercontroller/loaddocument-method/llms-full.txt) 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](https://docs.textcontrol.com/tx-text-control-dotnet-server/reference/javascript-document-viewer/documentviewercontroller/savedocument-method/llms-full.txt) returns a promise. The [DocumentSaveSettings](https://docs.textcontrol.com/tx-text-control-dotnet-server/reference/javascript-document-viewer/documentsavesettings/llms-full.txt) merge the form field values and embed the annotations, and [base64](https://docs.textcontrol.com/tx-text-control-dotnet-server/reference/javascript-document-viewer/documentsaveresult/base64-method/llms-full.txt) returns the saved PDF.
- `basePath` is 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:

```tsx
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](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/authentication/#frontends-on-another-origin) describes. Tokens in URLs can appear in the logs of web servers and proxies. [Access Tokens](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/access-tokens/llms-full.txt) and [WebSocket Security](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/websockets/llms-full.txt) describe the server side and the alternatives. When users sign in to the backend with a cookie instead, [Require Signed-In Users](https://docs.textcontrol.com/tx-text-control-dotnet-server/security/authentication/#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()` and `app.MapFallbackToFile("/index.html")`, next to `app.UseWebSockets()` and `app.UseTXWebSocketMiddleware()`. The application and the backend then share one origin. The blog article [Using the Document Editor in an ASP.NET Core React Application](https://www.textcontrol.com/blog/2024/02/27/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](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/aspnet-core/#choose-the-packages).

Behind a load balancer, keep every WebSocket connection of an editor on the same server, as [Scaling and Load Balancing](https://docs.textcontrol.com/tx-text-control-dotnet-server/document-editor/architecture/#scaling-and-load-balancing) explains. [Host the Connection](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/websockets/#host-the-connection) describes proxies and hosting platforms.

### Next Steps

- [WebSocket Architecture](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/websockets/llms-full.txt): the connection between the editor and the backend in detail.
- [ASP.NET Core](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/aspnet-core/llms-full.txt): the backend for the React applications.
- [JavaScript API](https://docs.textcontrol.com/tx-text-control-dotnet-server/document-editor/javascript-api/llms-full.txt): program the editor from React.
- [REST Services](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/rest-services/llms-full.txt): call document services from React.

---

## Nearby

- **Previous:** [Angular](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/angular/llms-full.txt)
- **Next:** [JavaScript](https://docs.textcontrol.com/tx-text-control-dotnet-server/integration/javascript/llms-full.txt)
