Let’s Build a WebSockets Project With Rust and Yew 0.19 🦀

Let me start by saying I’m a HUGE fan of Yew. Using the power and flexibility of Rust to build front-end components is something that I feel will only get bigger as the adaptation of WebAssembly grows. Recently, one of my side projects needed some WebSockets love. I figured this would be the perfect time to dive into Yew’s recently released 0.19 version, which introduced several significant changes to the framework (the removal of web-sys
and its supported services (i.e. ConsoleService
) and the introduction of Functional Components, to name a few).
As you might have guessed from the title already, we will build a chat app (such a cliche, I know, I’ll show myself out) using Yew, yew-router, yew-agent, and several other crates.

To give you a sense of what we are going to build, check out this lovely image:

🛑 Before moving forward make sure you have Rust and NodeJS installed.
🗿 The WebSocket Server
To use WebSockets on the client-side you need a server capable of working with WebSockets (a shocker, I know). To keep this post a (somewhat) reasonable length, and for the sake of focusing on a single subject, we won’t be discussing the WebSockets server too much. In a nutshell: other than handling the incoming and outgoing connections, the server saves the incoming connections in an array called users
. Every few seconds the server will compare the users
array to the server’s current active connection list to verify that all the users in the list are in fact still connected.
While you can go and build/use your own WebSockets server for our lovely chat app, the easier solution here will be to clone the NodeJS WebSocket server I used at https://github.com/jtordgeman/SimpleWebsocketServer. Once cloned, run npm i
and then npm start
and if all went well, you should now have a cute WebSockets server running locally on port 8080.
Time to Take Off 🚀
- Clone the starter project at https://github.com/jtordgeman/YewChat
- Install the toolchain dependencies
npm i
The starter project is nothing but an empty project with wasm-pack and webpack already set up, so we can focus our attention to Yew
🛤 ️Routing
Our app has three possible routes:
- login— a simple page with a textbox and a button for a user to enter their nickname
- chat — The main page — has a list of users, the chat display, and a textbox to write stuff to the chat
- 404 — If anyone tries to go to a page that doesn't exist — this catch-all page will show up in all its glory
If you are coming from React you probably know (and use) react-router-dom
. Well, good news! yew-router
is pretty much the exact same idea! yew-router
handles the display of different pages (components) depending on the URL.
So enough with the theory, let’s get busy:
- Create a new folder named components under the src folder. We will host all of our components here.
- Create three files under the newly created components folder: chat.rs, login.rs, mod.rs. We will leave chat and login empty for now, in mod.rs add the following:
3. At the top of lib.rs, add the following use statements:
Note that Login and Chat will show as unresolved. We will fix that when we start dealing with components later on.
4. Thanks to yew-router
we can use an enum to define our routes. We will add it to our lib.rs as follows:
Pretty neat stuff! We annotate each entry with the URL it handles. Note that the 404 route uses an additional annotation: #[not_found]
— this macro comes from the yew-router
package and is basically what makes this route a catch-all route.
5. Next we need a way to translate the enum value of a route to an actual component. Add the following switch
function just below the Route enum:
6. To render our router (and in turn, the components it routes to) we will use a functional component. But what is a functional component you may ask?
A functional component is a simplified version of the regular Yew component which can receive properties and determines the rendered content by returning HTML. In short — functional components are basically components that have been reduced to have only the view
method.
7. Last but not least, we need an entry point to our app — a function that the JavaScript can call in order to initiate our Yew app. Add the run_app
function to lib.rs as follows:
🔬So what do we have here?
- Line 1: Using the
wasm_bindgen
macro we expose therun_app
function to JavaScript. - Line 3: We initialize the
wasm_logger
crate. Usingwasm_logger
and thelog
crates we can write debug logs to the browser’s console. - Line 4: We are passing our Main component (defined in the previous step) to the
start_app
method ofyew
, to — as you might have guessed — start the app.
With the routing in place, let’s move on to creating our components.
🪅 ️Components — Phase 1
For simplicity sake, our app will consist of two components:
- Login — a simple functional component to get the username and connect to the WebSockets server.
- Chat — the main component of the app — shows the chat window, and a text area to write content to the chat.
When users browse to the Login component they will type a username and click connect — that username then needs to pass down to the Chat component and be used there to connect to the WebSocket server. To achieve that we will use a Context (think of it as a global state of the app) that will hold the username for any component to use. The way we define a Context is as follows:
- Define a
struct
for the context and atype
alias for it:
2. In the main
component, define a context using the use_state
hook:
3. Lastly, let’s use the context by wrapping the main component HTML with a ContextProvider
element which enables the child elements to actually use this context.
A ContextProvider
element requires a context struct (User
in our case) and a context object (ctx
), that is set using the element’s context
property.
👁️ Note: the child elements of a ContextProvider will re-render whenever the context changes.
With the context in place, let’s build the Login component!
- In components/login.rs add the login functional component:
2. Declare a use_state
hook that will manage the state of the username the user typed in. When declaring a use_state
hook we provide the default value (an empty String
in our case), similarly to React’s use_state
hook:
3. To get a reference to the context (which as you might remember will hold the global state for the username), use the use_context
hook:
4. The Login component UI will contain an input element and a button. Whenever the input changes we need to update the local username
variable. To connect between the UI element and username
, we use a callback
:
🔬So what do we have here?
- Line 7: We clone the username state handler so we can later update it using its
set
method. - Line 9: We create a Callback from the input element’s
onchange
event. - Line 10: We get the target element (the element that fired the event) using the
InputEvent
’starget_unchecked_into
method. - Line 11: We update the
username
state with the new value that theinput
element currently holds using the element’svalue
method.
🎉 If you have ever done any JavaScript work with HTML elements then all of this should look pretty similar.
5. The next callback we need to create is for the submit button. Once the username is typed, and the user clicks on the submit button — we need to save that username to the global context so that other components can use it if needed. Create the onclick
callback as follows:
6. Lastly, let’s add the actual UI for this component using the html!
macro:
👀 A few callouts:
- The
input
element registers to theoninput
callback. Every time the input changes — we calloninput
and update theusername
state. - We use the
Link
element from theyew_router
crate to route the browser to theChat
route using theto
property. - The
Link
element contains abutton
element, which registers to theonclick
callback which updates the global context with the value ofusername
. In addition to that, the button also sets thedisabled
property to prevent the user from clicking it, unlessusername
has a length of 2 characters or more.
And that’s a wrap for the Login component 🎊. We now have a component that uses a Context in order to share its data (username in our case) with other components.
Now we may be tempted to run ahead and create the chat component, but let’s stop for a minute and think about how the chat component even works…
👋 Hello WebSockets!
At the heart of our chat application are WebSockets. WebSockets enable us to asynchronously send and receive messages between the server and the client without the need for constant polling from the client. This feature makes Websockets the perfect centerpiece for our chat app. Let’s go ahead and create a WebSocket service that will handle all the aspects of working with WebSockets in our app!
- Create a new folder called services under the src folder.
- Add a new file called mod.rs inside services with the following content:
3. Our Websocket service will handle the following:
- Listen to incoming messages from the WebSocket server.
- Write messages to the WebSocket server.
- Communicate with other components using MPSC (Multi Producers Single Consumer) Channel (more on this soon 🤔)
Add a new file called websocket.rs inside the services folder with the following content:
😱 OMG there’s a lot going on here, let’s bite-size this whole thing:
- Lines 6–8: Creates the
WebsocketService
struct that holds a single property of typeSender
.Sender
allows us to asynchronously send messages on the channel that the receiver will later receive, in the order they are sent.Sender
is cloneable, which means that every component that uses the service can clone it and use it to send messages back to the receiver, which as the name suggests — there can only be a single one. - Lines 11–46: The
new
function initialize the service, similar to a constructor in other languages. The function does the following:
- Connects to the WebSocket server (line 12)
- Create the MPSC channel (line 16)
- Spawns a new future (async task) on the current thread that will listen to the receiving end of the MPSC channel and write the received message to the WebSocket server. This is how components communicate with our service — by sending messages across the channel (lines 18–23).
- Spawns another new future on the current thread to listen to the incoming messages from the WebSocket server and log them out(lines 25–43).
- Finally, the function returns an instance ofWebSocketService
(Self
) that holds the channel transmitter (in_tx
).
Here’s a handy diagram visualizing how everything connects:

Awesome, with the WebSocket service behind us, let’s proceed with creating the actual chat component!
🪅 ️Components — Phase 2
For our Chat component, we will use a regular (read not functional
) component because we will make good use of its different lifecycle methods.
- Open up chat.rs and add the following:
👀 A few callouts:
- The
Msg
enum holds the possible messages (read actions) our component can receive. Chat has two basic functionalities: either it handles a message received from the WebSocket server (HandleMsg
), or it submits a message to the server (SubmitMessage
) — for example when a user types something into the chat. MessageData
represents a chat message, containing who is it from and what the actual message is.- The
MsgTypes
enum holds the different types of messages the component can send or receive. - Lastly,
WebSocketMessage
represents what a message to the WebSocket server looks like: it has a type (message_type
) and then either an array of strings (such as in the case of the chat users list) or a single string (such as the case of sending a chat message to the server).
2. Next, let’s add the Chat component itself:
The Chat components hold the user list (users
), the input typed to the text box (chat_input
), a reference to the Websocket service (wss
), and finally, an array of all the messages typed to the chat (messages
).
3. The first component lifecycle method we add is the create
method. create
initializes the component state and it’s ComponentLink
:
👀 Quite a few things are happening here so let’s break them down:
- We get the
user
object (oftype Rc<UserInner>
) we saved in theContext
(lines 4–7) and then clone itsusername
field (line 9), which is the actual user name the user used to log in. - We create a new WebSocket message to register the current client with the WebSocket server (lines 11–15). Next, we send it to the WebSocket server using the
WebSocketService
’s channel transmitter (tx
) (lines 17–23). If everything goes well we log a message to the console (line 22). - We finish the method off by sending a fresh instance of
Chat
(lines 25–30).
4. Next, we add the view method, which is responsible for rendering the component:
That's one long method 😅 Most of the stuff here is just HTML and styling so we won't dive into that, but here are a few interesting pointers:
- On line 4 we create a
callback
of typeMouseEvent
to use when the submit button is clicked. Thecallback
will send a message of typeSubmitMessage
whenever the button is clicked. This message will then get handled by theupdate
lifecycle method. - On line 10 we iterate over the
users
field by usingmap
andcollect
to render a list of currently connected users (each user is rendered in its owndiv
element). - On line 33 we iterate over the
messages
field, similar to how we did it previously forusers
. - We use the
ref
property on theinput
element (line 57) so we can later reference it (i.e. to read its value) outside of theview
lifecycle method. - We set the Submit button’s
onclick
property (line 58) to submit the callback defined on line 4.
5. We are FINALLY ready to run the project for the first time and see what we achieved so far! Inside the project root folder run npm start
. This will kick the process of compiling our app into WASM, optimizing it, building the HTML (with Webpack), and finally — running a dev server.
If all goes well you should be greeted with a login page where you can enter a username. Once done the main chat screen appears, let’s inspect the dev tools console:

Three lines pop out almost immediately:
- We successfully sent a WebSocket message from our chat component to the WebSocket server (first debug line)
- The WebSocket service sent the username to the WebSocket server
- The WebSocket server returned a message with a type of
users
containing an array with the names of the connected users.
Now that begs the question: if our WebSocket service received a message back with all the connected users — why don't we see that on our UI? Did our chat component even get that message at all?
Eagled-eyed readers might notice that when we previously discussed our WebSocket service. We only discussed how components send messages to the service, but nowhere did we mention how the service sends back messages to the components. Let’s take another dive into the rabbit hole that is our WebSocket service…
🔌 WebSockets — Phase 2
We are currently using an MPSC channel to communicate between components and the WebSocket service, but as the name implies — we cannot use the same approach for updating the components since we would need the opposite direction, i.e. multi consumers (components) single producer (service), which doesn't exist.
We use Yew Agents! Agents can be used to “route messages between components independently of where they sit in the component hierarchy” (taken from Yew’s official docs). That means we can use an agent’s dispatcher to send a message from the WebSocket service down to any listening component. Let’s create an event bus service which we will use to send these messages with.
Create a new file called event_bus.rs under services and paste the following content to it:
🔬So what do we have here?
- Lines 10–13: we create the
EventBus
struct which holds a link and a list of subscribers. - Lines 15–47: we implement the
Agent
trait for ourEventBus.
There are a bunch of methods we need to implement (connected
,disconnected
etc), but the one we care about the most ishandle_input
, which iterates over all of the subscribers and usinglink
, sends them the content of the message.
Now let’s go back to our WebSocket service and use the new event bus:
- Open websocket.rs under the services folder. Add the required
use
statements and theevent_bus
initializing. We are creating adispatcher
because we need a one-way communication channel between the service and the components:
2. To send a message on the channel, all we have to do is use send
:
You can see we are making use of the EventBusMsg
struct we defined in event_bus.rs to send a String
message down the channel (lines 9 and 15).
Lastly, we need to add the event bus to the Chat component.
- Open chat.rs under the components folder, and add
_producer
to the Chat struct:
2. Head over to the create
lifecycle method of Chat
and add the default value for _producer
to the returning Self
statement:
If you run the app again now you’ll notice nothing really changes. The reason for that is we never implemented the update
lifecycle method, which is in charge of handling the different messages our component get (like HandleMsg
or SubmitMessage
). The method returns a boolean indicating whether the component should be re-rendered or not.
3. Under the create
method, add the following implementation of update
:
🔬So what do we have here?
The method matches msg
to one of the two possible messages the component can get: HandleMsg
or SubmitMessage
. If the message is of type HandleMsg
we perform another match and check whether it is a message of type Users
or of type Message
. Messages of type Users
get sent every time the connected user list changes (a user connect/disconnect) and upon receiving this message — we populate the users
array with the name and avatar of each user that is connected. Messages of type Message
get sent when a user posts a message to the chat, and pretty similar to how we handled users
, we append the new message to the messages
array. In both of these cases, we return true
since we need the component to rerender itself with the new data.
In the case the message is of type SubmitMessage
we grab the message text from the input
HTML element (remember that ref
property we set earlier?) and using the WebSocket service channel transmitter (tx) we send the message to the WebSocket server. In the case of SubmitMessage
we return false
as we don't need the component to rerender itself.
And with this change, our app is now ready for prime time!

This post was definitely on the longer side, but I hope you got to experience how it is like working with Yew when building a web app. You can find the full source code at https://github.com/jtordgeman/YewChat. Feel free to hit me up on Twitter or by leaving a comment here — I promise I'll answer nicely ;p
Last but not least, huge thanks to Sara Lumelsky for proofreading and fixing my stupid spelling mistakes! :)