I am trying to build a single-page application using React. Currently I have a ponent called App
that is being rendered by ReactDOM
, it contains navigation, then the ponent (or page) to be rendered after the navigation ponent.
import React from 'react';
import Navigation from './Navigation';
import NavigationLink from './NavigationLink';
import Home from './Home';
import About from './About';
const App = () => (
<div>
<Navigation>
<NavigationLink onClick={ ... }>Home</NavigationLink>
<NavigationLink onClick={ ... }>About</NavigationLink>
</Navigation>
<Home />
</div>
);
export default App;
I want to be able to select the "About" link and the Home
ponent update to be About
. I am looking for solutions to this issue that will work with more than two elements (or pages).
This is my current code that works, though is a very poor solution.
import React, { Component } from 'react';
import {
NavItem,
NavLink,
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
} from 'reactstrap';
import firebase from '../../utils/firebase';
import Navigation from '../../ponents/Navigation/Navigation';
import Login from '../Login/Login';
import Profile from '../Profile/Profile';
import Home from '../Home/Home';
class App extends Component {
state = {
currentShowingPage: 0,
currentlyLoggedInUser: null,
isLoginModalOpen: false,
};
ponentDidMount = () => {
firebase.auth().onAuthStateChanged((currentUser) => {
if (currentUser) {
this.setState({
currentlyLoggedInUser: currentUser,
isLoginModalOpen: false,
});
} else {
this.setState({
currentlyLoggedInUser: null,
});
}
});
};
handleLoginModalToggle = () => {
this.setState((previousState) => ({
isLoginModalOpen: !previousState.isLoginModalOpen,
}));
};
render = () => {
let currentShowingPageComponent;
const {
isLoginModalOpen,
currentlyLoggedInUser,
currentShowingPage,
} = this.state;
if (currentShowingPage === 0) {
currentShowingPageComponent = <Home />;
} else if (currentShowingPage === 1) {
currentShowingPageComponent = <Profile />;
}
return (
<div>
<Login
isModalOpen={isLoginModalOpen}
modalToggler={this.handleLoginModalToggle}
/>
<Navigation>
{currentlyLoggedInUser ? (
<UncontrolledDropdown nav>
<DropdownToggle nav caret>
{currentlyLoggedInUser.email}
</DropdownToggle>
<DropdownMenu right>
<DropdownItem
onClick={() =>
this.setState({
currentShowingPage: 1,
})
}
>
Profile
</DropdownItem>
<DropdownItem disabled>Expeditions</DropdownItem>
<DropdownItem divider />
<DropdownItem onClick={() => firebase.auth().signOut()}>
Sign Out
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
) : (
<NavItem>
<NavLink onClick={this.handleLoginModalToggle}>Login</NavLink>
</NavItem>
)}
</Navigation>
{currentShowingPageComponent}
</div>
);
};
}
export default App;
I am trying to build a single-page application using React. Currently I have a ponent called App
that is being rendered by ReactDOM
, it contains navigation, then the ponent (or page) to be rendered after the navigation ponent.
import React from 'react';
import Navigation from './Navigation';
import NavigationLink from './NavigationLink';
import Home from './Home';
import About from './About';
const App = () => (
<div>
<Navigation>
<NavigationLink onClick={ ... }>Home</NavigationLink>
<NavigationLink onClick={ ... }>About</NavigationLink>
</Navigation>
<Home />
</div>
);
export default App;
I want to be able to select the "About" link and the Home
ponent update to be About
. I am looking for solutions to this issue that will work with more than two elements (or pages).
This is my current code that works, though is a very poor solution.
import React, { Component } from 'react';
import {
NavItem,
NavLink,
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
} from 'reactstrap';
import firebase from '../../utils/firebase';
import Navigation from '../../ponents/Navigation/Navigation';
import Login from '../Login/Login';
import Profile from '../Profile/Profile';
import Home from '../Home/Home';
class App extends Component {
state = {
currentShowingPage: 0,
currentlyLoggedInUser: null,
isLoginModalOpen: false,
};
ponentDidMount = () => {
firebase.auth().onAuthStateChanged((currentUser) => {
if (currentUser) {
this.setState({
currentlyLoggedInUser: currentUser,
isLoginModalOpen: false,
});
} else {
this.setState({
currentlyLoggedInUser: null,
});
}
});
};
handleLoginModalToggle = () => {
this.setState((previousState) => ({
isLoginModalOpen: !previousState.isLoginModalOpen,
}));
};
render = () => {
let currentShowingPageComponent;
const {
isLoginModalOpen,
currentlyLoggedInUser,
currentShowingPage,
} = this.state;
if (currentShowingPage === 0) {
currentShowingPageComponent = <Home />;
} else if (currentShowingPage === 1) {
currentShowingPageComponent = <Profile />;
}
return (
<div>
<Login
isModalOpen={isLoginModalOpen}
modalToggler={this.handleLoginModalToggle}
/>
<Navigation>
{currentlyLoggedInUser ? (
<UncontrolledDropdown nav>
<DropdownToggle nav caret>
{currentlyLoggedInUser.email}
</DropdownToggle>
<DropdownMenu right>
<DropdownItem
onClick={() =>
this.setState({
currentShowingPage: 1,
})
}
>
Profile
</DropdownItem>
<DropdownItem disabled>Expeditions</DropdownItem>
<DropdownItem divider />
<DropdownItem onClick={() => firebase.auth().signOut()}>
Sign Out
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
) : (
<NavItem>
<NavLink onClick={this.handleLoginModalToggle}>Login</NavLink>
</NavItem>
)}
</Navigation>
{currentShowingPageComponent}
</div>
);
};
}
export default App;
Share
Improve this question
edited Jul 18, 2018 at 23:10
asked Jul 18, 2018 at 23:03
user7885981user7885981
4
- I don't quite understand what you want to do. Do you want the clicked link to be moved to the start of the link list? Could you include the code of what you have tried so far? – Tholle Commented Jul 18, 2018 at 23:08
- @Tholle Sure, it has been added. – user7885981 Commented Jul 18, 2018 at 23:11
- Thanks. Have you looked into something like React Router? I think your solution is good, but it might be difficult to scale up to more pages and nested pages. – Tholle Commented Jul 18, 2018 at 23:14
- 1 No, I'll take a look tomorrow. Thanks. – user7885981 Commented Jul 18, 2018 at 23:16
3 Answers
Reset to default 11This is just a really simple example of switching out ponents, there are many many ways to do it, and I'm sure you will get better practice answers but hopefully, this gives you some ideas. (in case it is not obvious, you would use this by calling setState({currentComponent: 'pX'}))
getComponent(){
let ponent;
switch (this.state.currentComponent){
case 'pA' :
ponent = <CompA/>;
break;
case 'pB' :
ponent = <CompB/>;
break;
case 'pC' :
ponent = <CompC/>;
break;
case 'pD' :
ponent = <CompD/>;
break;
}
return ponent;
}
render(){
return(
<div>
{this.getComponent()}
</div>
);
}
(Shameless plug ing)
I wrote a blog post about dynamically loading React ponents.
https://www.slightedgecoder./2017/12/03/loading-react-ponents-dynamically-demand/#case1
Refer to the 1st case where you load a ponent dynamically using dynamic import().
The gist is that you create a ponent
as a state, and update it depending on the type (name of module in my blog) passed to addComponent
.
The benefit of this approach is that, browser won't download ponent that's not needed.
In your case, if nobody clicks on About
page, that ponent is never downloaded.
addComponent = async type => {
console.log(`Loading ${type} ponent...`);
import(`./ponents/${type}.js`)
.then(ponent =>
this.setState({
ponents: this.state.ponents.concat(ponent.default)
})
)
.catch(error => {
console.error(`"${type}" not yet supported`);
});
};
TLDR: inside the render() method where the main body of your app is built, dynamically insert the React ponent that coincides with the current ponent/module/page that you are trying to display to the user.
You can absolutely use React Router. It's well-established and widely-used. But you can totally do this with without React Router if you wish. I'm also building a single-page app and I'm also swapping out ponents as you've described. Here are the two main files that acplish that:
template.default.js:
// lots o' imports up here...
// styles/themes that are needed to support the drawer template
class DefaultTemplate extends React.Component {
constructor(props) {
super(props);
this.state = {
mainHeight : 200,
mobileOpen : false,
toolbarSpacerHeight : 100,
};
session[the.session.key.for.DefaultTemplate] = this;
session.browser = detect();
}
getModule() {
// here I'm switching on a session variable (which is available throughout the entire app to determine which module ID the user has currently chosen
// notice that the return values are the dynamic React ponents that coincide with the currently-chosen module ID
switch (session.DisplayLayer.state.moduleId) {
case the.module.id.for.home:
return <HomeModule/>;
case the.module.id.for.lists:
return <ListsModule/>;
case the.module.id.for.login:
return <LogInModule/>;
case the.module.id.for.logout:
return <LogOutModule/>;
case the.module.id.for.register:
return <RegisterModule/>;
case the.module.id.for.roles:
return <RolesModule/>;
case the.module.id.for.teams:
return <TeamsModule/>;
case the.module.id.for.users:
return <UsersModule/>;
default:
return null;
}
}
handleDrawerToggle = () => {
this.setState({mobileOpen : !this.state.mobileOpen});
};
render() {
// the module is dynamically generated every time a render() is invoked on this template module
const module = this.getModule();
return (
<div className={classes.root} style={{height : the.style.of.percent.hundred}}>
<AppBar className={classes.appBar} style={{backgroundColor : the.color.for.appBar}}>
<Toolbar>
<IconButton
aria-label={the.ariaLabel.openDrawer}
className={classes.navIconHide}
color={the.style.of.inherit}
onClick={this.handleDrawerToggle}
>
<MenuIcon/>
</IconButton>
<FontAwesome name={the.icon.for.palette} style={{marginRight : '10px', fontSize : the.style.of.onePointFiveEms}}/>
<Typography variant={the.variant.of.title} color={the.style.of.inherit} noWrap>
<TranslatedText english={'Groupware'}/>.<TranslatedText english={'Studio'}/>
</Typography>
<LanguageMenu
containerStyle={{marginLeft : the.style.of.margin.auto}}
onClose={event => {this.updateLanguage(event)}}
selectedLanguageId={db.getItem(the.db.item.for.languageId)}
/>
</Toolbar>
</AppBar>
<Hidden mdUp>
<Drawer
anchor={theme.direction === the.direction.of.rightToLeft ? the.direction.of.right : the.direction.of.left}
classes={{paper : classes.drawerPaper}}
ModalProps={{keepMounted : true}}
onClose={this.handleDrawerToggle}
open={this.state.mobileOpen}
variant={the.variant.of.temporary}
>
{drawer}
</Drawer>
</Hidden>
<Hidden smDown implementation={the.implementation.of.css}>
<Drawer
classes={{paper : classes.drawerPaper}}
open
variant={the.variant.of.permanent}
>
{drawer}
</Drawer>
</Hidden>
<main
className={classes.content}
ref={main => this.main = main}
style={{backgroundColor : the.color.for.module.background}}
>
<div
className={classes.toolbar}
ref={toolbarSpacer => this.toolbarSpacer = toolbarSpacer}
/>
{/*
here is where that dynamically-generated module is rendered inside the template
*/}
{module}
</main>
</div>
);
}
}
export default withStyles(styles, {withTheme : true})(DefaultTemplate);
And navigation.left.js:
// lots o' imports up here
class LeftNavigation extends React.Component {
listButtons = [];
// this object controls the configuration of the nav links that show on the left side of the template
navigation = {
isLoggedIn : [
{
icon : the.icon.for.home,
isFollowedByDivider : false,
label : the.label.for.home,
moduleId : the.module.id.for.home,
},
{
icon : the.icon.for.powerOff,
isFollowedByDivider : true,
label : the.label.for.logOut,
moduleId : the.module.id.for.logout,
},
{
icon : the.icon.for.orderedList,
isFollowedByDivider : false,
label : the.label.for.lists,
moduleId : the.module.id.for.lists,
},
{
icon : the.icon.for.roles,
isFollowedByDivider : false,
label : the.label.for.roles,
moduleId : the.module.id.for.roles,
},
{
icon : the.icon.for.teams,
isFollowedByDivider : false,
label : the.label.for.teams,
moduleId : the.module.id.for.teams,
},
{
icon : the.icon.for.users,
isFollowedByDivider : false,
label : the.label.for.users,
moduleId : the.module.id.for.users,
},
],
isLoggedOut : [
{
icon : the.icon.for.home,
isFollowedByDivider : false,
label : the.label.for.home,
moduleId : the.module.id.for.home,
},
{
icon : the.icon.for.powerOff,
isFollowedByDivider : false,
label : the.label.for.logIn,
moduleId : the.module.id.for.login,
},
{
icon : the.icon.for.registered,
isFollowedByDivider : false,
label : the.label.for.register,
moduleId : the.module.id.for.register,
},
],
};
populateListButtons() {
// here we are generating an array of ListButtons that will prise the left-hand navigation
this.listButtons = [];
let buttonConfigs = [];
switch (db.getItem(the.db.item.for.isLoggedIn)) {
case true:
buttonConfigs = this.navigation.isLoggedIn;
break;
case false:
buttonConfigs = this.navigation.isLoggedOut;
break;
default:
return;
}
buttonConfigs.forEach(buttonConfig => {
let buttonIsEnabled = true;
let fontAwesomeStyle = {fontSize : the.style.of.onePointFiveEms};
let listItemStyle = {};
let textStyle = {};
switch (buttonConfig.label) {
case the.label.for.logIn:
fontAwesomeStyle[the.style.property.name.of.color] = the.color.for.success;
break;
case the.label.for.logOut:
fontAwesomeStyle[the.style.property.name.of.color] = the.color.for.error;
break;
default:
if (session.DisplayLayer.state.moduleId === buttonConfig.moduleId) {
fontAwesomeStyle[the.style.property.name.of.color] = the.color.for.white.text;
} else {
fontAwesomeStyle[the.style.property.name.of.color] = the.color.for.headerBar;
}
break;
}
if (session.DisplayLayer.state.moduleId === buttonConfig.moduleId) {
buttonIsEnabled = false;
listItemStyle[the.style.property.name.of.backgroundColor] = the.color.for.selectedLeftNavButtonOrange;
textStyle[the.style.property.name.of.color] = the.color.for.white.text;
}
this.listButtons.push(
<ListItem
button={buttonIsEnabled}
key={`${buttonConfig.label}-listItem`}
// notice that when one of the left nav links is clicked, we are updating the moduleId value in session,
// which dynamically determines which module shows up in the center panel
onClick={() => session.DisplayLayer.updateModuleId(buttonConfig.moduleId)}
style={listItemStyle}
>
<ListItemIcon>
<FontAwesome name={buttonConfig.icon} style={fontAwesomeStyle}/>
</ListItemIcon>
<TranslatedText english={buttonConfig.label} style={textStyle}/>
</ListItem>,
);
if (buttonConfig.isFollowedByDivider) {
this.listButtons.push(<Divider key={`${buttonConfig.label}-divider`}/>);
}
});
}
render() {
// dynamically generate the array of left nav buttons before rendering the links
this.populateListButtons();
return <List style={{paddingTop : the.style.of.pixels.zero}}>{this.listButtons}</List>;
}
}
export default LeftNavigation;