I am new to react and trying to create simple navigation which has two menu items (Dashboard and Users). But when I click on Users link it did not render that page content, but dashboard content getting hide. Someone please help me to resolve the issue.
App.js
import React, { Component } from 'react';
import Login from './pages/Login';
import { BrowserRouter as Router, Switch, Route, Link, Redirect, withRouter } from 'react-router-dom';
import { history } from './_helpers/history';
import { authenticationService } from './_services/authentication.service';
import Users from './pages/users/Users';
import Dashboard from './pages/Dashboard';
class App extends Component {
constructor(props) {
super(props);
this.state = {
currentUser: null
};
}
componentDidMount() {
authenticationService.currentUser.subscribe(x => this.setState({ currentUser: x }));
}
logout() {
authenticationService.logout();
history.push('/login');
}
render () {
const { currentUser } = this.state;
return (
currentUser ? (
<Router>
<div id="wrapper">
<ul>
<li><Link to={'/'} className="nav-link" > <i className="fas fa-fw fa-tachometer-alt"></i> <span>Dashboard</span> </Link></li>
<li><Link to={'/users'} className="nav-link" > <i className="fas fa-fw fa-users"></i> <span>Users</span> </Link></li>
</ul>
<Switch>
<Route path='/' component={Dashboard} />
<Route path='/users' component={Users} />
</Switch>
</div>
</Router>
) : <Login />
);
}
}
export default App;
Dashboard.js
import React, { Component } from 'react';
import { Formik, Field, Form, ErrorMessage } from 'formik';
import * as Yup from 'yup';
import { authenticationService } from '../_services/authentication.service';
import { history } from '../_helpers/history';
class Dashboard extends Component {
constructor (props){
super(props);
if (authenticationService.currentUserValue) {
history.push('/');
}
this.state = {
isPage: '/'
}
}
render (){
if(this.state.isPage == window.location.pathname){
return (
<div className="container">
dashboard
</div>
)
}else{
return '';
}
}
}
export default Dashboard;
Users.js
import React, { Component } from 'react';
import { Formik, Field, Form, ErrorMessage } from 'formik';
import * as Yup from 'yup';
import { authenticationService } from '../../_services/authentication.service';
import { history } from '../../_helpers/history';
class Users extends Component {
constructor (props){
super(props);
if (authenticationService.currentUserValue) {
history.push('/');
}
this.state = {
isPage: '/users'
}
}
render (){
if(this.state.isPage == window.location.pathname){
return (
<div className="container">
users
</div>
)
}else{
return '';
}
}
}
export default Users;
In App.js component; make Switch direct child of Router; that will fix the issue. You can refactor your code like so:
<Router>
<Switch>
<div id="wrapper">
<ul>
<li><Link to={'/'} className="nav-link" > <i className="fas fa-fw fa-tachometer-alt"></i> <span>Dashboard</span> </Link></li>
<li><Link to={'/users'} className="nav-link" > <i className="fas fa-fw fa-users"></i> <span>Users</span> </Link></li>
</ul>
<Route path='/' component={Dashboard} />
<Route path='/users' component={Users} />
</div>
</Switch>
</Router>
but dashboard content getting hide.
Can you elaborate on that? I'm not quite understanding what you mean.
The problem may lie with your use of react lifecycles.
authenticationService.currentUser.subscribe()
is set on componentDidMount() so only after the JSX gets mounted to the DOM. Your Users component is checking authenticationService.currentUserValue on the constructor which runs first before it gets mounted. authenticationService.currentUserValue maybe giving you a falsy which will kick you out to /. Console log that value or place those inside a componentDidMount so it will only check after the mount.
constructor (props){
super(props);
this.state = {
isPage: '/users'
}
}
componentDidMount() {
if (authenticationService.currentUserValue) {
history.push('/');
}
}
When using the <Switch> component, it will render the first component (in order) that matches the path. Optionally you can put an exact prop on the route so it must match the path 100%.
Your <Dashboard> component is being rendered, however your logic for returning an empty string if the path does not match is preventing you from seeing it. You can move the <Users> route higher, or put an exact prop on your routes.
I've created a small CodeSandbox
https://codesandbox.io/s/festive-worker-t7ly3
I assume your ../../_helpers/history looks like that
import { createBrowserHistory } from "history";
export default createBrowserHistory();
You forget to pass history to Router as props, so other components do not know what is history
<Router history={history}>...</Router>`
According to the documentation, <Switch>
Renders the first child or that matches the location.
In your code you have:
<Route path='/' component={Dashboard} />
<Route path='/users' component={Users} />
The problem is that path='/' actually matches any path, including /users, because /users starts with /. So when the route is /users, the Redirect component renders the Dashboard Route and stops looking for other routes.
To fix this, you could add the exact prop to the / Route. exact means that / will not match anything except paths that are exactly "/":
<Route exact path="/" component={Dashboard} />
<Route path="/users" component={Users} />
Now, if the path is /users, the Dashboard Route no longer matches, and the Switch checks if the next Route matches, which it does!
Fixed & simplified example: https://codesandbox.io/s/lucid-leaf-fkgv9
Note that I have removed some code (like this.state.isPage == window.location.pathname) which seemed to be checking if the route matches. You don't need to worry about this in your components, because React-Router takes care of all the routing for you!
Another solution would be to put the Users Route first so that it is checked first, but this can get messy if you have multiple Routes and want to keep them organized.
Remove this line.
if (authenticationService.currentUserValue) {
history.push('/');
}
this is redirecting you again and again to the same page.
Related
I am new to react and react-router, so please go easy on me.
I am trying to implement router in my Todo List project, where path="/" takes me to my todo list and path="/id" takes me to a test page (later will show the description of the task).
When I click the link that takes me to "/id", the URL in the browser changes but the page/content doesn't. However, when I refresh my browser, the test page loads.
I have put the Switch in App.js shown below.
import React, { Component } from "react";
import "./App.css";
import TodoList from "./components/TodoList";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Test from "./components/Test";
class App extends Component {
render() {
return (
<Router>
<div className="todo-app">
<p>
<Link to="/">Home</Link>
</p>
<Switch>
<Route exact path="/" component={TodoList} />
<Route path={`/id`} component={Test} />
</Switch>
</div>
</Router>
);
}
}
export default App;
And I have put the Link to "/id" as shown below in a child component of component which is called here in App.js.
<div key={todo.id}>
<Link className="todo-text" to={`/id/${todo.id}`}>
{todo.text}
</Link>
</div>
Am I missing something which is causing my component to not load when I click the link?
Edit: Here's a link to my project. https://stackblitz.com/edit/react-7cpjp9?file=src/index.js
Issue
Ok, the issue is exactly as I had suspected. You are rendering multiple routers in your app. The first is a BrowserRouter in your index.js file, the second, another BrowserRouter in App.js, and at least a third BrowserRouter in Todo.js. You need only one router to provide a routing context for the entire app.
The issue here is that the router in Todo component is the closest router context to the links to specific todo details. When a link in Todo is clicked, this closest router handles the navigation request and updates the URL in the address bar. The blocks, or "masks", the router in App component or index.js that is rendering the routes from "seeing" that a navigation action occurred. In other words, the URL in the address bar is updated by the inner router, but the outer router doesn't know to render a different route.
Solution
Keep the BrowserRouter wrapping App in index.js and remove all other routers used in your app.
App - Remove the Router component. Also, reorder the routes/paths from most specific to least specific so you don't need to specify the exact prop on every route. Allows more specific paths to be matched and rendered before less specific paths by the Switch component.
class App extends Component {
render() {
return (
<div className="todo-app">
<p>
<Link to="/">Home</Link>
</p>
<Switch>
<Route path="/id/:todoId" component={Test} />
<Route path="/" component={TodoList} />
</Switch>
</div>
);
}
}
Todo - Remove the Router component. Move the key={todo.id} up to the outer-most element so when todos array is updated React can reconcile updates.
class Todo extends Component {
constructor(props) {
super(props);
this.state = {
id: null,
value: "",
details: "",
};
this.submitUpdate = this.submitUpdate.bind(this);
}
submitUpdate(value) {
const { updateTodo } = this.props;
updateTodo(this.state.id, value);
this.setState({
id: null,
value: "",
});
}
render() {
const { todos, completeTodo, removeTodo } = this.props;
if (this.state.id) {
return <TodoForm edit={this.state} onSubmit={this.submitUpdate} />;
}
return todos.map((todo, index) => (
<div
className={todo.isComplete ? "todo-row complete" : "todo-row"}
key={todo.id}
>
<div>
<Link className="todo-text" to={`/id/${todo.id}`}>
{todo.text}
</Link>
</div>
<div className="icons">
<RiCloseCircleLine
onClick={() => removeTodo(todo.id)}
className="delete-icon"
/>
<TiEdit
onClick={() => this.setState({ id: todo.id, value: todo.text })}
className="edit-icon"
/>
<RiCheckboxCircleLine
onClick={() => completeTodo(todo.id)}
className="delete-icon"
/>
</div>
</div>
));
}
}
First of all the approach, you are taking for dynamic routing is wrong.
It should be like this you will have to add the exact keyword on the dynamic route.
<Route exact path="/id/:todoId" component={Test} />
And
<div key={todo.id}>
<Link className="todo-text" to={`/id/${todo.id}`}>
{todo.text}
</Link>
import React, { Component } from "react";
import "./App.css";
import TodoList from "./components/TodoList";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Test from "./components/Test";
class App extends Component {
render() {
return (
<Router>
<div className="todo-app">
<p>
<Link to="/">Home</Link>
</p>
<Switch>
<Route exact path="/" component={TodoList} />
**<Route exact path={`/id`} component={Test} />**
</Switch>
</div>
</Router>
);
}
}
export default App;
I'm trying to figure out why the component SubPage is not rendering whenever the route path of /sub/:_id is visited (e.g. /sub/5f1c54257ceb10816a13d999). This is the first time I've worked with react routes. The :_id part should presumably accept query parameters from the URL dynamically so I cannot see why this is not working.
I can get the /subs page to fetch the API and render each sub on the page but just not each individual sub page.
The route is as follows near the bottom of App.js: <Route path={"/sub/:_id"} component={SubPage} />
Thanks for any help here. I've made a stackblitz for convenience, or you can see the relevant code below:
And subPage.js:
import React from 'react'
export class SubPage extends React.Component {
render() {
return (
<div className="sub-details-individual">
<h1 class="std-intro">Viewing a Single Subscriber</h1>
<div className="sub-specs">
<div className="sub-specs-inner">
id: {this.props.params._id}
</div>
</div>
</div>
)
}
}
And App.js:
import React, {Component} from "react";
import {navLinks} from "./components/nav-links";
import Root from "./components/Root";
import {
BrowserRouter as Router,
Switch,
Route
} from "react-router-dom";
import {SubPage} from "./components/subPage";
import ShowSubs from "./components/show-subs";
export default class App extends Component {
constructor() {
super();
this.state = {
navLinks: navLinks,
intro: "hello world",
url: "someurl"
}
}
updateURL = (newUrl) => {
this.setState({
url: newUrl
})
}
render() {
return (
<Router>
<Root navLinks={this.state.navLinks} intro={this.state.intro}></Root>
<Switch>
<Route path="/subs">
<p>subs page</p>
{/*this.updateURL('/subs') fails presumably because it causes the rerender infinitely - but how to solve?*/}
<ShowSubs />
</Route>
<Route path="/">
<p>homepage</p>
</Route>
<Route path={"/sub/:_id"} component={SubPage} />
</Switch>
<p>the url: {this.state.url}</p>
</Router>
);
}
}
Two things:
this.props.params._id will crash since you are missing match before params
this.props.match.params._id
few exact props are missing, especially in the subs path:
<Route exact path="/subs">
Note: the exact prop will be useful in the / route as well.
import {BrowserRouter as Router, Route} from 'react-router-dom';
import Home from './Home';
class App extends Component {
constructor(props){
super(props);
this.state = {value: true}
this.goBack = this.goBack.bind(this);
}
goBack() {
this.props.history.goBack();
}
render() {
return (
<Router>
<div className="App">
<div className="App-header">
<button onClick={this.goBack}>Go Back</button>
</div>
<Route path="/" exact render={() => <Home value={this.state.value}/>}/>
<Route path="/details/:id" component={DetailView}/>
</div>
</Router>
);
}
}
export default App;
This is code. On click of Back button i want to take me to the previous age. But this goBack() is not working for me. Probably I am making some mistake in using it.I tried couple of ways from guthub and stackover flow but nothing worked.
can you try adding withRouter
import {..., withRouter} from 'react-router-dom';
then change export to
export default wihRouter(App);
App component does not receive history as prop because the Router is rendered inside it, instead you can create a wrapper component that is in the route to use this.props.history.
class App extends Component {
render() {
return (
<Router>
<Route path="/" component={Content} />
</Router>
)
}
}
For the content component:
class Content extends Component {
constructor(props){
super(props);
this.state = {value: true}
this.goBack = this.goBack.bind(this);
}
goBack() {
this.props.history.goBack();
}
render() {
return (
<div className="App">
<div className="App-header">
<button onClick={this.goBack}>Go Back</button>
</div>
<Route path="/" exact render={() => <Home value={this.state.value}/>}/>
<Route path="/details/:id" component={DetailView}/>
</div>
);
}
}
Now, Content component is in the route and can receive the history prop.
Another way you can handle this is to simply render the Content component in App with <Content /> and then use withRouter HOC on Content.
withRouter
PS: You cannot apply withRouter to App component because technically App is outside the Router
I'm building a simple app in Meteor with React, and I'm using React Router 4.
There is a button which creates a new map, and when the server returns the new map's id, the browser should navigate to the page that shows the newly created map.
I have got it nearly working but when I click the button, the URL changes in the address bar but the map component doesn't render. Refreshing the page doesn't help. But it works fine if I click the new map's link in the list of maps.
This code is ugly and I'm sure it's not the best way to do any of it, but it is literally the only way I've been able to make it work at all. I've read all the forum posts and docs I can find but nothing is clear to me.
I'd be very grateful for any help.
Main.js
```JSX
/* global document */
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
// Start the app with imports
import '/imports/startup/client';
import '../imports/startup/accounts-config.js';
import App from '../imports/ui/layouts/App.jsx';
Meteor.startup(() => {
render((<BrowserRouter><App /></BrowserRouter>), document.getElementById('app'));
});
App.js:
import React, { Component } from 'react';
import { withRouter, Switch, Route, Link, Redirect } from 'react-router-dom';
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
// Mongodb collection
import { Maps } from '../../api/maps/maps.js';
// User Accounts
import AccountsUIWrapper from '../AccountsUIWrapper.jsx';
import Home from '../pages/home';
import Map from '../pages/map';
import About from '../pages/about';
class App extends Component {
renderTestButton() {
return (
<button onClick={this.handleClick.bind(this)}>New Map</button>
);
}
handleClick() {
let that = this;
Meteor.call('newMap', {'name': 'new map'}, function(error, result) {
that.props.history.push(`/map/${result}`);
});
}
render() {
let newMap = this.renderNewMap();
let testButton = this.renderTestButton();
return (
<div className="primary-layout">
<header>
<AccountsUIWrapper />
{testButton}
<nav>
<ul>
<li><Link to='/'>Home</Link></li>
<li><Link to='/about'>About</Link></li>
</ul>
</nav>
</header>
<main>
<Switch>
<Route exact path='/' component={Home}/>
<Route exact path="/map/:_id" render={({ match }) => (
<Map
params={match.params}
/>
)} />
<Route path='/about' component={About}/>
</Switch>
</main>
</div>
);
}
}
export default withRouter(withTracker(() => {
const mapsHandle = Meteor.subscribe('maps');
return {
'maps': Maps.find({}).fetch(),
'loading': !mapsHandle.ready(),
'currentUser': Meteor.user(),
};
})(App));
EDIT: I had a typo in my path, I wrote that.props.history.push(/maps/${result}); when it should be that.props.history.push(/map/${result}); to match the defined route.
I've correct the code, it now works but I still feel this can't be the best solution...
After finding a typo in my original code ('/maps/' where the path should have been '/map/') I found another 'gotcha' which is this:
If the route expects a URL parameter, and it is not supplied, then the route doesn't seem to render at all. My route is defined as:
```JSX
<Route path="/map/:_id" render={({ match }) => (
<Map
params={match.params}
/>
)} />
If you try to navigate to 'http://localhost:3000/map/' then the component doesn't render. If you put any value on the end e.g. 'http://localhost:3000/map/dummyvalue' it renders.
I've now got a tidier version working:
```JSX
import React, { Component } from 'react';
import { withRouter, Switch, Route, Link } from 'react-router-dom';
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
// Mongodb collection
import { Maps } from '../../api/maps/maps.js';
// User Accounts
import AccountsUIWrapper from '../AccountsUIWrapper.jsx';
import Home from '../pages/home';
import Map from '../pages/map';
import About from '../pages/about';
function NewMap(props) {
function handleClick(e) {
e.preventDefault();
let history = props.history;
Meteor.call('newMap', {'name': 'new map'}, (error, result) => {
history.push(`/map/${result}`);
});
}
let disabled = 'disabled';
if (Meteor.userId()) { disabled = '';}
return (
<button disabled={disabled} onClick={handleClick}>New Map</button>
);
}
class App extends Component {
renderNewMap() {
const history = this.props.history;
return (
<NewMap history={history}
/>
);
}
render() {
let newMap = this.renderNewMap();
return (
<div className="primary-layout">
<header>
<AccountsUIWrapper />
{newMap}
<nav>
<ul>
<li><Link to='/'>Home</Link></li>
<li><Link to='/about'>About</Link></li>
</ul>
</nav>
</header>
<main>
<Switch>
<Route exact path='/' component={Home}/>
<Route path="/map/:_id" render={({ match }) => (
<Map
params={match.params}
/>
)} />
<Route path='/about' component={About}/>
</Switch>
</main>
</div>
);
}
}
export default withRouter(withTracker(() => {
const mapsHandle = Meteor.subscribe('maps');
return {
'maps': Maps.find({}).fetch(),
'loading': !mapsHandle.ready(),
'currentUser': Meteor.user(),
};
})(App));
I would like to ask if its possible not to re render the header component when navigating through page, because when I do console.log() in the header component it always fire up when navigating through page. Here's is my code:
// Route.jsx
<Route component={HeroesPage}>
<Route path="/reactjs" component={HeroesComponent}></Route>
<Route path="/reactjs2" component={HeroesCreateComponent}></Route>
</Route>
// HeroesPage.jsx
import React from 'react';
import Header from "components/common/Header.jsx";
class HeroesPage extends React.Component {
render() {
return (
<div>
<Header />
{ this.props.children }
</div>
);
}
}
export default HeroesPage;
// Header.jsx
import React from "react";
import { Link } from 'react-router';
class Header extends React.Component {
componentDidMount(){
console.log('header loaded');
// If I will add an api call here, it will fetch to the server everytime I navigate to the page
}
render(){
return(
<ul className="nav nav-tabs">
<li><Link to="/reactjs">Reactjs 1</Link></li>
<li><Link to="/reactjs2">Reactjs 2</Link></li>
</ul>
);
}
}
export default Header;
Try refactoring routes like
<Route path="/" component={HeroesPage}>
<Route path="reactjs" component={HeroesComponent} />
<Route path="reactjs2" component={HeroesCreateComponent} />
</Route>