React-Router v4 渲染错误的组件但正确匹配 [英] React-Router v4 rendering wrong component but matching correctly

查看:20
本文介绍了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 组件.因为 RouteSwitch 只是有条件地渲染他们的孩子.但是,React 无法区分/" LazyLoad 组件和/test" LazyLoad 组件.所以它第一次渲染 LazyLoad 组件并调用 componentDidMount.但是当路由发生变化时,React 将其视为先前渲染的 LazyLoad 组件的 prop 更改.所以它只是用新的道具调用前一个 LazyLoad 组件的 componentWillReceiveProps ,而不是卸载前一个并安装一个新的.这就是为什么它会持续显示关于组件直到刷新页面.

为了解决这个问题,如果 getComponent 属性发生了变化,我们必须在 componentWillReceiveProps 中使用新的 getComponent 加载新模块.所以我们可以修改 LazyLoad 如下,它有一个通用的方法来加载模块,并从 componentDidMountcomponentWillReceiveProps 使用正确的 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;

这篇关于React-Router v4 渲染错误的组件但正确匹配的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆