【问题标题】:In React, global state goes out of sync when route parameters are manually changed in url在 React 中,当在 url 中手动更改路由参数时,全局状态会不同步
【发布时间】:2020-07-31 00:23:34
【问题描述】:

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

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

下面是我们App.js文件相关部分的短代码sn-p then:

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 小部件在&lt;Navbar&gt; 组件中进行更改。当这个小部件被切换时,全局状态被更新,useHistory 钩子用于将应用推送到新路由,使用更新后的competition.value 设置competitionId url 参数。

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

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

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

编辑:这是我们用来切换competition 值的select 组件(抱歉,帖子太长了)。这个选择在我们的导航栏中,并且在我们的&lt;Switch&gt;之外是全局可访问的:

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当作状态吗?
  • 也许吧?我不确定什么是最好的,虽然看起来 url 参数和全局状态是重复的......
  • 我会尽量避免重复状态,让数据只向一个方向流动。当用户更改下拉值时,更新 url,并重新渲染组件。
  • 全局状态的一个优点是可以从所有组件全局访问,而我只能从顶级route 组件(而不是嵌套组件)获取路由参数。当useParams()App.js &lt;Switch/&gt; 中未定义的嵌套组件中运行时,它返回一个空对象...
  • 拥有一个事实来源(url 参数)并将其传递到您需要的任何地方会容易得多。如果没有在useParams 响应中提供它,我不明白你会在哪里需要它。你能举一个你不能使用useParams的例子吗?

标签: reactjs react-router-dom use-context


【解决方案1】:

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

  • 尝试从 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 处于全局状态,可以通过选择手动设置它,而且当它出现在相关 URL 参数中时,它也可以自动同步。

【讨论】:

  • 这看起来很棒。但是,通过在 App.js 文件中添加它,这不会在每次位置更改时引入额外的页面重新渲染(假设 competitionFromParams !== undefined 并且我们运行 setCompetition
  • 是的,这是真的。这不是优化的,它只是为了演示一般机制,但例如,您可以通过将相关竞争数据值从 params 添加到 useEffect 依赖项来进一步优化,因此状态不仅会在位置更改时更新但是当实际比赛数据也发生变化时。我猜这种优化是否必要取决于您的应用和性能要求。
猜你喜欢
  • 2019-08-10
  • 2018-09-04
  • 2020-12-08
  • 1970-01-01
  • 2020-01-25
  • 2018-01-30
  • 2020-06-02
  • 2012-06-01
  • 1970-01-01
相关资源
最近更新 更多