React useEffect异步setState引发额外重渲染与重复请求问题
useConfigDetails Hook Let's break down why you're seeing 9 requests instead of 3, and how to fix this properly:
Root Cause Analysis
The most likely issue here is unaborted pending requests when resourceName changes rapidly. When resourceName updates, your useEffect runs again immediately, spawning a new set of 3 requests—while the previous 3 are still in flight. If this happens 3 times (e.g., resourceName changes 3 times in quick succession), you end up with 9 total requests (3 environments × 3 rounds of calls).
Another edge case to consider: if your component re-renders for unrelated reasons and accidentally triggers the useEffect again, but since your dependency is resourceName (a stable string), this shouldn't happen unless resourceName is being recreated on every render (unlikely in your current code). The abort controller fix below will cover both scenarios.
Step-by-Step Solution
Here's the optimized version of your hook with fixes to prevent duplicate requests and unnecessary re-renders:
import React, { useState, useEffect } from 'react'; import axios, { AxiosError } from 'axios'; // Assuming these types are defined elsewhere type Environment = 'dev' | 'stage' | 'prod'; type ResourceDetails = { /* Add your type definition here */ }; export const useConfigDetails = (resourceName: string) => { const [isLoading, setLoading] = useState<boolean>(true); const [error, setError] = useState<AxiosError | undefined>(undefined); const [allEnvDetails, setAllEnvDetails] = useState<ResourceDetails[] | undefined>(undefined); useEffect(() => { // Create an AbortController to cancel pending requests if the effect cleans up const abortController = new AbortController(); const signal = abortController.signal; const fetchConfigs = async () => { setLoading(true); setError(undefined); // Reset error state for new requests try { const environments: Environment[] = ['dev', 'stage', 'prod']; // Attach abort signal to each request to cancel if needed const configPromises = environments.map((env) => axios.get(`someUrlGetEndpoint?resource=${resourceName}&env=${env}`, { signal }) ); const resp = await Promise.all(configPromises); setAllEnvDetails(resp); } catch (err) { // Ignore errors caused by intentional abortion if (!(err instanceof AxiosError) || err.code !== 'ERR_CANCELED') { setError(err as AxiosError); } } finally { // Only update loading state if the request wasn't aborted if (!signal.aborted) { setLoading(false); } } }; fetchConfigs(); // Cleanup: abort pending requests when effect re-runs or component unmounts return () => abortController.abort(); }, [resourceName]); // Only re-run when resourceName changes return { allEnvDetails, isLoading, error }; };
Key Improvements Explained
AbortController Integration: This ensures that if
resourceNamechanges before previous requests complete, those pending calls are canceled immediately. No more old requests finishing and updating state (or adding to your request count).State Reset on New Requests: We explicitly set
loadingtotrueand clear theerrorstate when starting a new fetch, keeping the UI consistent and avoiding stale error messages.Async/Await for Readability: Switched from
.then()/.catch()to async/await to make the code easier to follow and maintain.Guard Clauses for Aborted Requests: We skip state updates if the request was aborted, preventing unnecessary re-renders caused by stale state changes.
Extra Tips to Reduce Unnecessary Re-Renders
Memoize the Return Value: If your parent component re-renders too often, wrap the hook's returned object in
useMemoto stabilize its reference:return React.useMemo(() => ({ allEnvDetails, isLoading, error }), [allEnvDetails, isLoading, error]);This ensures the returned object only changes when one of its properties updates, preventing parent components from re-rendering unnecessarily.
Verify
resourceNameStability: EnsureresourceNameisn't being recreated on every render (e.g., if derived from an object, useuseMemoto stabilize it). Since it's a string in your case, this is probably not an issue, but it's worth double-checking.
With these changes, you should only see 3 requests per resourceName change—no more duplicate calls!
内容的提问来源于stack exchange,提问作者Christie Chen

