React:配置调用远程API的ContextProvider,避免重渲染重复调用
问题背景
我正在开发一款应用,需要存储部分仅需加载一次的核心远程信息。我了解可以通过localStorage缓存远程调用结果,但本次问题仅聚焦于避免在渲染和重渲染过程中重复调用fetchState函数。
根应用代码
import {getUser} from "./authenticator/CredentialManager"; export const UserContext = createContext( {user: getUser(), setUser:()=>{} }); export const LocationContext = createContext(null) function App() { const [user,setUser] = useState(getUser()); const [location, setLocation] = useState(null); const [schedule,setSchedule] = useState(null); const update = useMemo( () => ( {user,setUser} ),[user]); const fetchState = ()=>{ const api = new VetbookWpApi(); if(location){ if(location.id){ return; } } api.getLocation().then((result) => { let output = JSON.parse(result); let location = null if (Array.isArray(output)) { location = output[0]; } else location = output; setLocation(location); // <--triggers rerender return location; },(e)=>{ console.log(e); }).then((response) => { if (response) { api.getSchedules(response.id).then((result) => { setSchedule(JSON.parse(result)); // <--triggers rerender }); } }); } useEffect(() => { fetchState(); }, [] ); return ( <div className="App"> <LocationContext.Provider value={location}> <UserContext.Provider value={update}> <Navigation/> <Main/> </UserContext.Provider> </LocationContext.Provider> </div> ); } const Main = () => { return ( <div className="App"> <Routes> <Route path='/' element={<Home/>}></Route> <Route path='/page1' element={<Page1/>}></Route> <Route path='/page2' element={<Page2/>}></Route> <Route path='/page3' element={<Page3/>}></Route> </Routes> </div> ) } const Navigation = () => { const location = useContext(LocationContext); const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const handleClick = (event) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; let name = ''; if(location){ name = location.name; } return ( <AppBar position='static'> <Container maxWidth='1'> <Toolbar disableGutters> <IconButton id="fade-button" aria-controls={open ? 'fade-menu' : undefined} aria-haspopup="true" aria-expanded={open ? 'true' : undefined} onClick={handleClick} color='inherit' sx={{ mr: 2 }} ><MenuIcon/></IconButton> <Menu id="fade-menu" MenuListProps={{ 'aria-labelledby': 'fade-button', }} anchorEl={anchorEl} open={open} onClose={handleClose} TransitionComponent={Fade} > <MenuItem component='a' href='/' onClick={handleClose}>Home</MenuItem> <MenuItem component='a' href='/Page1' onClick={handleClose}>Page1</MenuItem> <MenuItem component='a' href='/Page2' onClick={handleClose}>Page2</MenuItem> </Menu> <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>VetBook - {name}</Typography> <LoginButton/> </Toolbar> </Container> </AppBar> ) };
问题
- 我是否错误处理了Context?有没有更优的处理方式?API调用中的多次状态更新是否可合并为reducer,从而仅触发一次重渲染?
- 我发现通过路由跳转至新页面时状态会丢失,例如跳转到page1时App状态重置为null,进而引发Context更新。是否路由会销毁并重新渲染整个App?
解答
问题1:Context处理优化与合并状态更新
Context处理优化点
你的Context处理有几个可优化的地方:
- 初始值设置:
UserContext的初始值直接调用getUser()会在每次组件初始化时执行,建议改为静态默认值{ user: null, setUser: () => {} },避免不必要的函数调用。 - Context结构:当前拆分的
UserContext和LocationContext如果职责清晰,保持拆分没问题;若后续状态关联紧密,可合并为一个AppContext,减少Provider嵌套层级。 update对象简化:useMemo的写法可以简化为const update = useMemo(() => ({ user, setUser }), [user]);,逻辑保持一致但更简洁。
用Reducer合并状态更新
可以通过useReducer合并location和schedule状态,减少重渲染次数,示例如下:
// 定义reducer函数 const appReducer = (state, action) => { switch (action.type) { case 'SET_LOCATION_AND_SCHEDULE': return { ...state, location: action.payload.location, schedule: action.payload.schedule }; default: return state; } }; // 在App组件中使用 function App() { const [user, setUser] = useState(getUser()); const [appState, dispatch] = useReducer(appReducer, { location: null, schedule: null }); const fetchState = async () => { const api = new VetbookWpApi(); if (appState.location?.id) return; try { // 先获取location const locationResult = await api.getLocation(); const output = JSON.parse(locationResult); const location = Array.isArray(output) ? output[0] : output; // 再获取对应schedule const scheduleResult = await api.getSchedules(location.id); const schedule = JSON.parse(scheduleResult); // 一次性更新两个状态,仅触发一次重渲染 dispatch({ type: 'SET_LOCATION_AND_SCHEDULE', payload: { location, schedule } }); } catch (e) { console.log(e); } }; useEffect(() => { fetchState(); }, []); const update = useMemo(() => ({ user, setUser }), [user]); return ( <div className="App"> <LocationContext.Provider value={appState.location}> <UserContext.Provider value={update}> <Navigation/> <Main/> </UserContext.Provider> </LocationContext.Provider> </div> ); }
问题2:路由跳转状态丢失
路由跳转不会销毁并重新渲染整个App组件,状态丢失的原因是你使用了原生<a>标签跳转,这种方式会刷新整个页面,导致所有React状态重置。
解决方法是用React Router提供的Link组件替代原生<a>标签:
// 先导入Link import { Link } from 'react-router-dom'; // 修改Navigation中的MenuItem <MenuItem component={Link} to='/' onClick={handleClose}>Home</MenuItem> <MenuItem component={Link} to='/page1' onClick={handleClose}>Page1</MenuItem> <MenuItem component={Link} to='/page2' onClick={handleClose}>Page2</MenuItem>
Link通过客户端路由跳转,不会刷新页面,从而保留App组件的状态。
内容的提问来源于stack exchange,提问作者Narrim
相关产品推荐
相关产品推荐

