You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 09:23:25