动态遍历useSWR返回数据调用useSWR触发React Hook顺序错误求助
错误原因及解决方案
错误原因
你碰到的这个错误,核心是违反了React Hook的调用规则:
- React要求所有Hook必须在组件/自定义Hook的顶层作用域调用,绝对不能放在循环、条件判断、嵌套函数(比如
map的回调函数)里。 - 原来的代码中,
useSWR是在GoogleCalendarListResult?.items?.map()的回调里调用的:第一次渲染时GoogleCalendarListResult还没异步返回(值为undefined),不会执行这个map,也就不会调用对应的useSWR;当日历列表数据加载完成后,组件重新渲染,此时会执行map,新增了多个useSWR调用,导致两次渲染的Hook调用顺序、数量不一致,React因此抛出了这个错误。 - 改成固定数组
['calendarid1', 'calendarid2']后,每次渲染map的次数固定,Hook调用的顺序和数量完全一致,所以不会触发错误,但这种写法无法动态适配用户的实际日历列表。
可行解决方案
方案1:基于SWR批量请求所有日历事件
等日历列表加载完成后,在Hook顶层批量请求所有日历的事件,确保Hook调用顺序稳定:
import { Schedule, User } from '../../types/commons.type'; import { useLocalStorage } from 'usehooks-ts'; import useSWR from 'swr'; import { FetchCalendarList } from '../../types/googleCalendarAPIResponse.type'; import { currentUTCMonthString } from '../../utils/DateUtils'; const baseURL = 'https://www.googleapis.com/calendar/v3'; const fetcher = ([path, method, body = {}, accessToken = '']: [string, string, object, string]) => { const headers = { 'Content-Type': ['POST', 'PUT', 'PATCH'].includes(method) ? 'application/json' : '', Authorization: `Bearer ${accessToken}`, }; const fetchOptions = { method, headers, body: method === 'GET' ? undefined : JSON.stringify(body), }; const apiURL = `${baseURL}${path}`; return fetch(apiURL, fetchOptions).then((r) => r.json()); }; export const useGoogleCalendarEvents = () => { const [user] = useLocalStorage<User | undefined>('user', undefined); const { startString, endString } = currentUTCMonthString(); const parameters = `timeMin=${startString}&timeMax=${endString}`; // 第一步:获取用户日历列表 const { data: calendarList } = useSWR<FetchCalendarList>( user?.googleAccessToken ? ['/users/me/calendarList', 'GET', {}, user.googleAccessToken] : null, fetcher, ); // 第二步:批量请求所有日历的事件(仅当日历列表存在时) const calendarIds = calendarList?.items?.map(cal => cal.id) || []; const { data: allEventResults } = useSWR( calendarIds.length && user?.googleAccessToken ? calendarIds.map(id => [`/calendars/${id}/events?${parameters}`, 'GET', {}, user.googleAccessToken]) : null, (requestPaths) => Promise.all(requestPaths.map(path => fetcher(path))), ); // 整理所有日历的事件到一个数组 const events = allEventResults?.flatMap(result => result.items || []) || []; return { events, calendarList }; };
方案2:封装子组件处理单个日历事件
把单个日历的事件请求逻辑封装成独立子组件,父组件拿到日历列表后循环渲染子组件,这样每个子组件内部的useSWR都处于顶层作用域,符合规则:
// 子组件:负责单个日历的事件请求 const CalendarEventFetcher = ({ calendarId, accessToken, parameters, onEventsLoaded }: { calendarId: string; accessToken: string; parameters: string; onEventsLoaded: (events: any[]) => void; }) => { const { data: eventResult } = useSWR( [`/calendars/${calendarId}/events?${parameters}`, 'GET', {}, accessToken], fetcher, ); useEffect(() => { if (eventResult?.items) { onEventsLoaded(eventResult.items); } }, [eventResult, onEventsLoaded]); return null; // 仅做数据获取,无UI输出 }; // 原自定义Hook export const useGoogleCalendarEvents = () => { const [user] = useLocalStorage<User | undefined>('user', undefined); const { startString, endString } = currentUTCMonthString(); const parameters = `timeMin=${startString}&timeMax=${endString}`; const [allEvents, setAllEvents] = useState<any[]>([]); // 获取日历列表 const { data: calendarList } = useSWR<FetchCalendarList>( user?.googleAccessToken ? ['/users/me/calendarList', 'GET', {}, user.googleAccessToken] : null, fetcher, ); // 收集所有子组件返回的事件 const handleEventsLoaded = (events: any[]) => { setAllEvents(prev => [...prev, ...events]); }; // 注意:该方案需要在使用Hook的组件中渲染CalendarEventFetcher // 若要纯数据Hook,方案1更合适 return { events: allEvents, calendarList, CalendarEventFetcher, // 暴露子组件供外部渲染 parameters, userAccessToken: user?.googleAccessToken }; };
内容的提问来源于stack exchange,提问作者Eddie Ciou
相关产品推荐
相关产品推荐

