Project 6~30 minutesGuided Practice

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
Loading...

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.

backend/main.py
1from fastapi import FastAPI, HTTPException
2from fastapi.middleware.cors import CORSMiddleware
3from pydantic import BaseModel
4from typing import List
5
6app = FastAPI()
7
8app.add_middleware(
9 CORSMiddleware,
10 allow_origins=["http://localhost:3000"],
11 allow_credentials=True,
12 allow_methods=["*"],
13 allow_headers=["*"],
14)
15
16# In-memory storage (resets when server restarts)
17polls = {}
18next_poll_id = 1
19next_option_id = 1
20
21class PollCreate(BaseModel):
22 question: str
23 options: List[str]
24
25@app.post("/api/polls")
26def create_poll(poll_data: PollCreate):
27 global next_poll_id, next_option_id
28 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 += 1
32
33 poll = {
34 "id": next_poll_id,
35 "question": poll_data.question,
36 "options": poll_options,
37 "total_votes": 0
38 }
39 polls[next_poll_id] = poll
40 next_poll_id += 1
41 return poll
42
43@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 poll
50
51@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"] += 1
58 return {"message": "Vote cast successfully"}
59 raise HTTPException(status_code=404, detail="Option not found")

Why no database?

In a 45-minute interview, setting up SQLAlchemy with foreign key relationships can eat 20+ minutes. Using nested dicts demonstrates the same one-to-many concept without the boilerplate. You can mention: "In production, I'd use a database with proper foreign keys. See Production Mode for that setup."

The trade-off

This in-memory approach resets when the server restarts. That's fine for interviews! Switch to Production Mode above to see the full database setup with relationships.

Test the endpoints:

Terminal
1# Create a poll
2curl -X POST http://localhost:8000/api/polls \
3 -H "Content-Type: application/json" \
4 -d '{"question": "Best programming language?", "options": ["Python", "JavaScript", "Rust", "Go"]}'
5
6# Get the poll
7curl http://localhost:8000/api/polls/1
8
9# Cast a vote for option 1
10curl -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.

frontend/app/poll/page.tsx
1"use client"
2
3import { useState, useEffect, useRef } from "react"
4
5interface PollOption {
6 id: number
7 text: string
8 votes: number
9}
10
11interface Poll {
12 id: number
13 question: string
14 options: PollOption[]
15 total_votes: number
16}
17
18export 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)
23
24 const POLL_ID = 1 // Hardcoded for demo - in real app, use URL params
25
26 // Fetch poll data
27 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 calls
34 } catch (err) {
35 setError("Failed to load poll")
36 } finally {
37 setLoading(false)
38 }
39 }
40
41 // Poll every 2 seconds
42 useEffect(() => {
43 fetchPoll() // Initial fetch
44
45 const interval = setInterval(() => {
46 fetchPoll()
47 }, 2000)
48
49 // CRITICAL: Clean up the interval when component unmounts
50 return () => {
51 clearInterval(interval)
52 }
53 }, [])
54
55 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 results
62 fetchPoll()
63 } catch (err) {
64 setError("Failed to cast vote")
65 }
66 }
67
68 // Calculate percentage for progress bar
69 const getPercentage = (votes: number) => {
70 if (!poll || poll.total_votes === 0) return 0
71 return Math.round((votes / poll.total_votes) * 100)
72 }
73
74 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>
77
78 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>
92
93 <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>
97
98 <div className="space-y-4">
99 {poll.options.map((option) => (
100 <div key={option.id} className="relative">
101 <button
102 onClick={() => handleVote(option.id)}
103 className="w-full p-4 text-left border rounded-lg hover:border-blue-400
104 transition-colors relative overflow-hidden group"
105 >
106 {/* Progress bar background */}
107 <div
108 className="absolute inset-0 bg-blue-100 transition-all duration-500"
109 style={{ width: `${getPercentage(option.votes)}%` }}
110 />
111
112 {/* 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>
123
124 {/* 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 seconds
131 </div>
132 </main>
133 )
134}

Watch the Network Tab!

Open your browser's DevTools and go to the Network tab while running this app. You'll see requests piling up every 2 seconds - even when nothing has changed! This is the fundamental problem with polling: it wastes bandwidth and server resources. We'll solve this properly with WebSockets in the Real-time Chat project.

The cleanup function is critical

Interviewers will specifically look for the 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

Polling is the simplest way to get "real-time" updates without WebSockets. The pattern is:
  1. Fetch data initially when component mounts
  2. Set up an interval to refetch at regular intervals
  3. Clean up the interval when component unmounts
It's easy to implement but inefficient. Use it for prototypes or low-traffic features.

useRef for values that shouldn't trigger re-renders

If you need to track something (like the interval ID) without causing re-renders, use 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?

This is a classic interview question. The polling approach we built has several problems:
  • 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