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

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>
    )
};

问题

  1. 我是否错误处理了Context?有没有更优的处理方式?API调用中的多次状态更新是否可合并为reducer,从而仅触发一次重渲染?
  2. 我发现通过路由跳转至新页面时状态会丢失,例如跳转到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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 20:39:54