React-Router v4 渲染错误的组件但正确匹配
我有一个带有两个按钮的侧边栏,测试"和关于".测试(火箭图标)在/test"处呈现,About(主页图标)在/"处呈现.
它们都位于应用程序的根目录并嵌套在一个组件中.
当我从 '/' 开始并单击 Link to="/test" 时,它总是加载 'About' 组件,当我检查 'About' 的 componentDidMount 的道具时,匹配对象包含匹配数据/测试".
只有当我刷新时,它才会再次呈现正确的组件测试".知道为什么会这样吗?
AppRoutes.js:
导出类 AppRoutes 扩展 React.Component {使成为() {返回 (<开关><路线确切路径="/"渲染={(matchProps) =>(<LazyLoad getComponent={() =>import('pages/appPages/About')} {...matchProps}/>)}/><路线路径="/登录"渲染={(matchProps) =>(<LazyLoad getComponent={() =>import('pages/appPages/Login')} {...matchProps}/>)}/><路线路径="/注册"渲染={(matchProps) =>(<LazyLoad getComponent={() =>import('pages/appPages/Register')} {...matchProps}/>)}/><路线路径="/测试"渲染={(matchProps) =>(<LazyLoad getComponent={() =>import('pages/appPages/Test')} {...matchProps}/>)}/>...
关于Page.js &&TestPage.js(除组件名外重复):
从'react'导入反应;从容器/SidebarContainer"导入 SidebarContainer;从样式/SidebarPageLayout"导入 SidebarPageLayout;export const About = (props) =>{console.log('关于加载:', props);返回 (<侧边栏页面布局><SidebarContainer/><div>关于</div></SidebarPageLayout>);}导出默认关于;
SidebarContainer.js:
从'react'导入反应;从 'prop-types' 导入 PropTypes;从'lodash'导入_;从侧边栏/侧边栏"导入侧边栏;从侧边栏/汉堡按钮"导入 HamburgerButton;从'sidebar/AboutButton'导入AboutButton;从侧边栏/配置文件按钮"导入配置文件按钮;从'sidebar/TestButton'导入TestButton;导出类 SidebarContainer 扩展 React.Component {构造函数(道具){超级(道具);这个.state = {sidebarIsOpen:假,侧边栏元素:[],};}组件DidMount() {如果(!this.props.authenticated){这个.setState({sidebarElements: _.concat(this.state.sidebarElements, HamburgerButton, ProfileButton, AboutButton, TestButton),});}}toggleSidebarIsOpenState = () =>{this.setState({ sidebarIsOpen: !this.state.sidebarIsOpen });}使成为() {const { 已验证,sidebarIsOpen,sidebarElements} = this.state;返回 (<侧边栏认证={认证}sidebarIsOpen={sidebarIsOpen}sidebarElements={_.isEmpty(sidebarElements) ?未定义:侧边栏元素}toggleSidebarIsOpenState={this.toggleSidebarIsOpenState}/></div>);}}SidebarContainer.propTypes = {认证:PropTypes.bool,};导出默认 SidebarContainer;
Sidebar.js:
从'react'导入反应;从'lodash'导入_;从 'prop-types' 导入 PropTypes从 '../styles/SidebarStyles' 导入 SidebarStyles;export const Sidebar = (props) =>{if (props && props.sidebarElements) {返回 (<SidebarStyles sidebarIsOpen={props.sidebarIsOpen}>{_.map(props.sidebarElements, (value, index) => {返回 React.createElement(价值,{键:索引,已认证:props.authenticated,sidebarIsOpen: props.sidebarIsOpen,toggleSidebarIsOpenState: props.toggleSidebarIsOpenState,},);})}</侧边栏样式>);}返回 (<div></div>);}Sidebar.propTypes = {认证:PropTypes.bool,sidebarIsOpen: PropTypes.bool,sidebarElements:PropTypes.array,toggleSidebarIsOpenState: PropTypes.func,};导出默认侧边栏;
TestButton.js:
从'react'导入反应;从 'prop-types' 导入 PropTypes;从'react-fontawesome'导入图标;进口 {关联} 来自 'react-router-dom';export const TestButton = (props) =>{返回 (<链接到="/test"><图标名称='火箭'大小='2x'/></链接>);}导出默认TestButton;
关于Button.js:
从'react'导入反应;从 'prop-types' 导入 PropTypes;从'react-fontawesome'导入图标;进口 {关联} 来自 'react-router-dom';export const AboutButton = (props) =>{返回 (<链接到="/"><图标名称='home' 大小='2x'/></链接>);}导出默认 AboutButton;
没有刷新,只是不断地从/"路径点击/test"路径:
刷新后:
根组件:
store.js:
导入{创建商店,应用中间件,撰写,} 来自'redux';从'redux-saga'导入createSagaMiddleware;从'./rootReducers'导入{rootReducer};从'./rootSagas'导入{rootSaga};//传奇const sagaMiddleware = createSagaMiddleware();//开发工具const composeEnhancers = typeof window === 'object' &&(窗口.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?(窗口.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) : 撰写);导出功能 configureStore() {常量中间件 = [saga中间件,];常量存储 = 创建存储(根减速器,{},composeEnhancers(applyMiddleware(...middlewares)));sagaMiddleware.run(rootSaga);退货商店;}导出常量存储 = configureStore();
index.js(根):
从'react'导入反应;从'react-redux'导入{提供者};从 'react-dom' 导入 ReactDOM;从 'react-router-dom' 导入 { BrowserRouter };从'./store'导入{商店};从容器/AppContainer"导入 AppContainer;ReactDOM.render(<提供者商店={商店}><浏览器路由器><应用容器/></浏览器路由器></提供者>,document.getElementById('root'));
应用容器:
从'react'导入反应;从 'react-router-dom' 导入 { withRouter };从'react-redux'导入{连接};从'./actions'导入{注销,验证令牌};导入 { selectAuthenticated, selectAuthenticating } from './selectors';从 'routes/AppRoutes' 导入 AppRoutes;导出类 AppContainer 扩展 React.Component {构造函数(道具){超级(道具);this.state = { 已加载:假 };}组件DidMount() {常量令牌 = localStorage.getItem('jwt');如果(令牌){this.props.verifyToken(token, () => this.setState({ loaded: true }));} 别的 {this.setState({ 加载: true });}}使成为() {如果(this.state.loaded){返回 (<AppRoutes认证={this.props.authenticated}身份验证={this.props.authenticating}注销={this.props.logout}/>);} 别的 {返回 <div>加载中...</div>}}}功能 mapStateToProps(状态){返回 {认证:选择认证(状态),身份验证:选择身份验证(状态),};}函数mapDispatchToProps(调度){返回 {verifyToken: (token = '', callback = false) =>调度(验证令牌(令牌,回调)),注销:()=>调度(注销()),};}导出默认与Router(连接(mapStateToProps,mapDispatchToProps)(AppContainer));
为 LazyLoad 编辑 2:
服务/LazyLoad/index.js:
从'react'导入反应;导出类 LazyLoad 扩展 React.Component {构造函数(道具){超级(道具);这个.state = {异步模块:空,};}组件DidMount() {this.props.getComponent()//getComponent={() =>导入('./someFile.js')}.then(module => module.default).then(AsyncModule => this.setState({AsyncModule}))}使成为() {const { loader, ...childProps } = this.props;常量 { AsyncModule } = this.state;如果(异步模块){返回 <AsyncModule {...childProps}/>;}如果(加载器){常量加载器 = 加载器;返回<加载器/>;}返回空值;}}导出默认的 LazyLoad;
解决方案 您的问题在于 LazyLoad
组件.对于 "/" 或 "test" 路径,AppRoutes
组件最终呈现的是 LazyLoad
组件.因为 Route
和 Switch
只是有条件地渲染他们的孩子.但是,React 无法区分/" LazyLoad
组件和/test" LazyLoad
组件.所以它第一次渲染 LazyLoad
组件并调用 componentDidMount
.但是当路由发生变化时,React 将其视为先前渲染的 LazyLoad
组件的 prop 更改.所以它只是用新的道具调用前一个 LazyLoad
组件的 componentWillReceiveProps
,而不是卸载前一个并安装一个新的.这就是为什么它会持续显示关于组件直到刷新页面.
为了解决这个问题,如果 getComponent
属性发生了变化,我们必须在 componentWillReceiveProps
中使用新的 getComponent
加载新模块.所以我们可以修改 LazyLoad
如下,它有一个通用的方法来加载模块,并从 componentDidMount
和 componentWillReceiveProps
使用正确的 props 调用它.
从'react'导入反应;导出类 LazyLoad 扩展 React.Component {构造函数(道具){超级(道具);这个.state = {异步模块:空,};}组件DidMount() {this.load(this.props);}加载(道具){this.setState({AsyncModule: null}props.getComponent()//getComponent={() =>导入('./someFile.js')}.then(module => module.default).then(AsyncModule => this.setState({AsyncModule}))}组件WillReceiveProps(nextProps) {if (nextProps.getComponent !== this.props.getComponent) {this.load(nextProps)}}使成为() {const { loader, ...childProps } = this.props;常量 { AsyncModule } = this.state;如果(异步模块){返回 <AsyncModule {...childProps}/>;}如果(加载器){常量加载器 = 加载器;返回<加载器/>;}返回空值;}}导出默认的 LazyLoad;
I've got a sidebar with two buttons, 'test' and 'about'. Test (rocket icon) is rendered at '/test', and About (home icon) is rendered at '/'.
They're both located at the root of the app and are nested within a component.
When I start at '/' and click the Link to="/test" it always loads the 'About' component, and when I check the props for the componentDidMount of 'About', the match object contains match data for "/test".
Only when I refresh does it render the proper component, 'Test', again. Any idea why this is happening?
AppRoutes.js:
export class AppRoutes extends React.Component {
render() {
return (
<div>
<Switch>
<Route
exact path="/"
render={(matchProps) => (
<LazyLoad getComponent={() => import('pages/appPages/About')} {...matchProps} />
)}
/>
<Route
path="/login"
render={(matchProps) => (
<LazyLoad getComponent={() => import('pages/appPages/Login')} {...matchProps} />
)}
/>
<Route
path="/register"
render={(matchProps) => (
<LazyLoad getComponent={() => import('pages/appPages/Register')} {...matchProps} />
)}
/>
<Route
path="/test"
render={(matchProps) => (
<LazyLoad getComponent={() => import('pages/appPages/Test')} {...matchProps} />
)}
/>
...
AboutPage.js && TestPage.js (duplicates except for component name):
import React from 'react';
import SidebarContainer from 'containers/SidebarContainer';
import SidebarPageLayout from 'styles/SidebarPageLayout';
export const About = (props) => {
console.log('About Loading: ', props);
return (
<SidebarPageLayout>
<SidebarContainer />
<div>About</div>
</SidebarPageLayout>
);
}
export default About;
SidebarContainer.js:
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import Sidebar from 'sidebar/Sidebar';
import HamburgerButton from 'sidebar/HamburgerButton';
import AboutButton from 'sidebar/AboutButton';
import ProfileButton from 'sidebar/ProfileButton';
import TestButton from 'sidebar/TestButton';
export class SidebarContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
sidebarIsOpen: false,
sidebarElements: [],
};
}
componentDidMount() {
if (!this.props.authenticated) {
this.setState({
sidebarElements: _.concat(this.state.sidebarElements, HamburgerButton, ProfileButton, AboutButton, TestButton),
});
}
}
toggleSidebarIsOpenState = () => {
this.setState({ sidebarIsOpen: !this.state.sidebarIsOpen });
}
render() {
const { authenticated, sidebarIsOpen, sidebarElements} = this.state;
return (
<div>
<Sidebar
authenticated={authenticated}
sidebarIsOpen={sidebarIsOpen}
sidebarElements={_.isEmpty(sidebarElements) ? undefined: sidebarElements}
toggleSidebarIsOpenState={this.toggleSidebarIsOpenState}
/>
</div>
);
}
}
SidebarContainer.propTypes = {
authenticated: PropTypes.bool,
};
export default SidebarContainer;
Sidebar.js:
import React from 'react';
import _ from 'lodash';
import PropTypes from 'prop-types'
import SidebarStyles from '../styles/SidebarStyles';
export const Sidebar = (props) => {
if (props && props.sidebarElements) {
return (
<SidebarStyles sidebarIsOpen={props.sidebarIsOpen}>
{_.map(props.sidebarElements, (value, index) => {
return React.createElement(
value,
{
key: index,
authenticated: props.authenticated,
sidebarIsOpen: props.sidebarIsOpen,
toggleSidebarIsOpenState: props.toggleSidebarIsOpenState,
},
);
})}
</SidebarStyles>
);
}
return (
<div></div>
);
}
Sidebar.propTypes = {
authenticated: PropTypes.bool,
sidebarIsOpen: PropTypes.bool,
sidebarElements: PropTypes.array,
toggleSidebarIsOpenState: PropTypes.func,
};
export default Sidebar;
TestButton.js:
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'react-fontawesome';
import {
Link
} from 'react-router-dom';
export const TestButton = (props) => {
return (
<Link to="/test">
<Icon name='rocket' size='2x' />
</Link>
);
}
export default TestButton;
AboutButton.js:
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'react-fontawesome';
import {
Link
} from 'react-router-dom';
export const AboutButton = (props) => {
return (
<Link to="/">
<Icon name='home' size='2x' />
</Link>
);
}
export default AboutButton;
No refresh, just constant clicking on the '/test' route from the '/' route:
after refresh:
Edit:
Root components:
Edit:
store.js:
import {
createStore,
applyMiddleware,
compose,
} from 'redux';
import createSagaMiddleware from 'redux-saga';
import { rootReducer } from './rootReducers';
import { rootSaga } from './rootSagas';
// sagas
const sagaMiddleware = createSagaMiddleware();
// dev-tools
const composeEnhancers = typeof window === 'object' && (
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? (
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
) : compose
);
export function configureStore() {
const middlewares = [
sagaMiddleware,
];
const store = createStore(
rootReducer,
{},
composeEnhancers(applyMiddleware(...middlewares))
);
sagaMiddleware.run(rootSaga);
return store;
}
export const store = configureStore();
index.js (root):
import React from 'react';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { store } from './store';
import AppContainer from 'containers/AppContainer';
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<AppContainer />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
AppContainer:
import React from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { logout, verifyToken } from './actions';
import { selectAuthenticated, selectAuthenticating } from './selectors';
import AppRoutes from 'routes/AppRoutes';
export class AppContainer extends React.Component {
constructor(props) {
super(props);
this.state = { loaded: false };
}
componentDidMount() {
const token = localStorage.getItem('jwt');
if (token) {
this.props.verifyToken(token, () => this.setState({ loaded: true }));
} else {
this.setState({ loaded: true });
}
}
render() {
if (this.state.loaded) {
return (
<AppRoutes
authenticated={this.props.authenticated}
authenticating={this.props.authenticating}
logout={this.props.logout}
/>
);
} else {
return <div>Loading ...</div>
}
}
}
function mapStateToProps(state) {
return {
authenticated: selectAuthenticated(state),
authenticating: selectAuthenticating(state),
};
}
function mapDispatchToProps(dispatch) {
return {
verifyToken: (token = '', callback = false) => dispatch(verifyToken(token, callback)),
logout: () => dispatch(logout()),
};
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AppContainer));
Edit 2 for LazyLoad:
services/LazyLoad/index.js:
import React from 'react';
export class LazyLoad extends React.Component {
constructor(props) {
super(props);
this.state = {
AsyncModule: null,
};
}
componentDidMount() {
this.props.getComponent() // getComponent={() => import('./someFile.js')}
.then(module => module.default)
.then(AsyncModule => this.setState({AsyncModule}))
}
render() {
const { loader, ...childProps } = this.props;
const { AsyncModule } = this.state;
if (AsyncModule) {
return <AsyncModule {...childProps} />;
}
if (loader) {
const Loader = loader;
return <Loader />;
}
return null;
}
}
export default LazyLoad;
解决方案
Your problem lies with LazyLoad
component. For both "/" or "test" paths, what AppRoutes
component ultimately renders is a LazyLoad
component. Because Route
and Switch
just conditionally render their children. However, React can't differentiate "/" LazyLoad
component and "/test" LazyLoad
component. So the first time it renders LazyLoad
component and invokes the componentDidMount
. But when route changes, React consider it as a prop change of previously rendered LazyLoad
component. So it just invokes componentWillReceiveProps
of previous LazyLoad
component with new props instead of unmounting previous one and mount a new one. That's why it continuously show About component until refresh the page.
To solve this problem, if the getComponent
prop has changed, we have to load the new module with new getComponent
inside the componentWillReceiveProps
. So we can modify the LazyLoad
as follows which have a common method to load module and invoke it from both componentDidMount
and componentWillReceiveProps
with correct props.
import React from 'react';
export class LazyLoad extends React.Component {
constructor(props) {
super(props);
this.state = {
AsyncModule: null,
};
}
componentDidMount() {
this.load(this.props);
}
load(props){
this.setState({AsyncModule: null}
props.getComponent() // getComponent={() => import('./someFile.js')}
.then(module => module.default)
.then(AsyncModule => this.setState({AsyncModule}))
}
componentWillReceiveProps(nextProps) {
if (nextProps.getComponent !== this.props.getComponent) {
this.load(nextProps)
}
}
render() {
const { loader, ...childProps } = this.props;
const { AsyncModule } = this.state;
if (AsyncModule) {
return <AsyncModule {...childProps} />;
}
if (loader) {
const Loader = loader;
return <Loader />;
}
return null;
}
}
export default LazyLoad;
相关文章