Run Go in the browser with react and Typescript using WebAssembly
TL;DR
$ npx create-react-app my-app --template typescript-golang
Why create-react-app?
create-react-app allows us to quickly generate the boilerplate required to bootstrap a react application, providing a level of abstraction above the nitty-gritty infrastructure required to create a modern react app (Webpack, Babel, ESlint, etc.)
Why include Go?
Go is a statically typed, compiled programming language designed at Google, it is syntactically similar to C, but with memory safety, garbage collection, structural typing, and CSP-style concurrency.
In my case, I needed to run Go for JSON schema validations, in other cases, you might want to perform a CPU-intensive task or use a CLI tool written in Go.
But WebAssembly is not supported in all browsers!
I thought so too. In reality, since November 2017, WebAssembly is supported in all major browsers. So as long as you don’t need to support Internet Explorer, there is nothing to worry about.
Let’s get down to business 😎
First, initialize a brand new create-react-app project, assuming you’re not an absolute savage, make sure to use the Typescript template 😇
$ npx create-react-app my-app --template typescript
Next, create a folder under /src/LoadWasm
$ cd my-app
$ mkdir ./src/LoadWasm`
create a file to extend the Window type declaration, we’ll use it soon.
/src/LoadWasm/wasmTypes.d.ts
declare global {
export interface Window {
Go: any;
myGolangFunction: (num1: number, num2: number) => number
}
}
export {};
Copy a file used to load the WebAssembly code into the browser, it adds the Go property on the global window object and will act as a bridge between Javascript and WebAssembly.
This file is similar to the one from the official Go repository, with a few minor tweaks, we’ll use it in the next section.
$ curl https://raw.githubusercontent.com/royhadad/cra-template-typescript-golang/main/template/src/LoadWasm/wasm_exec.js > ./src/LoadWasm/wasm_exec.js`
Next, we’ll create a wrapper component that will wrap our entire application and wait for WebAssembly to be loaded. This can be optimized for performance purposes, but it’s good enough for now, for simplicity’s sake.
/src/LoadWasm/index.tsx
import './wasm_exec.js';
import './wasmTypes.d.ts';
import React, { useEffect } from 'react';
async function loadWasm(): Promise<void> {
const goWasm = new window.Go();
const result = await WebAssembly.instantiateStreaming(fetch('main.wasm'), goWasm.importObject);
goWasm.run(result.instance);
}
export const LoadWasm: React.FC<React.PropsWithChildren<{}>> = (props) => {
const [isLoading, setIsLoading] = React.useState(true);
useEffect(() => {
loadWasm().then(() => {
setIsLoading(false);
});
}, []);
if (isLoading) {
return (
<div>
loading WebAssembly...
</div>
);
} else {
return <React.Fragment>{props.children}</React.Fragment>;
}
};
Finally, we’ll wrap our entire application with the LoadWasm component, This will make sure no other components will load before WebAssembly is loaded
/src/index.tsx
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<LoadWasm>
<App />
</LoadWasm>
</React.StrictMode>
);
But wait, where is the Go code?
Start by initializing a Go module
$ mkdir ./wasm
$ cd ./wasm
$ go mod init wasm
$ go mod tidy
$ touch ./main.go
$ cd ..
Now we’ll use the syscall/js package to access the javascript Global scope, and set a function on it.
Secondly, we’ll implement a little hack to keep the Go code from terminating: opening a channel and waiting for it to finish, without ever using it 😈
This will allow us to continuously communicate with the Go code without needing to re-instantiate it every time.
/wasm/main.go
package main
import (
"syscall/js"
)
func myGolangFunction() js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return args[0].Int() + args[1].Int()
})
}
func main() {
ch := make(chan struct{}, 0)
js.Global().Set("myGolangFunction", myGolangFunction())
<-ch
}
Note: you may need to configure your IDE to support WebAssembly, see VS Code guide, Intellij guide
Now we can add this button somewhere in the application, perhaps in App.tsx
<button onClick={() => { alert(window.myGolangFunction(2, 3)); }}>
Click here to invoke WebAssembly!
</button>
Putting it all together
Finally, we’ll alter the package.json scripts to support build and hot-reload for WebAssembly.
build:
"build": "npm run build:wasm && npm run build:ts",
"build:ts": "react-scripts build",
"build:wasm": "cd wasm && GOOS=js GOARCH=wasm go build -o ../public/main.wasm && cd .. && echo \"compiled wasm successfully!\""
hot reload
We’ll need a few dependencies
$ npm install watch concurrently --save-dev
And use them in the start script
"start": "concurrently \"npm run watch:ts\" \"npm run watch:wasm\"",
"watch:ts": "react-scripts start",
"watch:wasm": "watch \"npm run build:wasm\" ./wasm",
Finally, we’ll run npm start and access the app on localhost:3000
A full example can be found in this GitHub repository
Didn’t you say it’ll take 60 seconds? Liar!
Ok, maybe this does take a while, but fear no more! I have a cure for your laziness!
I have created a custom create-react-app template for typescript-golang, all you have to do is run the following command in your working directory
$ npx create-react-app my-app --template typescript-golang
And… boom! A working react app with Typescript & Go support, you can get right into coding 🥳
Top comments (0)