I have these components. I want to turn every into a dynamic url. For example, when accessing in the browser, http://localhost:3000/houses/1 I want to appear the House 1.
The other things in the application are working fine. I just want to solve this problem of implementing dynamic routes.
The data is fetched from a json file
db.json file
[
{
"houseId": 1,
"name": "House 1",
"photos": [
"house1_001.jpg",
"house1_002.jpg",
"house1_003.jpg",
"house1_004.jpg"
]
},
{
"houseId": 2,
"name": "House 2",
"photos": [
"house2_001.jpg",
"house2_002.jpg",
"house2_003.jpg",
"house2_004.jpg"
]
},
{
"houseId": 3,
"name": "House 3",
"photos": [
"house3_001.jpg",
"house3_002.jpg",
"house3_003.jpg",
"house3_004.jpg"
]
}
]
Router Component
import React from 'react';
import { BrowserRouter as Router, Route, NavLink } from 'react-router-dom'
import App from './App'
import Intro from './Intro'
import Houses from './Houses'
import House from './House'
export default props => (
<Router>
<Route exact path='/' render={() => <App />} >
<Route exact path='/intro' render={() => <Intro />} />
<Route exact path='/houses' render={() => <Houses />} />
<Route exact path='/houses/:houseId' render={(props) => <House {...props} />} />
</Route>
</Router>
)
Houses Component
import React, { Component } from 'react'
import House from './House'
var data = require('./db.json');
class Houses extends Component {
constructor(props) {
super(props);
this.state = {
houses: []
};
}
componentDidMount() {
this.setState({
houses: data
})
}
render() {
const { houses } = this.state;
return (
<div className="content house">
{
houses.map((house, index) => {
return (
<div>
<House house={house} />
</div>
)
})
}
</div>
)
}
}
export default Houses
**House Component**
import React, { Component } from 'react';
class House extends Component {
constructor(props) {
super(props)
this.state = {
houseId: ""
}
}
componentDidMount() {
this.setState({
houseId: this.props.match.params.id
})
}
render() {
return (
<div>
<h3>{this.props.house.name}</h3>
<ul>
{this.props.house.photos.map((photo, index) => {
return (
<li><img src={`/images/${photo}`} /></li>
)
})
}
</ul>
</div>
)
}
}
export default House;
House component
import React, { Component } from 'react';
class House extends Component {
constructor(props) {
super(props)
this.state = {
houseId: ""
}
}
componentDidMount() {
this.setState({
houseId: this.props.match.params.id
})
}
render() {
return (
<div>
<h3>{this.props.house.name}</h3>
<ul>
{this.props.house.photos.map((photo, index) => {
return (
<li><img src={`/images/${photo}`} /></li>
)
})
}
</ul>
</div>
)
}
}
export default House;
Pass the json data to <House/> component and use the id to display the correct data.
import React, { Component } from 'react';
const data = require('./db.json');
class House extends Component {
constructor(props) {
super(props)
this.state = {
houses: data,
}
}
render() {
const houseId = this.props.match.params.houseId;
return (
<div>
<h3>{this.state.houses[houseId].name}</h3>
<ul>
{this.state.houses[houseId].photos.map((photo, index) => {
return (
<li><img src={`/images/${photo}`} /></li>
)
})
}
</ul>
</div>
)
}
}
export default House;
Create two components, one will be rendered in Houses and one will be render on house/1
// rendered inside Houses
class House extends Component {
render() {
return (
<div>
<h3>{this.props.house.name}</h3>
<ul>
{this.props.house.photos.map((photo, index) => {
return (
<li><img src={`/images/${photo}`} /></li>
)
})
}
</ul>
</div>
)
}
}
HouseInfo, which display data by query parameter
import React, { Component } from 'react';
const data = require('./db.json');
class HouseInfo extends Component {
constructor(props) {
super(props)
this.state = {
houses: data,
}
}
render() {
const id = this.props.match.params.houseId;
const houseId = id >= 1 ? id - 1 : 0;
return (
<div>
<h3>{this.state.houses[houseId].name}</h3>
<ul>
{this.state.houses[houseId].photos.map((photo, index) => {
return (
<li><img src={`/images/${photo}`} /></li>
)
})
}
</ul>
</div>
)
}
}
export default HouseInfo;
Router
import React from 'react';
import { BrowserRouter as Router, Route, NavLink } from 'react-router-dom'
import App from './App'
import Intro from './Intro'
import Houses from './Houses'
import House from './House'
import HouseInfo from './HouseInfo'
export default props => (
<Router>
<Route exact path='/' render={() => <App />} >
<Route exact path='/intro' render={() => <Intro />} />
<Route exact path='/houses' render={() => <Houses />} />
<Route exact path='/houses/:houseId' render={(props) => <HouseInfo {...props} />} />
</Route>
</Router>
)
Entire snippet is right except the thing is that you have wrongly matched the params id,
change the following code in house component
this.setState({
houseId: this.props.match.params.houseId
})
you have to use the same param id ie.,houseId inside the component
using the houseId in the state ie.,(this.state.houseId) in House component, loop through the json data and find the houseId and display the corresponding data.
I don't see what props you are passing to the House component but my guess is not exactly intended ones. Try this:
import { withRouter } from 'react-router-dom';
...
export default withRouter(Houses);
or without withRouter:
<Route exact path='/houses/:houseId' render={House} />
and in your Route your param value is specified as houseId, as it should be in House component:
this.setState({
houseId: this.props.match.params.houseId
})
Related
I am writing a React app in which somebody can sign up as a business or user, and a user is able to search for a business by name. I do not understand why I am getting an error when trying to render my search component, saying "TypeError: Cannot read properties of undefined (reading 'toLowerCase')". I do not understand why I am getting this error because I believe I am passing in the appropriate data via my reducers and the Redux store. This is my search component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import BusinessCard from '../Business/BusinessCard'
import { Card } from 'semantic-ui-react';
class Businesses extends Component {
state = {newSearch: ""}
handleInputChange = e => {
this.setState({newSearch: e.target.value})
}
render() {
const { businessesReducer} = this.props;
let businessesMatch = businessesReducer.businesses.filter( (business ) => business.name.toLowerCase().includes(this.state.newSearch.toLowerCase()))
return (
<div>
<input placeholder="Search Events and Services Near You" value={this.state.newSearch} name="businessName" type="text" onChange={this.handleInputChange} />
<Card.Group itemsPerRow={3}>
{ businessesMatch.map((business, id) => <BusinessCard key={id} business={business} />)}
</Card.Group>
</div>
)
}
}
const mapStateToProps = (state) => {
return ({
businessesReducer: state.businessesReducer
})
}
export default connect(mapStateToProps)(Businesses);
My businesses reducer:
const initialState =
{
businesses:[],
isLoading: false
}
export default (state = initialState, action) => {
switch (action.type) {
case 'LOADING':
return {
...state,
isLoading: true
}
case "GET_ALL_BUSINESSES_SUCCESS":
return { ...state,
businesses: action.businesses,
isLoading: false
}
default:
return state
}
}
BusinessCard.js (which I am trying to render per the user's search)
import React, { Component } from 'react';
import { Card } from 'semantic-ui-react';
import { connect } from 'react-redux';
class BusinessCard extends Component {
constructor(props) {
super(props);
}
render(){
const { business, businessesReducer } = this.props;
return(
<Card>
<div key={business.id} >
<Card.Content>
<Card.Header><strong>{business.name}</strong></Card.Header>
</Card.Content>
</div>
</Card>
)
}
}
const mapStateToProps = state => {
return {
businesses: state.businesses,
businessesReducer: state.businessesReducer
}
}
export default connect(mapStateToProps)(BusinessCard);
And App.js
import { getAllBusinesses } from './actions/business/business';
import { BrowserRouter as Router, Route, Switch} from 'react-router-dom';
import history from './history';
class App extends React.Component {
componentDidMount() {
this.props.getAllBusinesses();
}
render() {
return (
<Router history={history}>
<div className="App">
<NavBar />
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About} />
<Route path="/services" component={Services} />
<Route path="/shop" component={Shop}/>
<Route path="/login-signup" component={LoginContainer}/>
<Route path="/signup" component={Signup}/>
<Route path="/business-signup" component={BusinessSignup}/>
<Route path="/professional-signup" component={ProfessionalSignup}/>
<Route path="/search" component={Businesses}/>
</Switch>
</div>
</Router>
)
}
}
const mapStateToProps = (state) => {
return {
businessesReducer: state.businessesReducer
}
}
export default connect(mapStateToProps, {getAllBusinesses})(App);
Does anybody have any idea why my search component cannot access "business" and its properties? Everything looks correct to me.
1: It would be good if you could show getAllBusinesses.
2: Please make sure if data exists in your store, you can use redux-dev-tools for that.
3: The first time that your component renders there is no data in your store and it's just an empty array so please first check if name exists and has value then try to convert it to lower case.
It would be something like this:
let businessesMatch = businessesReducer.businesses.filter(
(business) =>
business.name &&
business.name
.toLowerCase()
.includes(this.state.newSearch.toLowerCase())
);
Or if with optional chaining:
let businessesMatch = businessesReducer.businesses.filter((business) =>
business?.name
.toLowerCase()
.includes(this.state.newSearch.toLowerCase())
);
If none of these help please provide more information like a code sandbox.
I'm trying to update my child component's photos from the parent components state. For all the other routes, the appropriate function was already invoked once the app was mounted. The component that renders cats, dogs, or computers is PhotoList.js
But now, I want to be able to enter a parameter after search (ex. /search/:id) and run a function called getImages in my Container.js to search for any type of picture from the Flickr API.
I tried using componentDidMount and invoking the getImages function with the match parameter inside of it but it doesn't seem to change the data props that's put into it. Does anyone have any suggestions as to how I can make this?
Here is Container.js
import React, {Component} from 'react';
import Photo from './Photo';
class Container extends Component {
componentDidMount() {
this.props.getImages(this.props.match.id)
}
render() {
return (
<div className="photo-container">
<h2>Results</h2>
<ul>
{this.props.data.map((photo,index)=>
<Photo
farm={photo.farm}
server={photo.server}
id={photo.id}
secret={photo.secret}
key={index}
/>
)}
</ul>
</div>
);
}
}
export default Container
Here is PhotoList.js
import React, {Component} from 'react';
import Photo from './Photo';
import NoResults from './NoResults';
class PhotoList extends Component {
render() {
return (
<div className="photo-container">
<h2>Results</h2>
<ul>
{this.props.data.map((photo,index)=>
<Photo
farm={photo.farm}
server={photo.server}
id={photo.id}
secret={photo.secret}
key={index}
/>
)}
</ul>
</div>
);
}
}
export default PhotoList;
Here is App.js
import React, {Component} from 'react';
import {
BrowserRouter,
Route,
Switch,
Redirect
} from 'react-router-dom';
import Search from './Search';
import Nav from './Nav';
import '../index.css';
import axios from 'axios';
import apiKey from './Config';
import NotFound from './NotFound';
import PhotoList from './PhotoList';
import NoResults from './NoResults';
import Container from './Container';
class App extends Component {
state= {
cats: [],
dogs: [],
computers: [],
searchResult: [],
loading: true
}
componentDidMount() {
this.getCats()
this.getDogs()
this.getComputers()
}
getCats=(query='cats')=> {
axios.get(`https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=${apiKey}&tags=${query}&per_page=24&page=1&format=json&nojsoncallback=1`)
.then(res=> {
const cats=res.data.photos.photo
this.setState({cats})
}).catch((error)=> {
console.log("There was an error parsing your data", error);
})
}
getDogs=(query='dogs')=> {
axios.get(`https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=${apiKey}&tags=${query}&per_page=24&page=1&format=json&nojsoncallback=1`)
.then(res=> {
const dogs=res.data.photos.photo
this.setState({dogs})
}).catch((error)=> {
console.log("There was an error parsing your data", error);
})
}
getComputers=(query='computers')=> {
axios.get(`https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=${apiKey}&tags=${query}&per_page=24&page=1&format=json&nojsoncallback=1`)
.then(res=> {
const computers=res.data.photos.photo
this.setState({computers});
}).catch((error)=> {
console.log("There was an error parsing your data", error);
})
}
getImages=(query)=> {
axios.get(`https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=${apiKey}&tags=${query}&per_page=24&page=1&format=json&nojsoncallback=1`)
.then (res=> {
const searchResult=res.data.photos.photo
this.setState({searchResult});
}).catch((error)=> {
console.log("There was an error parsing your data", error);
})
}
render() {
return (
<div className="container">
<Search getImages={this.getImages}/>
<Nav />
<Switch>
<Route exact path="/" render={()=> <Redirect to={'/cats'} />} />
<Route path='/cats' render={()=> <PhotoList data={this.state.cats}/>} />
<Route path='/dogs' render={()=> <PhotoList data={this.state.dogs} />} />
<Route exact path='/computers' render={()=> <PhotoList data={this.state.computers} />} />
<Route path='/search/:id' render={(props)=> <Container {...props} getImages={this.getImages} data={this.state.searchResult} />} />
<Route component={NotFound}/>
</Switch>
</div>
)
}
}
export default App;
Assuming your are using react-router-dom 4 and above.
Try
import React, { Component } from "react";
import { withRouter } from "react-router-dom"; //<-- import this
import Photo from "./Photo";
class Container extends Component {
componentDidMount() {
// Your this.props.match.id is likely undefined
this.props.getImages(this.props.match.params.id); // <-- Change here
}
...
}
export default withRouter(Container); // <-- Change this
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 want to know how I can pass a status from one page to another page for if used in the other way.
My first page Body.js (Which I handle the state):
import React from 'react';
import './Body.css';
import axios from 'axios';
import { Link } from "react-router-dom";
import User from './User';
class Body extends React.Component {
constructor (){
super();
this.state ={
employee:[],
employeeCurrent:[],
}
}
componentDidMount(){
axios.get('http://127.0.0.1:3004/employee').then(
response=>this.setState({employee: response.data})
)
}
getName = () => {
const {employee} = this.state;
return employee.map(name=> <Link className='link' to={`/user/${name.name}`}> <div onClick={()=>this.add(name)} key={name.id} className='item'> <img className='img' src={`https://picsum.photos/${name.name}`}></img> <h1 className='name'> {name.name} </h1></div> </Link>)
}
add = (name) => {
const nam = name;
this.state.employeeCurrent.push(nam)
console.log(this.state.employeeCurrent)
}
render(){
return(
<div className='body'>
{this.getName()}
</div>
)
}
}
export default Body;
My second page which I want to get the state called employeeCurrent:
import React from 'react';
import Header from './Header';
import Body from './Body';
class User extends React.Component {
constructor (props){
super(props);
this.props ={
employeeCurrent:[],
}
}
render(){
return(
<div >
{this.props.employeeCurrent}
</div>
)
}
}
export default User;
I'm using the React Router, it looks like this:
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import './App.css';
import Home from './Home';
import User from './User';
const AppRouter = () => (
<Router>
<div className='router'>
<Route exact path="/" component={Home}/>
<Route path="/user/:id" component={User}/>
</div>
</Router>
);
export default AppRouter;
My project is:
Home page, where you have users, obtained from the API, all users have attributes (name, age, city and country). Saved in employeeCurrent variable:
What I want is: grab these attributes from the clicked user and play on the user page:
Someone would can help me PLEASE?????
Like I explained earlier, you need to lift the state up:
AppRouter (holds the state and passes it to children)
class AppRouter extends React.Component {
state = {
employeeCurrent: [],
employee: []
};
componentDidMount() {
axios
.get("http://127.0.0.1:3004/employee")
.then(response => this.setState({ employee: response.data }));
}
add = name => {
this.setState(prevState => {
const copy = prevState.employeeCurrent.slice();
copy.push(name);
return {
employeeCurrent: copy
};
});
};
render() {
return (
<Router>
<div className="router">
<Route
exact
path="/"
render={props => (
<Home
{...props}
add={this.add}
employee={this.state.employee}
currentEmployee={this.state.currentEmployee}
/>
)}
/>
<Route
path="/user/:id"
component={props => (
<User
{...props}
employee={this.state.employee}
currentEmployee={this.state.currentEmployee}
/>
)}
/>
</div>
</Router>
);
}
}
Body and User (receive parent state as props together with updater functions):
class Body extends React.Component {
getName = () => {
const { employee, add } = this.props;
return employee.map(name => (
<Link className="link" to={`/user/${name.name}`}>
{" "}
<div onClick={() => add(name)} key={name.id} className="item">
{" "}
<img
className="img"
src={`https://picsum.photos/${name.name}`}
/>{" "}
<h1 className="name"> {name.name} </h1>
</div>{" "}
</Link>
));
};
render() {
return <div className="body">{this.getName()}</div>;
}
}
class User extends React.Component {
render() {
// you will need to map employeeCurrent somehow
return <div>{this.props.employeeCurrent}</div>;
}
}
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>
);
}
}