Using React with templ
templ is great for server-side rendering. Combined with HTMX, it's even more powerful, since HTMX can be used to replace elements within the page with updated HTML fetched from the server, providing many of the benefits of React with reduced overall complexity. See /server-side-rendering/htmx for an example.
However, React has a huge ecosystem of rich interactive components, so being able to tap into the ecosystem is very useful.
With templ, it's more likely that you will use React components as islands of interactivity rather than taking over all aspects of displaying your app, with templ taking over server-side rendering, but using React to provide specific features on the client side.
Using React components
First, lets start by rendering simple React components.
Create React components
To use React components in your templ app, create your React components using TSX (TypeScript) or JSX as usual.
export const Header = () => (<h1>React component Header</h1>);
export const Body = () => (<div>This is client-side content from React</div>);
Create a templ page
Next, use templ to create a page containing HTML elements with specific IDs.
This page defines elements with ids of react-header
and react-content
.
A <script>
element loads in a JavaScript bundle that we haven't created yet.
package main
templ page() {
<html>
<body>
<div id="react-header"></div>
<div id="react-content"></div>
<div>This is server-side content from templ.</div>
<!-- Load the React bundle created using esbuild -->
<script src="static/index.js"></script>
</body>
</html>
}
Remember to run templ generate
when you've finished writing your templ file.
Render React components into the IDs
Write TypeScript or JavaScript to render the React components into the HTML elements that are rendered by templ.
import { createRoot } from 'react-dom/client';
import { Header, Body } from './components';
// Render the React component into the templ page at the react-header.
const headerRoot = document.getElementById('react-header');
if (!headerRoot) {
throw new Error('Could not find element with id react-header');
}
const headerReactRoot = createRoot(headerRoot);
headerReactRoot.render(Header());
// Add the body React component.
const contentRoot = document.getElementById('react-content');
if (!contentRoot) {
throw new Error('Could not find element with id react-content');
}
const contentReactRoot = createRoot(contentRoot);
contentReactRoot.render(Body());
Create a client-side bundle
To turn the JSX, TSX, TypeScript and JavaScript code into a bundle that can run in the browser, use a bundling tool.
https://esbuild.github.io/ is commonly used for this task. It's fast, it's easy to use, and it's written in Go.
Executing esbuild
with the following arguments creates an index.js
file in the static directory.
esbuild --bundle index.ts --outdir=../static --minify
Serve the templ component and client side bundle
To serve the server-side rendered templ template, and the client-side JavaScript bundle created in the previous step, setup a Go web server.
package main
import (
"fmt"
"log"
"net/http"
"github.com/a-h/templ"
)
func main() {
mux := http.NewServeMux()
// Serve the templ page.
mux.Handle("/", templ.Handler(page()))
// Serve static content.
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
// Start the server.
fmt.Println("listening on localhost:8080")
if err := http.ListenAndServe("localhost:8080", mux); err != nil {
log.Printf("error listening: %v", err)
}
}
Results
Putting this together results in a web page that renders server-side HTML using templ. The server-side HTML includes a link to the static React bundle.
Passing server-side data to React components
Moving on from the previous example, it's possible to pass data to client-side React components.
Add a React component that accepts data arguments
First, add a new component.
export const Hello = (name: string) => (
<div>Hello {name} (Client-side React, rendering server-side data)</div>
);
Export a JavaScript function that renders the React component to a HTML element
// Update the import to add the new Hello React component.
import { Header, Body, Hello } from './components';
// Previous script contents...
export function renderHello(e: HTMLElement) {
const name = e.getAttribute('data-name') ?? "";
createRoot(e).render(Hello(name));
}
Update the templ component to use the new function
Now that we have a renderHello
function that will render the React component to the given element, we can update the templ components to use it.
In templ, we can add a Hello
component that does two things:
- Renders an element for the React component to be loaded into that sets the
data-name
attribute to the value of the server-sidename
field. - Writes out JS that calls the
renderHello
function to mount the React component into the element.
The template renders three copies of the Hello
React component, passing in a distinct name
parameter ("Alice", "Bob" and "Charlie").
package main
import "fmt"
templ Hello(name string) {
<div data-name={ name }>
<script type="text/javascript">
bundle.renderHello(document.currentScript.closest('div'));
</script>
</div>
}
templ page() {
<html>
<head>
<title>React integration</title>
</head>
<body>
<div id="react-header"></div>
<div id="react-content"></div>
<div>
This is server-side content from templ.
</div>
<!-- Load the React bundle that was created using esbuild -->
<!-- Since the bundle was coded to expect the react-header and react-content elements to exist already, in this case, the script has to be loaded after the elements are on the page -->
<script src="static/index.js"></script>
<!-- Now that the React bundle is loaded, we can use the functions that are in it -->
<!-- the renderName function in the bundle can be used, but we want to pass it some server-side data -->
for _, name := range []string{"Alice", "Bob", "Charlie"} {
@Hello(name)
}
</body>
</html>
}
Update the esbuild
command
The bundle
namespace in JavaScript is created by adding a --global-name
argument to esbuild
. The argument causes any exported functions in index.ts
to be added to that namespace.
esbuild --bundle index.ts --outdir=../static --minify --global-name=bundle
Results
The HTML that's rendered is:
<html>
<head>
<title>React integration</title>
</head>
<body>
<div id="react-header"></div>
<div id="react-content"></div>
<div>This is server-side content from templ.</div>
<script src="static/index.js"></script>
<div data-name="Alice">
<script type="text/javascript">
// Place the React component into the parent div.
bundle.renderHello(document.currentScript.closest('div'));
</script>
</div>
<div data-name="Bob">
<script type="text/javascript">
// Place the React component into the parent div.
bundle.renderHello(document.currentScript.closest('div'));
</script>
</div>
<div data-name="Charlie">
<script type="text/javascript">
// Place the React component into the parent div.
bundle.renderHello(document.currentScript.closest('div'));
</script>
</div>
</body>
</html>
And the browser shows the expected content after rendering the client side React components.
React component Header
This is client-side content from React
This is server-side content from templ.
Hello Alice (Client-side React, rendering server-side data)
Hello Bob (Client-side React, rendering server-side data)
Hello Charlie (Client-side React, rendering server-side data)
Example code
See https://github.com/a-h/templ/tree/main/examples/integration-react for a complete example.