DEV Community

Ayomide Onigbinde
Ayomide Onigbinde

Posted on

Create a WebAssembly app with React and Go

Getting started with WASM, React and Go

WebAssembly is awesome --barely a news, but how can you use it with React and Go? Well, honestly there are materials out there that are good to help to figure this out. But personally, it wasnt a smooth experience. I wanted a straightforward, up-to-date article. For one, I was looking for a solution that used Create-React-App because who wants to mess with Webpack? 🙃🙃

The aim of this short and simple tutorial is to show you how I pieced everything together and have a React based Go program compiled to WebAssembly up and running. I'd assume the following:

  • You have Go installed (1.11 and above).
  • Npm/Yarn installed.

So lets get to it.

In this tutorial, we'll enter a value into the input box in react and then use WebAssembly to render this value. Simple and interesting, right?

Alt Text

Clone the repo here. The repo contains the ejected app. I am trying to make this article as brief as possible so I might (unintentionally) miss some things.

In the root, we have main.go that contains the following code:

package main

import (
    "fmt"
    "syscall/js"
)

var c chan bool

// init is called even before main is called. This ensures that as soon as our WebAssembly module is ready in the browser, it runs and prints "Hello, webAssembly!" to the console. It then proceeds to create a new channel. The aim of this channel is to keep our Go app running until we tell it to abort.
func init() {
    fmt.Println("Hello, WebAssembly!")
    c = make(chan bool)
}

func main() {
    // here, we are simply declaring the our function `sayHelloJS` as a global JS function. That means we can call it just like any other JS function.
    js.Global().Set("sayHelloJS", js.FuncOf(SayHello))
    println("Done.. done.. done...")

    // tells the channel we created in init() to "stop".
    <-c
}

// SayHello simply set the textContent of our element based on the value it receives (i.e the value from the input box)
// the element MUST exist else it'd throw an exception
func SayHello(jsV js.Value, inputs []js.Value) interface{} {
    message := inputs[0].String()
    h := js.Global().Get("document").Call("getElementById", "message")
    h.Set("textContent", message)
    return nil
}

Our function sayHello takes two arguments. We're more concerned with the second. What the second does essentially is that it takes an array of js.Value. This is because we can pass as many argument as we want from JavaScript. To get the values, we simply use the index. So, in our case, we want to get the value entered in the input box.

message := inputs[0].String()

Like I said earlier, we use the index of the array to get the value of whatever we want.

h := js.Global().Get("document").Call("getElementById", "message")
h.Set("textContent", message)

the above code is similar to:

let h = document.getElementById("message")
h.textContent = message

So what we're doing is that we're changing the text of our element with the id "message" with the input value.

Run the following to compile the main.go:

GOOS=js GOARCH=wasm go build -o ../client/public/main.wasm

Client side of things

In the App.js, we have this in the componentDidMount():

async componentDidMount() {
    let { instance, module } = await WebAssembly.instantiateStreaming(fetch("main.wasm"), window.go.importObject)
    await window.go.run(instance)
    this.setState({
      mod: module,
      inst: instance
    })
  }

We're instantiating our main.wasm and running the instance. That means we can now go ahead and call our WASM functions in our app. Also, setting the module and instance to state incase we need them later on. Also, you'd notice that we're doing window.go.run(instance). Where did it come from? Well, its already handled in the React app. You'd notice there is a wasmjs folder which contains an init_js file. This and the wasm_exec.js file needed to use our WASM file with JS have been created and bundled with our React App using webpack. So it binds the Go() instance to the global window. Therefore instead of declaring a new instance of Go(), it exists as a window object variable

 handleSubmit = async (e) => {
    e.preventDefault()
    window.sayHelloJS(this.state.message)
  }

and this is us calling our function sayHelloJS that we registered in our Go code earlier! Notice we're accessing as a window object property. This is because we're calling it from React. It's going to be undefined if we called it as sayHelloJS.

<span id="message">
    Ayomide Onigbinde wrote this!!😉...💕 from WebAssembly and Golang
</span>

We have this HTML element. Note that it has an id "message". This element was the one we manipulated in our Go code which we compiled to WebAssembly! So this element must exist else it'd throw an exception. This is what will change into whatever value we enter in the input box! And its WebAssembly (compiled from Go) doing this! 🎉🎉

I tried to explain as best as I could! Please if there's something not clear, comment and we'd figure it out together 😉😉.

Huge props to Aaron for his tutorial and big big thanks to Chris at LogRocket. It was a huuuggeee help! This Dev.to article also helped to get an understanding about React with WASM.

Discussion (2)

Collapse
oayomide profile image
Ayomide Onigbinde Author

Yeah I saw this and I have to say: I love it. It actually inspired me to start working on something similar! Thanks so much for making this :)