React/Redux点击应用:如何每次点击更新数据库且减少请求?
Building Your Learning App: Step-by-Step Solution
Got it, let's break down how to build this learning app step by step, focusing on solving that core request-throttling challenge.
1. Third-Party Authentication (Google/FB)
I’d recommend using Firebase Auth for this—it’s straightforward to integrate with React and supports both Google and Facebook out of the box.
- Install dependencies:
npm install firebase @reduxjs/toolkit react-redux - Initialize Firebase in your app with your project credentials
- Create a login component with buttons for Google/FB:
import { getAuth, signInWithPopup, GoogleAuthProvider, FacebookAuthProvider } from "firebase/auth"; import { useDispatch } from "react-redux"; import { setUser } from "./scoreSlice"; const Login = () => { const auth = getAuth(); const dispatch = useDispatch(); const handleGoogleLogin = async () => { const provider = new GoogleAuthProvider(); const result = await signInWithPopup(auth, provider); // Dispatch user data to Redux store (name, uid, initial score) dispatch(setUser({ name: result.user.displayName, uid: result.user.uid, score: 0 })); }; const handleFacebookLogin = async () => { const provider = new FacebookAuthProvider(); const result = await signInWithPopup(auth, provider); dispatch(setUser({ name: result.user.displayName, uid: result.user.uid, score: 0 })); }; return ( <div className="login-buttons"> <button onClick={handleGoogleLogin}>Sign in with Google</button> <button onClick={handleFacebookLogin}>Sign in with Facebook</button> </div> ); };
2. State Management with Redux Toolkit
We’ll use a slice to manage the user’s score, pending clicks, and the top 10 scores:
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; // Async thunk to update user score in DB export const updateUserScore = createAsyncThunk( "scores/updateUserScore", async ({ uid, delta }, { rejectWithValue }) => { try { // Replace with your backend API or Firestore call await fetch(`/api/users/${uid}/score`, { method: "POST", body: JSON.stringify({ delta }), headers: { "Content-Type": "application/json" } }); return delta; } catch (err) { return rejectWithValue(err.message); } } ); // Async thunk to fetch initial top 10 scores export const fetchTopScores = createAsyncThunk( "scores/fetchTopScores", async (_, { rejectWithValue }) => { try { const res = await fetch("/api/scores/top10"); const data = await res.json(); return data; } catch (err) { return rejectWithValue(err.message); } } ); const scoreSlice = createSlice({ name: "scores", initialState: { user: null, userScore: 0, pendingClicks: 0, topScores: [], status: "idle", error: null }, reducers: { setUser: (state, action) => { state.user = action.payload; state.userScore = action.payload.score; }, incrementLocalScore: (state) => { state.userScore += 1; state.pendingClicks += 1; }, resetPendingClicks: (state) => { state.pendingClicks = 0; }, setTopScores: (state, action) => { state.topScores = action.payload; } }, extraReducers: (builder) => { builder .addCase(updateUserScore.fulfilled, (state) => { state.resetPendingClicks(); }) .addCase(fetchTopScores.fulfilled, (state, action) => { state.topScores = action.payload; }); } }); export const { incrementLocalScore, resetPendingClicks, setTopScores } = scoreSlice.actions; export default scoreSlice.reducer;
3. Solving the Core Challenge: Throttling DB Requests
The biggest pain point is avoiding a DB call on every single click. Here’s a practical fix using debouncing:
- Debounce the update call: Wait a short window (e.g., 3 seconds) after the last click before sending all pending clicks to the server. This batches multiple clicks into one request.
- Sync on unmount: Ensure pending clicks are saved if the user navigates away before the debounce window ends.
Implement this in your score button component:
import { useEffect, useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { incrementLocalScore, updateUserScore } from "./scoreSlice"; const ScoreButton = () => { const dispatch = useDispatch(); const { user, pendingClicks } = useSelector((state) => state.scores); // Debounce function to delay DB update const debouncedUpdate = useCallback( debounce(async () => { if (pendingClicks > 0 && user?.uid) { await dispatch(updateUserScore({ uid: user.uid, delta: pendingClicks })); } }, 3000), // Wait 3 seconds after last click [pendingClicks, user, dispatch] ); const handleClick = () => { dispatch(incrementLocalScore()); debouncedUpdate(); }; // Sync pending clicks when component unmounts useEffect(() => { return () => { if (pendingClicks > 0 && user?.uid) { dispatch(updateUserScore({ uid: user.uid, delta: pendingClicks })); } }; }, [pendingClicks, user, dispatch]); // Helper debounce utility function debounce(func, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; } return ( <div className="score-section"> <h3>Your Score: {useSelector(state => state.scores.userScore)}</h3> <button onClick={handleClick} disabled={!user}>Click to Score!</button> </div> ); };
4. Dynamic Top 10 Leaderboard
To keep the leaderboard fresh without constant requests:
- Real-time listeners (Firebase): If using Firestore, set up a listener that automatically updates Redux when the top 10 changes:
// Add this to your app's initialization or a useEffect hook import { getFirestore, collection, query, orderBy, limit, onSnapshot } from "firebase/firestore"; import { useDispatch } from "react-redux"; import { setTopScores } from "./scoreSlice"; const db = getFirestore(); const dispatch = useDispatch(); // Listen for changes to the top 10 users const topScoresQuery = query( collection(db, "users"), orderBy("totalClicks", "desc"), limit(10) ); onSnapshot(topScoresQuery, (snapshot) => { const updatedTopScores = snapshot.docs.map(doc => ({ id: doc.id, name: doc.data().name, score: doc.data().totalClicks })); dispatch(setTopScores(updatedTopScores)); });
- Polling with cache (REST API): If using a REST backend, fetch the top 10 every 30 seconds and cache the result to avoid redundant calls.
5. Displaying the Top 10 Leaderboard
Create a clean component to render the leaderboard:
import { useSelector } from "react-redux"; const TopScoresBoard = () => { const { topScores } = useSelector((state) => state.scores); return ( <div className="leaderboard"> <h2>Top 10 Clickers</h2> <ul> {topScores.length === 0 ? ( <li>No scores yet—be the first!</li> ) : ( topScores.map((user, index) => ( <li key={user.id} className="leaderboard-item"> <span className="rank">{index + 1}.</span> <span className="name">{user.name}</span> <span className="score">{user.score} clicks</span> </li> )) )} </ul> </div> ); };
内容的提问来源于stack exchange,提问作者GeraltDieSocke
相关产品推荐
相关产品推荐

