I'm trying to load component asynchronicity with appropriate component base on the ReactCSSTransitionGroup package by this example: https://codesandbox.io/s/zkqlq2vo?from-embed
So I combined all to one component as the following:
import React, { Component } from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import './PageShell.less';
const PageShell = (importComponent) => {
return class extends Component {
state = {
component: null
}
componentDidMount() {
importComponent()
.then(cmd => {
this.setState({ component: cmd.default });
})
.catch({});
}
render() {
const C = this.state.component;
const component = C ? (<C {...this.props} />) : null;
return (
<ReactCSSTransitionGroup
transitionAppear={true}
transitionAppearTimeout={600}
transitionEnterTimeout={600}
transitionLeaveTimeout={200}
transitionName={`Slide${Math.random() >= 0.5 ? 'In' : 'Out'}`}>
{component}
</ReactCSSTransitionGroup>
);
}
};
};
export default PageShell;
And on my App.js:
import React, { Component } from 'react';
import Layout from './hoc/Layout/Layout';
import BurgerBuilder from './containers/BurgerBuilder/BurgerBuilder';
import Logout from './containers/Auth/Logout/Logout';
import AsyncComponent from './hoc/AsyncComponent/AsyncComponent';
const asyncCheckout = AsyncComponent(() => {
return import('./containers/Checkout/Checkout/Checkout');
});
const asyncOrders = AsyncComponent(() => {
return import('./containers/Orders/Orders');
});
const asyncAuth = AsyncComponent(() => {
return import('./containers/Auth/Auth/Auth');
});
class App extends Component {
render() {
let routes = (
<Switch>
<Route path="/auth" component={PageShell(asyncAuth)} />
<Route path="/" exact component={PageShell(BurgerBuilder)} />
<Redirect to="/" />
</Switch>
);
return (
<div>
<Layout>
{routes}
</Layout>
</div>
);
}
}
export default App;
For some reason that i don't understand, the transition in my upgraded PageShell component not working, like in the example from codesandbox, and i can't figure out why is that.
Related
I'm using react router v4 and using the render prop to load up a settings component that has a dynamic input (a value prop based on the state with an onChange handler). When I load the component without using react router, typing into the input field is dynamic, and changes state as you type. But when I use react router, each character press re-renders the entire settings component, causing the input field to lose focus. Not sure why this is happening, since I'm using the render prop instead of the component prop on the <Route /> component. Any help would be appreciated!
My App Component:
import React, { Component, Fragment } from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import Home from "../Home/Home";
import Header from "../Header/Header";
import AppSettings from "../AppSettings/AppSettings";
import NotFound from "../NotFound/NotFound";
import { secsToMs, minsToMs, msToTime } from "../../helpers";
import "./App.css";
class App extends Component {
state = {
settings: {
time: {
break: minsToMs(5),
relax: minsToMs(15),
work: minsToMs(25)
},
trackLength: 2,
autoplay: true
},
defaultSettings: {
time: {
break: minsToMs(5),
relax: minsToMs(15),
work: minsToMs(25)
},
trackLength: 4,
autoplay: false
},
time: minsToMs(25),
totalTime: minsToMs(25),
timerPlaying: false,
track: {
tasksCompleted: 0,
breaksCompleted: 0,
timerName: "work"
}
};
updateSettings = (key, updatedSetting) => {
let settings = { ...this.state.settings };
settings.time[key] = updatedSetting;
this.setState({ settings });
};
//...other App methods
render() {
const MainAppContent = ({ location }) => (
<Fragment>
<Header track={this.state.track} location={location} />
<Home
timerPlaying={this.state.timerPlaying}
totalTime={this.state.totalTime}
time={this.state.time}
track={this.state.track}
trackLength={this.state.settings.trackLength}
startTimer={this.startTimer}
pauseTimer={this.pauseTimer}
resetTimer={this.resetTimer}
skipTimer={this.skipTimer}
/>
<AppSettings
settings={this.state.settings}
updateSettings={this.updateSettings}
restoreDefaultSettings={this.restoreDefaultSettings}
/>
</Fragment>
);
const SettingsAppContent = ({ location }) => (
<Fragment>
<Header track={this.state.track} location={location} />
<AppSettings
settings={this.state.settings}
updateSettings={this.updateSettings}
restoreDefaultSettings={this.restoreDefaultSettings}
/>
</Fragment>
);
return (
<main className="App">
<BrowserRouter>
<Switch>
<Route exact path="/" component={MainAppContent} />
<Route
path="/settings"
render={props => <SettingsAppContent {...props} />}
/>
<Route component={NotFound} />
</Switch>
</BrowserRouter>
</main>
);
}
}
export default App;
My AppSettings Component:
import React, { Component, Fragment } from "react";
import RangeSlider from "../RangeSlider/RangeSlider";
import { minsToMs } from "../../helpers";
import "./AppSettings.css";
class Settings extends Component {
render() {
return (
<Fragment>
<h1>Settings</h1>
{Object.keys(this.props.settings.time).map(key => (
<RangeSlider
name={key}
key={key}
time={this.props.settings.time[key]}
updateSettings={this.props.updateSettings}
/>
))}
<button onClick={this.props.restoreDefaultSettings}>
Revert to Default
</button>
</Fragment>
);
}
}
export default Settings;
My Input Component:
import React, { Component } from "react";
import { msToTime, minsToMs } from "../../helpers";
import "./RangeSlider.css";
class RangeSlider extends Component {
onSettingsChange = e => {
let rangeValue = parseInt(e.currentTarget.value);
if (rangeValue > 60) {
rangeValue = 60;
} else if (rangeValue < 1 || rangeValue === NaN) {
rangeValue = 1;
}
let rangeValueMs = minsToMs(rangeValue);
let key = e.currentTarget.name;
let updatedSetting = rangeValueMs;
const updatedSettings = {
...this.props.settings,
[key]: rangeValueMs
};
console.log("updatedSettings", updatedSettings);
this.props.updateSettings(key, updatedSetting);
};
render() {
const { name, time } = this.props;
return (
<div>
<input
type="number"
min="1"
max="60"
value={msToTime(time).m}
className="text-box"
name={name}
onChange={this.onSettingsChange}
/>
</div>
);
}
}
export default RangeSlider;
I am sorry for my stupid question but i am really new in react and this problem make me stuck for days. I am kinda confused to make a login page in reactjs. my app.js code is like this :
import React from 'react';
import {HashRouter as Router, Route} from 'react-router-dom';
import asyncComponent from './AsyncComponent';
import AppShell from './AppShell';
import Login from './login/Login';
const Dashboard = asyncComponent(() => {
return import(/* webpackChunkName: "dashboard" */ './dashboard/Dashboard')
.then(module => module.default);
});
const LoginPage = asyncComponent(() => {
return import(/* webpackChunkName: "login" */ './login/Login')
.then(module => module.default);
});
class App extends React.Component {
render() {
return (
<Router>
<AppShell>
<div>
<Route exact path="/" component={Dashboard} />
<Route path="/login" component={LoginPage} />
</div>
</AppShell>
</Router>
);
}
}
export default App;
And this is my AppShell code :
import React, {Component} from 'react';
import {Link} from 'react-router-dom';
import {MuiThemeProvider} from 'material-ui/styles';
import {AppBar, Drawer, MenuItem} from 'material-ui';
import {DashboardIcon} from './icon/Icons';
import ArrowDropRight from 'material-ui/svg-icons/navigation-arrow-drop-right';
const ContentStyle = {
width: '90%',
margin: 'auto',
marginTop: '30px'
};
class SidebarDrawer extends React.Component {
componentDidMount() {
let frameCount = 0;
const open = () => (frameCount++ > 0) ? this.props.onMounted() :
requestAnimationFrame(open);
requestAnimationFrame(open);
}
render() {
return (
<Drawer
docked={false}
width={200}
open={this.props.open}
onRequestChange={this.props.onRequestChange}
>
<MenuItem
primaryText={'Dashboard'}
leftIcon={<DashboardIcon/>}
containerElement={<Link to={'/'}/>}
onClick={this.props.onClick}
/>
</Drawer>
);
}
}
class AppShell extends Component {
constructor(props) {
super(props);
this.state = {
open: false,
drawer : false
};
}
handleDrawerToggle = (e) => {
if (!this.state.drawer) {
this.setState({drawer: true});
e.preventDefault();
} else {
this.setState({open: !this.state.open});
}
}
render() {
const LazySidebarDrawer = this.state.drawer && (<SidebarDrawer
open={this.state.open}
onMounted={() => this.setState({open: true})}
onClick={() => this.setState({open: false})}
onRequestChange={open => this.setState({open: open})}
/>)
return (
<MuiThemeProvider>
<div>
<AppBar
title="Dashboard"
iconClassNameRight="muidocs-icon-navigation-expand-more"
onLeftIconButtonTouchTap={this.handleDrawerToggle}
/>
{LazySidebarDrawer}
<div id="content" style={ContentStyle}>
{React.cloneElement(this.props.children)}
</div>
</div>
</MuiThemeProvider>
);
}
};
export default AppShell;
But i still can access dashboard when i open login page. How is the correct pattern for login page?
Thanks
Your routing is correct, the exact '/' will only render the Dashboard component when the path is '/'. What you're seeing is the dashboard drawer or AppBar component. The dashboard drawer is still there in the login screen because it's always there in the AppShell code and your routes are children of AppShell. A potential solution would be to move that AppBar component to your Dashboard component if you only want it there.
In a React app I wrote a function in file1.js and use this function in file2.js
// file1.js
export const withPrefix = (Component) => (props) => (
<PrefixContext.Consumer>
{prefix => <Component {...props} prefix={prefix}/>}
</PrefixContext.Consumer>
)
// file2.js
import { withPrefix } from '/path/to/file1.js'
let Toolbar = withPrefix(({prefix}) => ( // !error happens here
<Fragment>
<div style={{flexGrow: 1}}>
<Button><Link to={`${prefix}/create`}>New Artifact</Link></Button>
</div>
<Search style={{width: 200}}/>
</Fragment>
))
Then I got the error "TypeError: Object(...) is not a function". So I changed export withPrefix function
export function withPrefix(Component) {
return (props) => (
<PrefixContext.Consumer>
{prefix => <Component {...props} prefix={prefix}/>}
</PrefixContext.Consumer>
)
}
And the error is gone, everything works. But I wonder why these two exports result differently?
And another question is if I want to export an arrow function in es6, is the 2nd export function style the only method?
Attachment 1 (DefaultView.js):
import React, {Component} from 'react'
import {Layout} from 'antd'
import Toolbar from './Toolbar'
import Content from './Content'
export const PrefixContext = React.createContext()
export function withPrefix(Component) {
return (props) => (
<PrefixContext.Consumer>
{prefix => <Component {...props} prefix={prefix}/>}
</PrefixContext.Consumer>
)
}
export default class DefaultView extends Component {
constructor(props) {
super(props)
this.state = {
view: props.defaultView
}
}
handleViewChange = (view) => {
this.setState({view})
}
render() {
const {prefix, views} = this.props
const {view} = this.state
return (
<PrefixContext.Provider value={prefix}>
<Layout>
<Toolbar view={view} views={views} onViewChange=
{this.handleViewChange}/>
<hr/>
<Content view={view}/>
</Layout>
</PrefixContext.Provider>
)
}
}
Attachment 2 (Summary.js)
import React, {Component, Fragment} from 'react'
import {Button, Input} from 'antd'
import {Link} from 'react-router-dom'
import ArtifactTable from './ArtifactTable'
import {withPrefix} from "./DefaultView"
const {Search} = Input
export const Toolbar = withPrefix(({prefix}) => (
<Fragment>
<div style={{flexGrow: 1}}>
<Button><Link to={`${prefix}/create`}>新建软件包</Link></Button>
</div>
<Search style={{width: 200}}/>
</Fragment>
))
class Summary extends Component {
state = {
data: []
}
componentDidMount() {
const {prefix} = this.props
console.log('prefix=' + prefix)
fetch(prefix).then(json => {
this.setState({data: json.content})
})
}
render() {
const {data} = this.state
return (
<div>
<ArtifactTable data={data}/>
</div>
)
}
}
export default withPrefix(Summary)
Attachment 3 (Toolbar.js)
import React from 'react'
import {Switch, Route} from 'react-router-dom'
import {Toolbar as SummaryToolbar} from './Summary'
import Create from './Create'
import Details from './Details'
import Edit from './Edit'
import {withPrefix} from "./DefaultView"
const Toolbar = withPrefix(({prefix, view, onViewChange}) => (
<div style={{background: '#fff', padding: 16, display: 'flex',
alignItems: 'baseline'}}>
<Switch>
<Route exact path={`${prefix}`} component={SummaryToolbar}/>
<Route exact path={`${prefix}/create`}
component={() => <Create.Toolbar view={view} onViewChange=
{onViewChange}/>}/>
<Route exact path={`${prefix}/:id`}
component={() => <Details.Toolbar view={view} onViewChange=
{onViewChange}/>}/>
<Route exact path={`${prefix}/:id/edit`}
component={() => <Edit.Toolbar view={view} onViewChange=
{onViewChange}/>}/>
</Switch>
</div>
))
export default Toolbar
Update It's indeed the cyclic dependency problem as #Bergi and #loganfsmyth said. After I moved out
the withPrefix export snippet into a new file Context.js from DefaultView.js, the problem resolved. But I still have one quesion. In a cyclic dependency circumstances, why export const f = () => () => {} different from export function f() => { return () => {} }. Is export const lazy evaluated than export function as #loganfsmyth said?
I want to change rendered component place each time the route (url) changes.
e.g. I have 3 blocks: Home, Works, Contacts. When url is site.com/home the content renders in Home block, when url is site.com/works the content moves to Works block and so on.
I did a kind of what I want but it renders the whole page when It seems more optimal to just moves new content.
So can you suggest better decisions?
The whole project you can get and run locally from here: https://github.com/g1un/reactjs-site
What it looks like (buggy regarding routing) you can see here: http://g1un.ru/reactjs/
I paste the main files below.
index.js
import React from 'react';
import { render } from "react-dom";
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import style from './../scss/style.scss';
import { Header } from './components/Header';
import { About } from './components/About';
import { Works } from './components/Works';
import { Contacts } from './components/Contacts';
import { NotFound } from './components/NotFound';
class App extends React.Component {
render() {
return (
<Router>
<div className="container">
<Switch>
<Route exact path="/" component={About}/>
<Route exact path="/works" component={Works}/>
<Route exact path="/contacts" component={Contacts}/>
<Route component={NotFound}/>
</Switch>
</div>
</Router>
);
}
}
render(<App />, window.document.getElementById('app'));
About.js (Works.js, Contacts.js are similar)
import React from 'react';
import DocumentTitle from 'react-document-title';
import { Header } from './Header';
export class About extends React.Component {
render() {
return (
<DocumentTitle title='About'>
<Header currentPath={this.props.location.pathname}>
<h1>
About
</h1>
</Header>
</DocumentTitle>
);
}
}
Header.js
import React from 'react';
const PATHS = ['/', '/works', '/contacts'];
const PAGES = ['About', 'Works', 'Contacts'];
import { HeaderItem } from './HeaderItem';
export class Header extends React.Component {
constructor(props) {
super();
this.currentPath = props.currentPath;
this.content = props.children;
this.paths = PATHS;
this.pages = PAGES;
}
render() {
return (
<header className="header">
<nav className="nav">
<div className="nav__list">
{this.paths.map((path, i) => {
return <HeaderItem key={i} currentPath={path} currentPage={this.pages[i]} pageContent={path === this.currentPath ? this.content : ''}/>;
})}
</div>
</nav>
</header>
);
}
}
HeaderItem.js
import React from 'react';
import { NavLink } from 'react-router-dom';
export class HeaderItem extends React.Component {
render() {
return (
<div className={"nav__item " + (this.props.pageContent ? "_active" : "")}>
<NavLink className="nav__link" exact activeClassName="_active" to={this.props.currentPath}>
{this.props.currentPage}
</NavLink>
{this.props.pageContent ? <div className="nav__content content">{this.props.pageContent}</div> : ''}
</div>
);
}
}
I've found the desicion myself.
The fact is that I can't use any blocks inside <Switch/>, so I've simply used <Route>s without <Switch/>, putting them in any blocks I need.
And for '404' page I've created <Switch/> block with same Routes inside without component attributes except one for '404' page.
Now in each HeaderItem I can control whether its content is visible or not.
New index.js:
import React from 'react';
import { render } from "react-dom";
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import DocumentTitle from 'react-document-title';
import style from './../scss/style.scss';
import { Header } from './components/Header';
import { HeaderItem } from './components/HeaderItem';
import { About } from './components/About';
import { Works } from './components/Works';
import { Contacts } from './components/Contacts';
import { NotFound } from './components/NotFound';
const PATHS = ['/', '/works', '/contacts'];
const PAGES = ['About', 'Works', 'Contacts'];
const COMPONENTS = [About, Works, Contacts];
class App extends React.Component {
constructor() {
super();
this.paths = PATHS;
this.pages = PAGES;
this.components = COMPONENTS;
this.state = {
documentTitle: this.getDocumentTitle()
};
}
updateDocumentTitle(pageTitle) {
if(this.state.documentTitle === pageTitle) return;
this.setState({
documentTitle: this.getDocumentTitle()
});
}
getDocumentTitle() {
let pathIndex = this.paths.indexOf(window.location.pathname);
if(pathIndex === -1) {
return 'Not found';
} else {
return this.pages[pathIndex];
}
}
render() {
return (
<DocumentTitle title={this.state.documentTitle}>
<Router>
<div className="container">
<Switch>
{this.paths.map((path, i) => {
return <Route key={i} exact path={path}/>;
})}
<Route render={() => <NotFound text="Error 404"/>}/>
</Switch>
<Header>
{this.paths.map((path, i) => {
return (
<HeaderItem
key={i}
routePath={path}
pageTitle={this.pages[i]}
updateDocumentTitle={this.updateDocumentTitle.bind(this)}
>
<Route exact path={path} component={this.components[i]}/>
</HeaderItem>
);
})}
</Header>
<Switch>
{this.paths.map((path, i) => {
return <Route key={i} exact path={path}/>;
})}
<Route render={() => <NotFound text="Page not found"/>}/>
</Switch>
</div>
</Router>
</DocumentTitle>
);
}
}
render(<App />, window.document.getElementById('app'));
New HeaderItem.js:
import React from 'react';
import { NavLink } from 'react-router-dom';
import { Content } from './Content';
import { About } from './About';
import { Works } from './Works';
import { Contacts } from './Contacts';
const COMPONENTS = [About, Works, Contacts];
const PAGES = ['About', 'Works', 'Contacts'];
export class HeaderItem extends React.Component {
constructor(props) {
super();
this.path = props.routePath;
this.content = props.children;
this.page = props.pageTitle;
this.components = COMPONENTS;
this.pages = PAGES;
this.index = this.pages.indexOf(this.page);
this.updateDocumentTitle = props.updateDocumentTitle;
this.state = {
isActive: this.path === window.location.pathname
};
}
componentWillUpdate() {
//to change page title if this component is active
if(this.path === window.location.pathname) {
this.updateDocumentTitle(this.page);
}
this.saveRouteComponent();
}
isActive() {
return this.path === window.location.pathname;
}
saveRouteComponent() {
if(this.state.isActive || this.path !== window.location.pathname) return;
//once opened route get 'isActive' state and its content will not removed when this route is deactivated
this.setState({ isActive: true });
}
render() {
return (
<div className={"nav__item " + (this.isActive() ? "_active" : "")}>
<div className="nav__item-wrapper">
<NavLink className="nav__link" exact activeClassName="_active" to={this.path}>
{this.page}
</NavLink>
</div>
{(this.isActive() || this.state.isActive) ? <div className="nav__content"><Content pageTitle={this.page}>{this.state.isActive ? React.createElement(this.components[this.index]) : this.content}</Content></div> : ''}
</div>
);
}
}
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;