【问题标题】:React router Link not causing component to update within nested routes反应路由器链接不会导致组件在嵌套路由中更新
【发布时间】:2016-12-13 02:15:52
【问题描述】:

这快把我逼疯了。当我尝试在嵌套路由中使用 React Router 的链接时,链接会在浏览器中更新,但视图不会改变。然而,如果我将页面刷新到链接,它确实如此。不知何故,组件没有在应该更新的时候更新(或者至少这是目标)。

这是我的链接的样子(prev/next-item 真的是 vars):

<Link to={'/portfolio/previous-item'}>
    <button className="button button-xs">Previous</button>
</Link>
<Link to={'/portfolio/next-item'}>
    <button className="button button-xs">Next</button>
</Link>

一个 hacky 解决方案是手动调用 forceUpate() ,例如:

<Link onClick={this.forceUpdate} to={'/portfolio/next-item'}>
    <button className="button button-xs">Next</button>
</Link>

这可行,但会导致整页刷新,这是我不想要的并且出现错误:

ReactComponent.js:85 Uncaught TypeError: Cannot read property 'enqueueForceUpdate' of undefined

我已经到处搜索了答案,我能找到的最接近的答案是:https://github.com/reactjs/react-router/issues/880。但它很旧,我没有使用纯渲染混合。

这是我的相关路线:

<Route component={App}>
    <Route path='/' component={Home}>
        <Route path="/index:hashRoute" component={Home} />
    </Route>
    <Route path="/portfolio" component={PortfolioDetail} >
        <Route path="/portfolio/:slug" component={PortfolioItemDetail} />
    </Route>
    <Route path="*" component={NoMatch} />
</Route>

无论出于何种原因,调用 Link 都不会导致组件重新挂载,这需要发生以获取新视图的内容。它确实调用了componentDidUpdate,我确信我可以检查一个url slug更改,然后在那里触发我的ajax调用/视图更新,但似乎不需要这样做。

编辑(更多相关代码):

PortfolioDetail.js

import React, {Component} from 'react';
import { browserHistory } from 'react-router'
import {connect} from 'react-redux';
import Loader from '../components/common/loader';
import PortfolioItemDetail from '../components/portfolio-detail/portfolioItemDetail';
import * as portfolioActions  from '../actions/portfolio';

export default class PortfolioDetail extends Component {

    static readyOnActions(dispatch, params) {
        // this action fires when rendering on the server then again with each componentDidMount. 
        // but not firing with Link...
        return Promise.all([
            dispatch(portfolioActions.fetchPortfolioDetailIfNeeded(params.slug))
        ]);
    }

    componentDidMount() {
        // react-router Link is not causing this event to fire
        const {dispatch, params} = this.props;
        PortfolioDetail.readyOnActions(dispatch, params);
    }

    componentWillUnmount() {
        // react-router Link is not causing this event to fire
        this.props.dispatch(portfolioActions.resetPortfolioDetail());
    }

    renderPortfolioItemDetail(browserHistory) {
        const {DetailReadyState, item} = this.props.portfolio;
        if (DetailReadyState === 'WORK_DETAIL_FETCHING') {
            return <Loader />;
        } else if (DetailReadyState === 'WORK_DETAIL_FETCHED') {
            return <PortfolioItemDetail />; // used to have this as this.props.children when the route was nested
        } else if (DetailReadyState === 'WORK_DETAIL_FETCH_FAILED') {
            browserHistory.push('/not-found');
        }
    }

    render() {
        return (
            <div id="interior-page">
                {this.renderPortfolioItemDetail(browserHistory)}
            </div>
        );
    }
}

function mapStateToProps(state) {
    return {
        portfolio: state.portfolio
    };
}
function mapDispatchToProps(dispatch) {
    return {
        dispatch: dispatch
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(PortfolioDetail);

PortfolioItemDetail.js

import React, {Component} from 'react';
import {connect} from 'react-redux';
import Gallery from './gallery';

export default class PortfolioItemDetail extends React.Component {

    makeGallery(gallery) {
        if (gallery) {
            return gallery
                .split('|')
                .map((image, i) => {
                    return <li key={i}><img src={'/images/portfolio/' + image} alt="" /></li>
            })
        }
    }

    render() {
        const { item } = this.props.portfolio;

        return (
            <div className="portfolio-detail container-fluid">
                <Gallery
                    makeGallery={this.makeGallery.bind(this)}
                    item={item}
                />
            </div>
        );
    }
}

function mapStateToProps(state) {
    return {
        portfolio: state.portfolio
    };
}

export default connect(mapStateToProps)(PortfolioItemDetail);

gallery.js

import React, { Component } from 'react';
import { Link } from 'react-router';

const Gallery = (props) => {

    const {gallery, prev, next} = props.item;
    const prevButton = prev ? <Link to={'/portfolio/' + prev}><button className="button button-xs">Previous</button></Link> : '';
    const nextButton = next ? <Link to={'/portfolio/' + next}><button className="button button-xs">Next</button></Link> : '';

    return (
        <div>
            <ul className="gallery">
                {props.makeGallery(gallery)}
            </ul>
            <div className="next-prev-btns">
                {prevButton}
                {nextButton}
            </div>
        </div>
    );
};

export default Gallery;

新路线,基于 Anoop 的建议:

<Route component={App}>
    <Route path='/' component={Home}>
        <Route path="/index:hashRoute" component={Home} />
    </Route>
    <Route path="/portfolio/:slug" component={PortfolioDetail} />
    <Route path="*" component={NoMatch} />
</Route>

【问题讨论】:

    标签: reactjs react-router


    【解决方案1】:

    无法深入了解,但我能够通过 ComponentWillRecieveProps 实现我的目标:

    componentWillReceiveProps(nextProps){
        if (nextProps.params.slug !== this.props.params.slug) {
            const {dispatch, params} = nextProps;
            PortfolioDetail.readyOnActions(dispatch, params, true);
        }
    }
    

    换句话说,无论出于何种原因,当我使用 React Router Link 链接到具有相同父组件的页面时,它都不会触发 componentWillUnMount/componentWillMount。所以我不得不手动触发我的动作。每当我使用不同的父组件链接到 Routes 时,它确实可以正常工作。

    也许这是设计的,但它看起来不正确且不直观。我注意到 Stackoverflow 上有很多类似的问题,关于链接更改 url 但不更新页面,所以我不是唯一一个。如果有人对此有任何见解,我仍然很想听听!

    【讨论】:

      【解决方案2】:

      分享组件代码也很好。但是,我尝试在本地重新创建相同的内容,并且对我来说效果很好。下面是示例代码,

      import { Route, Link } from 'react-router';
      import React from 'react';
      import App from '../components/App';
      
      const Home = ({ children }) => (
        <div>
          Hello There Team!!!
          {children}
        </div>
      );
      
      const PortfolioDetail = () => (
        <div>
          <Link to={'/portfolio/previous-item'}>
            <button className="button button-xs">Previous</button>
          </Link>
          <Link to={'/portfolio/next-item'}>
            <button className="button button-xs">Next</button>
          </Link>
        </div>
      );
      
      const PortfolioItemDetail = () => (
        <div>PortfolioItemDetail</div>
      );
      
      const NoMatch = () => (
        <div>404</div>
      );
      
      module.exports = (
        <Route path="/" component={Home}>
          <Route path='/' component={Home}>
              <Route path="/index:hashRoute" component={Home} />
          </Route>
          <Route path="/portfolio" component={PortfolioDetail} />
          <Route path="/portfolio/:slug" component={PortfolioItemDetail} />
          <Route path="*" component={NoMatch} />
        </Route>
      );
      

      【讨论】:

      • 完全同意,所以我添加了组件代码的相关部分。感谢您提供的工作示例,但我不确定它是否符合我的需求,因为我希望在更改为下一个/上一个投资组合项目时不必重新呈现页眉/页脚。尽管如此,本着让它工作的精神,我在没有嵌套的情况下尝试了它,类似于你所做的(但没有 /portfolio 路由并且针对父组件而不是 Item 详细信息)。它仍然像以前一样工作,但我仍然没有运气让 Link 重新安装组件。
      • 值得一提的是,这是一款通用应用。当页面从非 /portfolio 路由(如主页)链接到时,它工作正常,但我似乎无法在没有刷新的情况下链接投资组合页面。现场示例在这里:jasongallagher.org
      • 你发现了吗?我遇到了同样的问题。是因为组件有自己的“路线”吗?什么会导致它们触发和更新?
      • 不是真的,除了下面的解决方法。任何状态更改都应该触发更新,但由于某种原因,Link 并不总是这样做。在某些时候,我可能会切换到 React Router Redux,看看是否可以在我使用 Redux 时修复它。
      【解决方案3】:

      componentWillReceiveProps 是这个问题的答案,但它有点烦人。我写了一个 BaseController “概念”,它在路由更改时设置状态操作,即使路由的组件是相同的。所以想象你的路线是这样的:

      <Route path="test" name="test" component={TestController} />
      <Route path="test/edit(/:id)" name="test" component={TestController} />
      <Route path="test/anything" name="test" component={TestController} />
      

      那么 BaseController 会检查路由更新:

      import React from "react";
      
      /**
       * conceptual experiment
       * to adapt a controller/action sort of approach
       */
      export default class BaseController extends React.Component {
      
      
          /**
           * setState function as a call back to be set from
           * every inheriting instance
           *
           * @param setStateCallback
           */
          init(setStateCallback) {
              this.setStateCall = setStateCallback
              this.setStateCall({action: this.getActionFromPath(this.props.location.pathname)})
          }
      
          componentWillReceiveProps(nextProps) {
      
              if (nextProps.location.pathname != this.props.location.pathname) {
                  this.setStateCall({action: this.getActionFromPath(nextProps.location.pathname)})
              }
          }
      
          getActionFromPath(path) {
      
              let split = path.split('/')
              if(split.length == 3 && split[2].length > 0) {
                  return split[2]
              } else {
                  return 'index'
              }
      
          }
      
          render() {
              return null
          }
      
      }
      

      然后你可以从那个继承:

      从“反应”导入反应; 从'./BaseController'导入BaseController

      export default class TestController extends BaseController {
      
      
          componentWillMount() {
              /**
               * convention is to call init to
               * pass the setState function
               */
              this.init(this.setState)
          }
      
          componentDidUpdate(){
              /**
               * state change due to route change
               */
              console.log(this.state)
          }
      
      
          getContent(){
      
              switch(this.state.action) {
      
                  case 'index':
                      return <span> Index action </span>
                  case 'anything':
                      return <span>Anything action route</span>
                  case 'edit':
                      return <span>Edit action route</span>
                  default:
                      return <span>404 I guess</span>
      
              }
      
          }
      
          render() {
      
              return (<div>
                          <h1>Test page</h1>
                          <p>
                              {this.getContent()}
                          </p>
                  </div>)
              }
      
      }
      

      【讨论】:

        【解决方案4】:

        我在 React 16 中也遇到了这个问题。

        我的解决方法如下:

        componentWillMount() {
            const { id } = this.props.match.params;
            this.props.fetchCategory(id); // Fetch data and set state
        }
        
        componentWillReceiveProps(nextProps) {
            const { id } = nextProps.match.params;
            const { category } = nextProps;
        
            if(!category) {
                this.props.fetchCategory(id); // Fetch data and set state
            }
        }
        

        我正在使用 redux 来管理状态,但我认为这个概念是一样的。

        在 WillMount 方法上按正常设置状态,当调用 WillReceiveProps 时,您可以检查状态是否已更新,如果尚未更新,您可以调用设置状态的方法,这应该重新渲染您的组件.

        【讨论】:

        • 请注意,componentWillReceiveProps 从 16 开始已被弃用,并将在 17 中完全删除。
        【解决方案5】:

        我不确定它是否解决了原来的问题,但我遇到了类似的问题,通过传入函数回调 () =&gt; this.forceUpdate() 而不是 this.forceUpdate 解决了。

        由于没有其他人提及它,我看到您正在使用onClick={this.forceUpdate},并会尝试onClick={() =&gt; this.forceUpdate()}

        【讨论】:

          【解决方案6】:

          尝试导入 BrowserRouter 而不是 Router

          import { Switch, Route, BrowserRouter as Router } from 'react-router-dom;
          

          花了几个小时解决了这个问题后,它对我有用。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-03-24
            • 2021-08-11
            • 2021-07-29
            • 2019-02-18
            • 2019-04-06
            • 2016-08-18
            • 1970-01-01
            • 2019-04-16
            相关资源
            最近更新 更多