React router Link не вызывает обновление компонента внутри вложенных маршрутов
Это сводит меня с ума. Когда я пытаюсь использовать React Router Link во вложенном маршруте, ссылка обновляется в браузере, но вид не меняется. Однако, если я обновляю страницу до ссылки, она делает это. Так или иначе, компонент не обновляется, когда он должен (или, по крайней мере, что цель).
Вот как выглядят мои ссылки (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>
Хакерное решение заключается в том, чтобы manaully вызвать 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. Но он старый, и я не использую чистую визуализацию mixin.
Вот мои соответствующие маршруты:
<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-адресов, а затем запустить мой аймакс-вызов/просмотр там, но похоже, что это не нужно.
EDIT (больше соответствующего кода):
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>
Ответы
Ответ 1
Не удалось дойти до конца, но я смог достичь своих целей с помощью ComponentWillRecieveProps:
componentWillReceiveProps(nextProps){
if (nextProps.params.slug !== this.props.params.slug) {
const {dispatch, params} = nextProps;
PortfolioDetail.readyOnActions(dispatch, params, true);
}
}
Другими словами, по какой-либо причине, когда я использую React Router Link для ссылки на страницу с SAME PARENT COMPONENT, она не запускает компонент componentWillUnMount/componentWillMount. Поэтому мне приходится вручную запускать мои действия. Он работает, как я ожидаю, всякий раз, когда я связываюсь с Маршрутами с другим родительским компонентом.
Возможно, это так же конструировано, но оно не кажется правильным и не интуитивным. Я заметил, что в Stackoverflow есть много похожих вопросов о том, что Link меняет 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>
);
Ответ 3
Я застрял на этом также в 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, вы можете проверить, было ли состояние обновлено, если у вас нет возможности вызвать метод, который устанавливает ваше состояние, это должно повторно отобразить ваш компонент.
Ответ 4
componentWillReceiveProps - это ответ на этот вопрос, но это немного раздражает. Я написал концепцию BaseController, которая устанавливает действие состояния при изменении маршрута EVEN, хотя компонент маршрута тот же. Итак, представьте, что ваши маршруты выглядят так:
<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
}
}
Затем вы можете наследовать его:
import React from "реагировать"; импортировать 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>)
}
}
Ответ 5
Я не уверен, исправляет ли это исходную проблему, но у меня была похожая проблема, которая была решена путем передачи функции callback () => this.forceUpdate()
вместо this.forceUpdate
.
Поскольку никто больше не упоминает об этом, я вижу, что вы используете onClick={this.forceUpdate}
и пытаетесь использовать onClick={() => this.forceUpdate()}
.