Live Poll
Interview Scenario: "Build a simple voting poll where users can vote and see results update in near real-time. Don't worry about WebSockets yet - just make it work."
What You'll Learn
- The polling pattern for near real-time updates
- setInterval + useEffect cleanup to prevent memory leaks
- Understanding the limitations of polling
- Nested data structures to represent relationships
Interview Mode: Simplified in-memory storage. Perfect for live coding.
Step 1: Backend Setup
Let's create a FastAPI backend with nested dictionaries. This avoids database setup while still demonstrating the poll-options relationship structure.
1from fastapi import FastAPI, HTTPException2from fastapi.middleware.cors import CORSMiddleware3from pydantic import BaseModel4from typing import List56app = FastAPI()78app.add_middleware(9 CORSMiddleware,10 allow_origins=["http://localhost:3000"],11 allow_credentials=True,12 allow_methods=["*"],13 allow_headers=["*"],14)1516# In-memory storage (resets when server restarts)17polls = {}18next_poll_id = 119next_option_id = 12021class PollCreate(BaseModel):22 question: str23 options: List[str]2425@app.post("/api/polls")26def create_poll(poll_data: PollCreate):27 global next_poll_id, next_option_id28 poll_options = []29 for text in poll_data.options:30 poll_options.append({"id": next_option_id, "text": text, "votes": 0})31 next_option_id += 13233 poll = {34 "id": next_poll_id,35 "question": poll_data.question,36 "options": poll_options,37 "total_votes": 038 }39 polls[next_poll_id] = poll40 next_poll_id += 141 return poll4243@app.get("/api/polls/{poll_id}")44def get_poll(poll_id: int):45 if poll_id not in polls:46 raise HTTPException(status_code=404, detail="Poll not found")47 poll = polls[poll_id]48 poll["total_votes"] = sum(opt["votes"] for opt in poll["options"])49 return poll5051@app.post("/api/polls/{poll_id}/vote/{option_id}")52def cast_vote(poll_id: int, option_id: int):53 if poll_id not in polls:54 raise HTTPException(status_code=404, detail="Poll not found")55 for opt in polls[poll_id]["options"]:56 if opt["id"] == option_id:57 opt["votes"] += 158 return {"message": "Vote cast successfully"}59 raise HTTPException(status_code=404, detail="Option not found")
Why no database?
The trade-off
Test the endpoints:
1# Create a poll2curl -X POST http://localhost:8000/api/polls \3 -H "Content-Type: application/json" \4 -d '{"question": "Best programming language?", "options": ["Python", "JavaScript", "Rust", "Go"]}'56# Get the poll7curl http://localhost:8000/api/polls/189# Cast a vote for option 110curl -X POST http://localhost:8000/api/polls/1/vote/1
Step 2: React Frontend with Polling
Here's where the magic happens. We'll use setInterval inside useEffectto fetch the poll data every 2 seconds, giving us near real-time updates. We'll also add a visible API call counter to demonstrate why polling isn't ideal for production.
1"use client"23import { useState, useEffect, useRef } from "react"45interface PollOption {6 id: number7 text: string8 votes: number9}1011interface Poll {12 id: number13 question: string14 options: PollOption[]15 total_votes: number16}1718export default function LivePoll() {19 const [poll, setPoll] = useState<Poll | null>(null)20 const [loading, setLoading] = useState(true)21 const [error, setError] = useState<string | null>(null)22 const [apiCalls, setApiCalls] = useState(0)2324 const POLL_ID = 1 // Hardcoded for demo - in real app, use URL params2526 // Fetch poll data27 const fetchPoll = async () => {28 try {29 const res = await fetch(`http://localhost:8000/api/polls/${POLL_ID}`)30 if (!res.ok) throw new Error("Failed to fetch poll")31 const data = await res.json()32 setPoll(data)33 setApiCalls(prev => prev + 1) // Track API calls34 } catch (err) {35 setError("Failed to load poll")36 } finally {37 setLoading(false)38 }39 }4041 // Poll every 2 seconds42 useEffect(() => {43 fetchPoll() // Initial fetch4445 const interval = setInterval(() => {46 fetchPoll()47 }, 2000)4849 // CRITICAL: Clean up the interval when component unmounts50 return () => {51 clearInterval(interval)52 }53 }, [])5455 const handleVote = async (optionId: number) => {56 try {57 await fetch(58 `http://localhost:8000/api/polls/${POLL_ID}/vote/${optionId}`,59 { method: "POST" }60 )61 // Immediately refetch to show updated results62 fetchPoll()63 } catch (err) {64 setError("Failed to cast vote")65 }66 }6768 // Calculate percentage for progress bar69 const getPercentage = (votes: number) => {70 if (!poll || poll.total_votes === 0) return 071 return Math.round((votes / poll.total_votes) * 100)72 }7374 if (loading) return <div className="p-8">Loading poll...</div>75 if (error) return <div className="p-8 text-red-500">{error}</div>76 if (!poll) return <div className="p-8">Poll not found</div>7778 return (79 <main className="min-h-screen p-8 max-w-2xl mx-auto">80 {/* API Call Counter - Shows the "cost" of polling */}81 <div className="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-lg">82 <div className="flex items-center justify-between">83 <span className="text-amber-800 font-medium">API Calls Made:</span>84 <span className="text-2xl font-mono font-bold text-amber-600">85 {apiCalls}86 </span>87 </div>88 <p className="text-sm text-amber-600 mt-1">89 Watch this number grow! Every 2 seconds, we make a request.90 </p>91 </div>9293 <h1 className="text-3xl font-bold mb-2">{poll.question}</h1>94 <p className="text-gray-500 mb-8">95 {poll.total_votes} total vote{poll.total_votes !== 1 ? "s" : ""}96 </p>9798 <div className="space-y-4">99 {poll.options.map((option) => (100 <div key={option.id} className="relative">101 <button102 onClick={() => handleVote(option.id)}103 className="w-full p-4 text-left border rounded-lg hover:border-blue-400104 transition-colors relative overflow-hidden group"105 >106 {/* Progress bar background */}107 <div108 className="absolute inset-0 bg-blue-100 transition-all duration-500"109 style={{ width: `${getPercentage(option.votes)}%` }}110 />111112 {/* Content */}113 <div className="relative flex justify-between items-center">114 <span className="font-medium">{option.text}</span>115 <span className="text-gray-600">116 {option.votes} ({getPercentage(option.votes)}%)117 </span>118 </div>119 </button>120 </div>121 ))}122 </div>123124 {/* Polling indicator */}125 <div className="mt-8 flex items-center gap-2 text-sm text-gray-500">126 <span className="relative flex h-3 w-3">127 <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>128 <span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>129 </span>130 Live updating every 2 seconds131 </div>132 </main>133 )134}
Watch the Network Tab!
The cleanup function is critical
return () => clearInterval(interval)cleanup function. Without it, if the component unmounts (user navigates away), the interval keeps running! This causes memory leaks, unnecessary API calls, and the dreaded "can't update state on unmounted component" warning. Always clean up your intervals and subscriptions.Key Patterns
The polling pattern
- Fetch data initially when component mounts
- Set up an interval to refetch at regular intervals
- Clean up the interval when component unmounts
useRef for values that shouldn't trigger re-renders
useRefinstead of useState. The interval ID doesn't need to be displayed, so useRef is more appropriate.Exercises
What You Learned
Polling Pattern
- setInterval for periodic fetching
- useEffect cleanup to prevent memory leaks
- Visualizing the cost of polling (API counter)
- When polling is appropriate vs. not
Data Relationships
- Nested dicts for one-to-many relationships
- Managing related IDs manually
- Calculating aggregates (total votes)
- Trade-offs of in-memory storage
What are the limitations of this approach?
- Wasteful: We make requests even when nothing changed
- Not truly real-time: There's always a delay (up to 2 seconds)
- Doesn't scale: 1000 users = 500 requests/second to your server
- Battery drain: Mobile devices suffer from constant network activity
The solution? WebSockets - a persistent connection where the server pushes updates only when something changes. We'll implement this properly in the next project!
Ready for more?
In the next project, you'll build an AI-powered chat using streaming responses and learn how to handle real-time data from AI APIs.
Project 7: AI Chat