在 React 中,当在 url 中手动更改路由参数时,全局状态会不同步

In React, global state goes out of sync when route parameters are manually changed in url

我们的 React 应用程序中有一个变量是:

  1. App.js 中定义为全局状态,通过 GlobalContext.Provider
  2. 以其 setter 方法全局传递给其他组件
  3. 单独用作许多应用程序路由的路由参数。

下面是我们 App.js 文件相关部分的简短代码片段:

import React, { useState, useEffect } from 'react';
import GlobalContext from './context/GlobalContext';
import OtherComponents...

function App() {
    const [competition, setCompetition] = useState({ value: 15, label: 'Season 1' });

    return (
        <GlobalContext.Provider value={{ // Pass Global State Through To Entire App
            competition, setCompetition
        }}>
            <Navbar />
            <Switch>
                <Route exact path='/' render={(props) => <HomePage {...props} />} />
                <Route exact path='/stats' component={StatsPage} />
                <Route exact path='/about' component={AboutUs} />
                <Route exact path='/persons/:competitionId/ component={PersonsComponent} />
                <Route exact path='/teams/:competitionId component={TeamsComponent} />
            </Switch>
        </GlobalContext.Provider>
    );
}

export default App;

competition 全局状态有键 valuelabel,然后 url 参数中的 competitionIdcompetition.value值。

competition 的全局状态值旨在使用 select 小部件在 <Navbar> 组件中更改。当这个小部件被切换时,全局状态被更新,并且 useHistory 挂钩用于将应用程序推送到新路由,使用更新的 competition.value 设置 competitionId url 参数.

competition 的值在我们应用程序的许多组件中是 需要的,包括那些没有 url 参数的组件(例如在 <HomePage>组件)。出于这个原因,我们觉得它需要作为一个全局变量传递给所有其他组件。这对我们来说也非常方便,因为可以使用 useContext 挂钩在任何地方轻松访问变量。

但是,在我们的 url 参数中,该值似乎 需要 。这些组件根据传递的 competitionId 获取不同的数据,它们在 url 参数中是应用程序路由的重要组成部分。

我们的问题那么用户可以手动更改网站的url,这可以更改url参数而无需更改变量的全局状态。通过手动更改 url 而不是使用 select 小部件,全局状态和 url 参数就会不同步...

编辑: 这是我们用来切换 competition 值的 select 组件(抱歉 post 变长了) .这个 select 在我们的导航栏中,并且在我们的 <Switch>:

之外是全局可访问的
function CompetitionSelect({ currentPath }) {
    // Grab History In Order To Push To Selected Pages
    let history = useHistory();
    let { competition, setCompetition } = useContext(GlobalContext);

    // Fetch Data on All Competitions (dropdown options)
    const competitionInfosConfig = {};
    const [competitionInfos, isLoading1, isError1] = useInternalApi('competitionInfo', [], competitionInfosConfig);

    // Messy digging of competitionId out of current path.
    let competitionIds = competitionInfos.map(row => row.competitionId);
    let pathCompetitionId = null;
    competitionIds.forEach(id => {
        if (currentPath.includes(`/${id}/`)) {
            pathCompetitionId = id;
        }
    });

    // Messy Handling State/Params Out Of Sync
    if (pathCompetitionId === null) {
        console.log('Not a page where testing is needed');
    }
    else if (competition.value !== pathCompetitionId) {
        console.log('WERE OUT OF SYNC...');
        let correctGlobalState = { value: pathCompetitionId, label: 'Label Set' };
        setCompetition(correctGlobalState);
    } else {
        console.log('IN SYNC: ', competition.value, ' == ', pathCompetitionId);
    }

    // Handle updating state + pushing to new route
    const handleSelect = (event) => {
        let oldPath = JSON.parse(JSON.stringify(history.location.pathname));
        let newPath = '';
        competitionIds.forEach(id => {
            if (oldPath.includes(`/${id}/`)) {
                newPath = oldPath.replace(`/${id}/`, `/${event.value}/`)
            }
        });

        if (newPath !== '') {
            setCompetition(event);
            history.push(newPath);
        }
    };

    // Create The Select
    const competitionSelect =
        (<Select
            styles={appSelectStyles}
            value={competition}
            options={competitionInfos}
            onChange={handleSelect}
            placeholder={'Select Competition'}
        />);

    return (
        {competitionSelect}
    );
}

export default CompetitionSelect;

该组件在技术上确实解决了 if, if else, else 子句中的不同步问题,但是每当调用 setCompetition(correctGlobalState) 时,React 都会抛出以下警告消息:

Warning: Cannot update a component (App) while rendering a different component (CompetitionSelect). To locate the bad setState() call inside CompetitionSelect, follow the stack trace as described...

我认为解决这个问题的好方法是

  • 尝试从 URL
  • 中获取比赛数据
  • 如果可用,则将其用作状态的初始值,如果不可用,则使用默认值
  • 使用 useEffect 使用来自 URL 的比赛数据更新状态(如果可用),在位置更改时

例如

function App() {
  const location = useLocation();
  const params = useParams();

  // implement getCompetitionFromParams as appropriate
  // returns undefined if no competition data in URL params
  const competitionFromParams = getCompetitionFromParams(location, params)

  const [competition, setCompetition] = 
    // init state on first load, 
    // with default if competitionFromParams not available
    useState(competitionFromParams || { value: 15, label: 'Season 1' });


  // update state on location change,
  // if competitionFromParams available
  useEffect(() => {
    if (competitionFromParams !== undefined) {
      setCompetition(competitionFromParams); 
    }
  }, [location]); // runs useEffect only on location change

  ...

有了它,你就得到了你想要的 - competition 处于全局状态,可以使用 select 手动设置它,但也可以 auto-synced 当它出现在相关的 URL 参数。