Table of Contents
- Introduction
- Setting Up the Project
- Designing the Data Model (Rails)
- Building the API Endpoints (Rails)
- Setting Up WebSockets (Rails)
- Remix.js Frontend Setup (Remix.js)
- Implementing API Integration in Remix
- Real-time Updates with WebSockets in Remix
- Conclusion: Reflecting on Our Live Polling System
1. Introduction
WebSockets: The Real MVP of Real-Time Web Apps
Today, we’re exploring the world of WebSockets. If you’ve ever wondered how modern web apps achieve that smooth, real-time feel, you’re in for a treat. Let’s break it down!
What’s the Big Deal with WebSockets?
Imagine you’re at a party (a dev party, of course 🎉). In the old days of the web, you’d have to keep asking the host, “Any news?” every few seconds. That’s basically how traditional HTTP requests work - constant asking, or “polling.” Pretty inefficient, right?
Enter WebSockets: It’s like having a direct hotline to the party host. Once you’re connected, you both can shout updates to each other whenever something cool happens. No need to keep asking!
In tech speak, WebSockets enable full-duplex, bidirectional communication between a client (usually a web browser) and a server over a single TCP connection. Unlike the request-response model of HTTP, WebSockets keep the party line open for instant, two-way chatter.
The Cool Kids of WebSocket Town
Always On: Once you establish a WebSocket connection, it stays open. It’s like leaving your WhatsApp open - messages can come through anytime!
Speed Demon: With an open connection, there’s less overhead. This means lower latency compared to constantly knocking on the server’s door with HTTP requests.
Two-Way Street: Both the client and server can initiate a conversation. It’s a true dialogue, not just a Q&A session.
Real-Time Magic: Perfect for apps that need to stay fresh without hitting that refresh button.
Why Should You Care?
Instant Gratification: Great for live chats, gaming, or any app where real-time updates are crucial.
Server’s Best Friend: No more unnecessary polling requests clogging up your server.
UX on Steroids: Users get a snappier, more interactive experience. Happy users, happy life!
Bandwidth Diet: Uses less data compared to constantly asking, “Are we there yet?”
Scaling Up: Handles tons of connections more efficiently than traditional HTTP.
Plays Well with Others: Facilitates real-time chat between different domains. Useful for when your app needs to make friends across the internet.
Where You’ve Seen WebSockets in Action
- That addictive live chat on your favorite shopping site
- Google Docs’ collaborative magic
- Live sports scores that update faster than you can say “goal!”
- Multiplayer games that don’t lag (well, mostly)
- Your smart home devices staying in sync
- Those fancy real-time dashboards that make you look good in meetings
WebSockets in Our Live Polling App
For our live polling system, WebSockets are the secret sauce. They’ll zap vote counts and poll results to all connected clients faster than you can click “submit.” No more furiously refreshing the page or pestering the server with constant requests. It’s smooth, it’s real-time!
Stay tuned as we dive deeper into building this WebSocket-powered polling app with Remix.js and Rails.
2. Setting Up the Project
Rails API Backend Setup
Okey, let’s get started. Let’s start by creating a new Rails API project.
|
|
Here, we’re using the –api flag to set up a lightweight API-only application and –database=postgresql to set up a postgresql database.
Edit config/database.yml
to set up your PostgreSQL database credentials.
Let’s configure CORS. Add the following to config/initializers/cors.rb
:
|
|
Now, we need to configure Action Cable. Edit config/application.rb
to include the following:
|
|
Next, we’ll need to create a new database and migrate the database.
|
|
Next, let’s add some additional gems to our project:
|
|
Setting up a new Remix.js project
|
|
I’ll keep this simple and just add a few extra folders and files to the project.
To the apps folder, add a components
folder and a lib
folder. Under lib
, create a types.ts
and api.ts
- Installing necessary dependencies and creating project structure
3. Designing the Data Model (Rails)
Designing the Data Model for Our Live Polling System
In this section, we’ll set up the core models for our live polling application: Poll, Option, and Vote. These models will form the backbone of our data structure, allowing us to create polls, add options, and record votes efficiently.
Creating the Poll Model
Let’s start by generating our Poll model. This will be the primary model in our application:
|
|
This command creates a Poll model with a title, description, and end date. The title and description will provide information about the poll, while the end date allows us to set a time limit for voting.
Creating the Option Model
Next, we’ll create the Option model. Each poll needs a set of options for users to choose from:
|
|
This generates an Option model with content (the text of the option) and a reference to the Poll it belongs to.
Creating the Vote Model
Finally, we need a way to record votes:
|
|
This creates a Vote model that references an Option, allowing us to track which option was chosen for each vote.
Understanding Model Relationships
Now that we have our models, let’s explore how they relate to each other. Rails makes it easy to establish these relationships:
Poll and Option Relationship
A Poll can have multiple Options, creating a one-to-many relationship:
1 2 3 4 5 6 7 8 9 10
# app/models/poll.rb class Poll < ApplicationRecord has_many :options, dependent: :destroy accepts_nested_attributes_for :options, allow_destroy: true end # app/models/option.rb class Option < ApplicationRecord belongs_to :poll end
The
dependent: :destroy
ensures that when a poll is deleted, its associated options are also removed, maintaining data integrity.Option and Vote Relationship
Similarly, an Option can have multiple Votes:
1 2 3 4 5 6 7 8 9 10
# app/models/option.rb class Option < ApplicationRecord belongs_to :poll has_many :votes end # app/models/vote.rb class Vote < ApplicationRecord belongs_to :option end
Poll and Vote Relationship
While there’s no direct relationship between Polls and Votes, we can establish an indirect connection:
1 2 3 4 5 6
# app/models/poll.rb class Poll < ApplicationRecord has_many :options, dependent: :destroy accepts_nested_attributes_for :options, allow_destroy: true has_many :votes, through: :options end
This allows us to easily access all votes for a given poll, even though votes are directly associated with options.
The Big Picture
With this structure in place, we’ve created a flexible and powerful data model:
- Polls serve as the main container, holding information about the voting event.
- Options provide the choices within each poll.
- Votes record the selections made by users.
This design allows us to:
- Create new polls with multiple options
- Record votes for specific options
- Easily tally votes for individual options or entire polls
- Maintain a clear, organized data structure
Remember to run rails db:migrate
after creating these models to update your database schema. With our data model in place, we’re now ready to build the API endpoints that will bring our live polling system to life.
4. Building the API Endpoints (Rails)
Building API Endpoints for Our Live Polling System
In this section, we’ll create the necessary API endpoints for our live polling application. We’ll focus on polls and votes, implement CRUD operations, and ensure our API is accessible from our Remix.js frontend.
Creating API Controllers
First, let’s generate our API controllers. We’ll use Rails’ namespacing to organize our API under v1
:
|
|
These commands create controller files in app/controllers/api/v1/
.
Implementing CRUD Operations for Polls
Let’s implement CRUD (Create, Read, Update, Delete) operations for polls in app/controllers/api/v1/polls_controller.rb
:
|
|
Adding Endpoints for Casting Votes
Now, let’s implement the voting functionality in app/controllers/api/v1/votes_controller.rb
:
|
|
Setting Up Routes
Let’s define our API routes in config/routes.rb
:
|
|
This sets up nested routes for votes under polls, allowing for URLs like /api/v1/polls/1/votes
.
Setting up CORS for Frontend Access
To allow our Remix.js frontend to access these API endpoints, we need to configure CORS (Cross-Origin Resource Sharing). We’ve already added the rack-cors
gem to our Gemfile, so let’s configure it.
In config/initializers/cors.rb
, add:
|
|
Make sure to replace 'http://localhost:3000'
with the actual URL of your Remix.js application.
Testing Our API
With these endpoints in place, you can now:
- Create a new poll: POST to
/api/v1/polls
- List all polls: GET to
/api/v1/polls
- View a specific poll: GET to
/api/v1/polls/:id
- Update a poll: PUT to
/api/v1/polls/:id
- Delete a poll: DELETE to
/api/v1/polls/:id
- Cast a vote: POST to
/api/v1/polls/:poll_id/votes
We can use tools like cURL and Postman while we build out the frontend.
Remember to restart your Rails server after making these changes. With these API endpoints set up, we’ve laid the groundwork for our live polling system’s backend. Next, we’ll focus on integrating these endpoints with our Remix.js frontend and implementing real-time updates with Action Cable.
5. Setting Up WebSockets (Rails)
Setting Up WebSockets with Action Cable for Real-Time Updates
In this section, we’ll configure Action Cable to enable real-time updates in our live polling system. We’ll create a channel for live updates and explain how the publish-subscribe model works in our application context.
Configuring Action Cable in Rails
Action Cable seamlessly integrates WebSockets with the rest of your Rails application. Let’s start by configuring it:
First, ensure Redis is installed and running on your system, as Action Cable uses Redis as its default adapter.
In
config/application.rb
, add the following line to allow Action Cable to accept WebSocket requests:1
config.action_cable.mount_path = '/cable'
In
config/environments/development.rb
, add:1
config.action_cable.allowed_request_origins = ['http://localhost:3000']
Replace
http://localhost:3000
with your Remix.js app’s URL.Alternatively, you can disable the origin check in development by adding:
1
config.action_cable.disable_request_forgery_protection = true
The second option is useful during development since we can use a simple chrome extension to test the websocket connection.
Creating a Channel for Live Updates
Now, let’s create a channel for our live updates:
Generate a new channel:
1
rails generate channel Poll
Open
app/channels/poll_channel.rb
and modify it:1 2 3 4 5 6 7 8 9 10 11 12 13
class PollChannel < ApplicationCable::Channel def subscribed stream_from "poll_channel" end def unsubscribed # Any cleanup needed when channel is unsubscribed end def receive(data) ActionCable.server.broadcast("poll_channel", data) end end
Implementing Real-Time Updates
To broadcast updates when a vote is cast, modify the votes_controller.rb
:
|
|
Understanding the Publish-Subscribe Model in Our App
The publish-subscribe (pub/sub) model is at the heart of how we’re using WebSockets in our live polling system. Here’s how it works in our context:
Subscribe: When a user opens a poll, their client (the Remix.js app) subscribes to the
poll_channel
. This is like tuning into a radio station.Publish: When any user casts a vote, our Rails app publishes (broadcasts) this update to the
poll_channel
. This is like the radio station sending out a signal.Receive: All subscribed clients receive this update in real-time. They can then update their local state to reflect the new vote count without refreshing the page.
This model allows for efficient, real-time communication. Instead of clients constantly polling the server for updates, the server proactively sends out updates when they occur. This reduces server load and provides a more responsive user experience.
Testing WebSocket Connection
To test your WebSocket connection before integrating with the frontend, we can use a Chrome extension.
Start your Rails server:
1
rails server
Open the chrome extension and connect to
ws://localhost:3000/cable
.
If you see “WebSocket connection established”, your Action Cable setup is working correctly.
In the next section, we’ll integrate this WebSocket functionality with our Remix.js frontend to create a truly real-time live polling experience.
6. Remix.js Frontend Setup (Remix.js)
Setting Up the Remix.js Frontend for Our Live Polling System
In this section, we’ll set up the frontend of our live polling system using Remix.js. We’ll explore the Remix.js project structure, set up routing, and create base layouts and components.
Creating a New Remix.js Project
After creating the project, you’ll see a structure like this:
|
|
Key directories and files:
app/
: Contains the core of your Remix application.app/routes/
: Where you define your routes and page components.app/root.tsx
: The root component of your application.public/
: For static assets.
Setting Up Routing in Remix
Remix uses a file-based routing system. Let’s set up some routes for our polling app:
- Create
app/routes/polls._index.tsx
for listing polls:
|
|
1.1 Let’s also update the types to include the new interfaces:
|
|
- Create
app/routes/polls.$pollId.tsx
for individual polls:
|
|
Creating Base Layouts and Components
Let’s create a base layout and some reusable components:
- Create
app/components/Layout.tsx
:
|
|
- Create
app/components/PollCard.tsx
:
|
|
- Update
app/root.tsx
to use the Layout:
|
|
Updating the Index Route
Finally, let’s update app/routes/_index.tsx
to welcome users to our polling app:
|
|
With this setup, we’ve created a basic structure for our Remix.js frontend. We have routes for listing polls and viewing individual polls, a layout component for consistent page structure, and a reusable poll card component.
Go ahead and run the dev server with:
|
|
In the next sections, we’ll integrate this frontend with our Rails API and implement real-time updates using WebSockets.
7. Implementing API Integration in Remix
In this section, we’ll connect our Remix.js frontend to our Rails API. We’ll cover setting up API calls, handling form submissions, and implementing error handling and validation.
Setting up API Calls Using Remix’s Data Loading Patterns
Remix provides powerful data loading patterns through its loader
and action
functions. Let’s implement these for our polls:
- Update
app/routes/polls._index.tsx
to fetch all available polls:
|
|
- Update
app/routes/polls.$pollId.tsx
to fetch a single poll and handle voting:
|
|
Handling Form Submissions for Creating Polls and Casting Votes
Now, let’s implement form submissions for creating polls and casting votes:
- Create
app/routes/polls.new.tsx
for creating new polls:
|
|
Error Handling and Validation
As you can see in the code above, we’ve implemented error handling and validation in several ways:
In the
loader
functions, we catch any errors that occur during the API calls and return an error message with an appropriate status code.In the
action
functions for creating polls and casting votes, we validate the form data before sending it to the API. If the data is invalid, we return an error message.In the components, we display any error messages returned by the
loader
oraction
functions.We use try-catch blocks to handle any unexpected errors that might occur during API calls.
This approach ensures that we handle errors gracefully and provide feedback to the user when something goes wrong.
By implementing these patterns, we’ve created a robust integration between our Remix.js frontend and our Rails API. Users can now view polls, create new polls, and cast votes, with all actions properly validated and error-handled.
8. Real-time Updates with WebSockets in Remix
Implementing Real-time Updates with WebSockets in Remix
In this section, we’ll add real-time functionality to our Remix.js frontend using WebSockets. We’ll connect to the WebSocket server, implement client-side logic for real-time updates, and use Remix’s useFetcher
for optimistic UI updates.
Connecting to WebSocket from Remix
First, let’s create a utility function to manage our WebSocket connection. Create a new file app/utils/websocket.ts
:
|
|
This function creates a singleton WebSocket connection and handles the initial subscription to our PollChannel
.
Implementing Client-side Logic for Real-time Updates
Now, let’s update our poll detail page to use this WebSocket connection. Update app/routes/polls.$pollId.tsx
:
|
|
Let’s break down the changes:
- We import the
getSocket
function and necessary hooks from React and Remix. - We use
useState
to manage the poll data, initializing it with the data from the loader. - In the
useEffect
hook, we set up the WebSocket connection and handle incoming messages. - We update the poll data in real-time when we receive a message from the WebSocket.
- We clean up the WebSocket subscription when the component unmounts.
Using Remix’s useFetcher for Optimistic UI Updates
We’re using Remix’s useFetcher
hook to handle voting. This allows us to update the UI optimistically, providing a more responsive user experience.
In the handleVote
function:
|
|
We use fetcher.submit
to send the vote to the server. While the request is in progress, we can update the UI immediately, giving the user instant feedback.
In the render method, we disable the vote buttons and the selected option marked as voted. This prevents users from submitting multiple votes while one is still being processed (in a real-world scenario, we would more sofisticated validations for preventing double votes).
With these changes, our polling app now updates in real-time:
- When a user votes, the UI updates immediately (optimistic update).
- If other users vote, all connected clients see the updated vote counts in real-time.
- The WebSocket connection ensures that updates are pushed to the client, reducing the need for polling and improving the app’s responsiveness. This is what it looks like:
Remember to test your webSocket connection thoroughly and handle potential disconnections gracefully in a production environment.
9. Conclusion: Reflecting on Our Live Polling System
As we wrap up this rather long tutorial, let’s take a moment to recap what we’ve built, and compare our approach to traditional methods.
Recap of What We’ve Built
Throughout this tutorial, we’ve created a real-time live polling system using a modern tech stack:
Backend: We built a Ruby on Rails API that handles:
- CRUD operations for polls and votes
- WebSocket connections via Action Cable for real-time updates
Frontend: We developed a Remix.js application that provides:
- A responsive user interface for creating polls and casting votes
- Real-time updates using WebSocket connections
- Optimistic UI updates for a smoother user experience
Key Features:
- Creating and managing polls
- Real-time voting and result updates
- Seamless integration between frontend and backend
- Efficient data loading and form handling with Remix
This project demonstrates how to combine the strengths of Rails for backend operations with the modern frontend capabilities of Remix.js, all tied together with real-time WebSocket communication.
Comparing This Approach to a Traditional Rails App
Our approach differs from a traditional Rails application in several key ways:
Separation of Concerns: By using Rails as an API and Remix for the frontend, we’ve created a clear separation between backend and frontend logic. This can lead to easier maintenance and the ability to scale each part independently.
Enhanced Frontend Capabilities: Remix.js provides powerful features like nested routing, server-side rendering, and efficient data loading patterns. These can lead to a more responsive and performant user experience compared to traditional server-rendered Rails views.
Real-Time Functionality: While it’s possible to implement WebSockets in a monolithic Rails app, our approach with a dedicated frontend makes it easier to manage real-time state and updates.
API-First Design: This approach encourages thinking in terms of API design from the start, which can be beneficial if you ever need to support multiple frontends or third-party integrations.
Modern JavaScript Ecosystem: Using Remix allows us to tap into the vast React and JavaScript ecosystem for additional libraries and tools.
However, this approach also comes with trade-offs:
- Increased complexity in project setup and deployment
- Potential for duplication of logic between frontend and backend
- Need for developers to be proficient in both Rails and modern JavaScript frameworks
Additional Resources for Learning
To deepen your understanding of the technologies we’ve used, consider exploring these resources:
Remix.js:
Ruby on Rails API:
WebSockets and Action Cable:
Full-Stack JavaScript:
API Design: