Skip to content

Browser / React

In this chapter, you’ll integrate Oicana into a React application using WebAssembly. Unlike the server-side integrations, Oicana runs entirely in the browser here - no server required for PDF generation.

Let’s start with a fresh React project using Vite. Run the following command:

Terminal window
npm create vite@latest my-pdf-app -- --template react-ts
cd my-pdf-app
npm install

You can test it by running npm run dev and navigating to http://localhost:5173 in your browser.

Install the Oicana browser packages:

Terminal window
npm install @oicana/browser @oicana/browser-wasm
  1. Create a public directory (if it doesn’t exist) and copy example-0.1.0.zip into it. This makes the template accessible to the browser.

  2. Replace the contents of src/App.tsx with the following:

    src/App.tsx
    import { useEffect, useState } from 'react';
    import { initialize, Template, CompilationMode, Pdf } from '@oicana/browser';
    import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
    function App() {
    const [template, setTemplate] = useState<Template | null>(null);
    useEffect(() => {
    async function setup() {
    await initialize(wasmUrl);
    const response = await fetch('/example-0.1.0.zip');
    const templateBytes = new Uint8Array(await response.arrayBuffer());
    setTemplate(new Template(templateBytes));
    }
    void setup();
    }, []);
    function generatePdf() {
    const pdf = template!.export(
    new Map(),
    new Map(),
    Pdf,
    CompilationMode.Development,
    );
    const blob = new Blob([pdf], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'example.pdf';
    a.click();
    URL.revokeObjectURL(url);
    }
    if (!template) {
    return <p>Template is being prepared...</p>;
    }
    return (
    <button onClick={generatePdf}>
    Generate PDF
    </button>
    );
    }
    export default App;

Start the dev server with npm run dev and click the “Generate PDF” button. The generated example.pdf file should contain your template with the development value.

PDF compilation is CPU-bound and runs on whichever thread calls it, so any non-trivial UI should run it from a Web Worker rather than the main thread. The Deploying the Browser WASM guide covers the production setup, and the open source React example project shows a complete shared worker setup.

Our code currently compiles with empty inputs and development mode. This means Oicana will use the development value for the input. Now we’ll provide an explicit input value and switch to production mode. Update the generatePdf function:

Part of src/App.tsx
function generatePdf() {
const jsonInputs = new Map<string, string>();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
const pdf = template!.export(
jsonInputs,
new Map(),
Pdf,
);
const blob = new Blob([pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.pdf';
a.click();
URL.revokeObjectURL(url);
}

With this change, you need to remove the now unused CompilationMode import, or the build will fail.

Your explicit input value takes precedence over the development value in either mode, so it is what changes the output here. Notice that we removed the explicit CompilationMode.Development parameter. The export() method defaults to CompilationMode.Production. Production mode is the recommended default - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values for inputs. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles none values for that input.

Clicking the button now will download a PDF with “Baby Yoda” instead of “Chuck Norris”. For a more complete example including Web Workers, take a look at the open source React example project on GitHub.

For inputs other than JSON, see Template inputs, which documents blob inputs with examples for every integration.

The setup promise can reject. If it does, nothing catches it, so the UI shows “Template is being prepared…” forever and the only trace is a console error. Catching the rejection and keeping the reason in state can make the failure visible:

Part of src/App.tsx
const [template, setTemplate] = useState<Template | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function setup() {
await initialize(wasmUrl);
const response = await fetch('/example-0.1.0.zip');
if (!response.ok) {
throw new Error(`Failed to fetch the template: ${response.status}`);
}
const templateBytes = new Uint8Array(await response.arrayBuffer());
setTemplate(new Template(templateBytes));
}
setup().catch((cause) => setError(String(cause)));
}, []);

Compiling can fail too, most often because an input is missing or fails schema validation. Wrap the export() call in a try/catch that writes to the same error state, and add a branch that renders it. Without that branch the error is stored but never shown:

Part of src/App.tsx
function generatePdf() {
const jsonInputs = new Map<string, string>();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
try {
const pdf = template!.export(
jsonInputs,
new Map(),
Pdf,
);
const blob = new Blob([pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.pdf';
a.click();
URL.revokeObjectURL(url);
} catch (cause) {
setError(String(cause));
}
}
if (error) {
return <p>Something went wrong: {error}</p>;
}
if (!template) {
return <p>Template is being prepared...</p>;
}
Complete code at the end of this chapter
src/App.tsx
import { useEffect, useState } from 'react';
import { initialize, Template, Pdf } from '@oicana/browser';
import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
function App() {
const [template, setTemplate] = useState<Template | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function setup() {
await initialize(wasmUrl);
const response = await fetch('/example-0.1.0.zip');
if (!response.ok) {
throw new Error(`Failed to fetch the template: ${response.status}`);
}
const templateBytes = new Uint8Array(await response.arrayBuffer());
setTemplate(new Template(templateBytes));
}
setup().catch((cause) => setError(String(cause)));
}, []);
function generatePdf() {
const jsonInputs = new Map<string, string>();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
try {
const pdf = template!.export(
jsonInputs,
new Map(),
Pdf,
);
const blob = new Blob([pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.pdf';
a.click();
URL.revokeObjectURL(url);
} catch (cause) {
setError(String(cause));
}
}
if (error) {
return <p>Something went wrong: {error}</p>;
}
if (!template) {
return <p>Template is being prepared...</p>;
}
return (
<button onClick={generatePdf}>
Generate PDF
</button>
);
}
export default App;