当路由参数改变时,组件不会重新挂载

124

我正在使用react-router开发一个React应用程序。我有一个项目页面,其url如下:

myapplication.com/project/unique-project-id

当项目组件加载时,我从componentDidMount事件触发该项目的数据请求。现在我遇到了一个问题,如果我直接在两个项目之间切换,只更改id,就会出现以下问题...

myapplication.com/project/982378632
myapplication.com/project/782387223
myapplication.com/project/198731289

componentDidMount不会再次触发,因此数据不会刷新。是否有其他生命周期事件可用于触发数据请求,或者有不同的策略可以解决这个问题?


1
你能发布你的组件代码吗? - Pierre Criulanscy
1
好的文章可能与问题相关 https://www.nikgraf.com/blog/using-reacts-key-attribute-to-remount-a-component - Parixit
14个回答

123
如果您确实需要在路由更改时重新挂载组件,您可以将唯一的键传递给组件的键属性(该键与您的路径/路由相关联)。因此,每次路由更改时,键也会更改,这将触发React组件的卸载/重新挂载。我从 这个答案中得到了这个想法。

4
太棒了!谢谢你提供这个简单而优雅的答案。 - Z_z_Z
只是问一下,这不会影响应用程序的性能吗? - Alpit Anand
1
@AlpitAnand 这是一个比较宽泛的问题。重新挂载肯定比重新渲染慢,因为它会触发更多的生命周期方法。在这里,我只回答如何在路由更改时进行重新挂载。 - wei
但这不会有太大的影响吧?你的建议是我们可以安全地使用它而不会有太多的影响吗? - Alpit Anand
1
@AlpitAnand,你的问题太宽泛且应用程序特定。对于某些应用程序而言,是的,这将会影响性能,在这种情况下,您应该自己监视道具变化并仅更新应该更新的元素。但对于大多数情况而言,上述方法是一个很好的解决方案。 - Chris
这个不起作用,会产生无限的渲染循环和错误,也许对于非常轻量级的组件可以工作,但这不是我的情况。 - stackdave

107

这是我的答案,类似于上面的一些答案但包含代码。

<Route path="/page/:pageid" render={(props) => (
  <Page key={props.match.params.pageid} {...props} />)
} />

7
这里关键的是"key"(键),因为假设你在个人资料页面上有一个链接,点击它只会重定向到相同的路由,但参数不同。但组件不会更新,因为React找不到差异,这是因为必须要有一个“key”来比较旧的结果。 - Georgi Peev
这是一个很棒的答案 - 让我摆脱了componentDidUpdate地狱 ;) - tonysepia
最佳答案是只有在更改:pageid参数时才更新组件。 - Cássio Lacerda

90

如果链接只是指向相同路由但参数不同,那么组件不会被重新加载,而是接收到新的props。因此,您可以使用componentWillReceiveProps(newProps)函数并查找newProps.params.projectId

如果你尝试加载数据,我建议在路由处理匹配之前使用组件上的静态方法来获取数据。可以查看这个例子:React Router Mega Demo。这样,组件将加载数据,并在路由参数更改时自动更新,而无需依赖于componentWillReceiveProps


2
谢谢。我使用componentWillReceiveProps成功地让它工作了。我仍然不太理解React Router Mega Demo的数据流。我看到一些组件上定义了静态fetchData。这些静态方法是如何从路由器客户端被调用的呢? - Constellates
1
很好的问题。请查看client.js文件和server.js文件。在路由器的运行周期中,它们调用fetchData.js文件并将“state”传递给它。一开始可能有点困惑,但后来会变得更加清晰。 - BradByte
13
了解: "componentWillReceiveProps" 被视为遗留的。 - Idan Dagan
5
在 React 16 中,由于 componentWillReceiveProps 已被弃用,因此您应该使用 ComponentDidUpdate。 - Pato Loco
1
这个方法可以工作,但是它存在一个问题,就是你需要为每个加载数据的组件添加componentWillReceiveProps()或componentDidUpdate()来处理重新渲染。例如,想象一下一个页面有几个组件,它们根据相同的路径参数加载数据。 - Juha Syrjälä
显示剩余3条评论

15

请明确,路由变化不会导致页面刷新,您必须自己处理。

import theThingsYouNeed from './whereYouFindThem'

export default class Project extends React.Component {

    componentWillMount() {

        this.state = {
            id: this.props.router.params.id
        }

        // fire action to update redux project store
        this.props.dispatch(fetchProject(this.props.router.params.id))
    }

    componentDidUpdate(prevProps, prevState) {
         /**
         * this is the initial render
         * without a previous prop change
         */
        if(prevProps == undefined) {
            return false
        }

        /**
         * new Project in town ?
         */
        if (this.state.id != this.props.router.params.id) {
           this.props.dispatch(fetchProject(this.props.router.params.id))
           this.setState({id: this.props.router.params.id})
        }

    }

    render() { <Project .../> }
}

谢谢,这个解决方案对我很有用。但是我的eslint设置正在抱怨这个:https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-did-update-set-state.md 你对此有什么看法? - Joshua
是的,那其实不是最佳实践,对此很抱歉。在调度“fetchProject”之后,您的reducer将无论如何更新props,我只是用这个来证明我的观点,而没有使用redux。 - chickenchilli
嗨,chickenchilli, 我还卡在这个问题上...你有其他建议或推荐的教程吗?或者我现在就坚持你的解决方案。感谢你的帮助! - Joshua

15

如果你有:

<Route
   render={(props) => <Component {...props} />}
   path="/project/:projectId/"
/>

在 React 16.8 及以上版本中,使用 Hooks,你可以进行以下操作:

import React, { useEffect } from "react";
const Component = (props) => {
  useEffect(() => {
    props.fetchResource();
  }, [props.match.params.projectId]);

  return (<div>Layout</div>);
}
export default Component;

在这里,只有当props.match.params.id发生改变时才会触发一个新的fetchResource调用。


4
鉴于其他答案大多依赖已被弃用且不安全的componentWillReceiveProps,因此这是更好的答案。 - pjb

10
基于@wei,@Breakpoint25和@PaulusLimma的回答,我制作了这个替代组件用于<Route>。当URL更改时,它将重新装载页面,强制所有页面中的组件都被创建和装载,而不仅仅是重新渲染。所有componentDidMount()和其他启动钩子也在URL更改时执行。
实现的思路是当URL更改时更改组件的key属性,从而强制React重新装载组件。
您可以像这样将其用作<Route>的即插即用替代方案:
<Router>
  <Switch>
    <RemountingRoute path="/item/:id" exact={true} component={ItemPage} />
    <RemountingRoute path="/stuff/:id" exact={true} component={StuffPage} />
  </Switch>
</Router>

<RemountingRoute>组件的定义如下:

export const RemountingRoute = (props) => {
  const {component, ...other} = props
  const Component = component
  return (
    <Route {...other} render={p => <Component key={p.location.pathname + p.location.search}
                                              history={p.history}
                                              location={p.location}
                                              match={p.match} />}
    />)
}

RemountingRoute.propsType = {
  component: PropTypes.object.isRequired
}

这已经在React-Router 4.3中进行了测试。


7
@wei的回答很好,但在某些情况下,我发现不设置内部组件的键,而是路由本身更好。此外,如果组件的路径是静态的,但您希望每次用户导航到它时(可能在componentDidMount()中进行api调用),都会重新加载组件,则将location.pathname设置为路由的键非常方便。通过这样做,当位置改变时,路由及其所有内容都会被重新加载。
const MainContent = ({location}) => (
    <Switch>
        <Route exact path='/projects' component={Tasks} key={location.pathname}/>
        <Route exact path='/tasks' component={Projects} key={location.pathname}/>
    </Switch>
);

export default withRouter(MainContent)

key 直接添加到 <Route /> 更简单,也适用于使用 Typescript 的 React,无需额外的代码(因为其他类似的修复需要在类型上进行一些额外的工作)。 - Raghav Manikandan

5
这是我解决问题的方法:
该方法从API中获取单个项目:
loadConstruction( id ) {
    axios.get('/construction/' + id)
      .then( construction => {
        this.setState({ construction: construction.data })
      })
      .catch( error => {
        console.log('error: ', error);
      })
  }

我在componentDidMount中调用这个方法,这个方法只会在我第一次加载这个路由时被调用:

componentDidMount() {   
    const id = this.props.match.params.id;
    this.loadConstruction( id )
  }

当我们第二次加载相同路由但不同ID时,将会调用componentWillReceiveProps方法。我会先调用第一个方法来重新加载状态,然后组件将加载新的项目。

componentWillReceiveProps(nextProps) {
    if (nextProps.match.params.id !== this.props.match.params.id) {
      const id = nextProps.match.params.id
      this.loadConstruction( id );
    }
  }

这个方法可以工作,但是它存在一个问题,就是你需要为每个加载数据的组件添加componentWillReceiveProps()来处理重新渲染。 - Juha Syrjälä
使用componentDidUpdate(prevProps)替代componentWillUpdate()和componentWillReceiveProps()已经被弃用。 - Mirek Michalak

3

React Router v6+

您需要指定一个键。在React Router早期的版本中,您可以执行以下操作:

<Route path="/project/:pid"
       render={(props) => (
           <Page key={props.match.params.pid} {...props} />)}
/>

从 React Router v6 开始,<Route> 不再支持使用 render,解决方案之一是创建一个辅助性组件:

const PageProxy = (props) =>
{
  const { pid } = useParams();
  return <Page key={pid} {...props} />;
}

你的<Route>变得非常简单:

<Route path="/project/:pid" element={<PageProxy />} />

应该是<Route path="/project/:pid" children={<PageProxy />} />,我找不到有关元素属性的文档。 - Fabio Delarias

2

您可以使用提供的方法:

useEffect(() => {
  // fetch something
}, [props.match.params.id])

当组件保证在路由更改后重新渲染时,您可以将props作为依赖项传递。

这不是最好的方法,但根据Kent C Dodds的说法,已经足够好处理您想要的内容:更多考虑您想要发生的事情。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接