I'm trying to create a feature where when a user clicks on a <Link>, they navigate to another component (Post) and scroll directly to a Comment. The path satisfies the requirements for the <Route> definition, but when I use a "#" as part of the URL, the redirect does not take affect:
Route: <Route path="/post/:id/:hash" component={Post} />
URL: https://b881s.codesandbox.io/post/4/#ve33e
However, what's interesting is that the feature works as expected when I use a "#" instead of "#".
URL: https://b881s.codesandbox.io/post/4/#ve33e
I've tried to find any mentions of "#" being some sort of special character to react-router-dom, but have not found anything. Maybe there's something I'm fundamentally missing here?
Here's the sandbox with working code: https://codesandbox.io/s/scrollintoview-with-refs-and-redux-b881s
App.js
import React from "react";
import ReactDOM from "react-dom";
import Home from "./Home";
import Posts from "./Posts";
import Post from "./Post";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import store from "./store";
import { Provider } from "react-redux";
import "./styles.css";
const App = () => {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/posts" component={Posts} />
<Route path="/post/:id/:hash" component={Post} />
<Route path="/post/:id/" component={Post} />
</Switch>
</div>
</BrowserRouter>
</Provider>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Posts.js
import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
class Posts extends React.Component {
createPostList = () => {
const { posts } = this.props;
if (posts.posts) {
return posts.posts.map(post => {
return (
<div key={post.id} className="post">
<Link to={`/post/${post.id}`}>{post.title}</Link>
<p>{post.text}</p>
<div>
{post.comments.map(comment => {
return (
<div>
<Link to={`/post/${post.id}/#${[comment.id]}`}>
{comment.id}
</Link>
</div>
);
})}
</div>
</div>
);
});
} else {
return <h4>Loading...</h4>;
}
};
render() {
return <div>{this.createPostList()}</div>;
}
}
const mapStateToProps = state => {
return {
posts: state.posts
};
};
export default connect(mapStateToProps)(Posts);
Anything after # in a URL string is called hash. You can access the hash for a given location using location.hash. So in your routes you won't need to mention :hash. You should instead read the hash through the location object injected to the component by the Route component.
Your Route:
<Route path="/post/:id" component={Post} />
To read hash in Post component:
class Post extends React.Component {
render() {
const {location} = this.props;
console.log(location.hash);
...
}
}
Use %23 as a hash sign, should definitely solve it.
More information about it: https://en.wikipedia.org/wiki/Percent-encoding
Here is a forked from you, that I use %23 to represent #
https://codesandbox.io/s/scrollintoview-with-refs-and-redux-z8pz8
Related
Whenever I console log props.match.params, I get an error:
TypeError: Cannot read property 'params' of undefined at App. I'm not sure this is relevant, but even if I console.log(props) I get four empty arrays.
Here is all the relevant code:
Home.js
import React from "react";
import App from "./App";
import { render } from "react-dom";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
Redirect,
} from "react-router-dom";
const Home = () => {
return (
<div>
<Router>
<Route exact path="/">
<App />
</Route>
<Route path="/:roomCode" component={App} />
</Router>
</div>
);
};
export default Home;
App.js (only the relevant part)
const App = (props) => {
console.log(props.match.params);
};
export default App;
const appDiv = document.getElementById("app");
render(<App />, appDiv);
I have been trying to figure this out for the past two days. Nothing works. Also, history.push also doesn't work, returns a very similar error. I have a feeling react-router-dom is broken in my project.
Help is much appreciated.
Edit:
Here is the codesandbox: https://codesandbox.io/s/reverent-microservice-iosu2?file=/src/App.js
Your Home Component is the root of all your components so it needs to be pass to render function not your App which is a descendent of Home.
after that change you need to change this line in your Home Component:
<Route exact path="/" render={(props) => <App {...props} />} />
import React from "react";
import App from "./App";
import { render } from "react-dom";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
Redirect,
} from "react-router-dom";
const Home = () => {
return (
<div>
<Router>
<Route exact path="/" render={(props) => <App {...props} />} />
<Route path="/:roomCode" component={App} />
</Router>
</div>
);
};
export default Home;
here is how to render it:
const appDiv = document.getElementById("app");
render(<Home />, appDiv);
now you can get the props
const App = (props) => {
console.log(props.match.params);
};
export default App;
You could try using the React hooks provided by the React Router framework. There are several different hooks used to interact with the router.
const App = () => {
const { roomCode } = useParams()
console.log(params);
};
More info here
I am using Router and customHistory to help me redirect the pages, but the pages not render correctly.
The code works like this: if the user is authorized or log in, then the user should be redirected to "localhost:8080/dashboard" and see the dashboard(with data fetching from firebase) & header; if the use is log out, then the user should be redirect to "locahost:8080/" and see the log in button with the header.
However, after I successfully log in, the url is "localhost:8080/dashboard" without any data fetched from firebase, only things I can see are the header and login button. But if I hit "RETURN" with the current url which is "localhost:8080/dashboard", it will redirect to correct page with all data fetching from firebase, and no login button.
This is the github_link to the code.
I have spent times searching online, but do not find any positive result except this one. After reading the stackoverflow I feel my code has some problems with asynchronization. Any thoughts?
I really appreciate for your help! Thanks!
This is my AppRouter.js:
export const customHistory = createBrowserHistory();
const AppRouter = () => (
<Router history={customHistory}>
<div>
<Header />
<Switch>
<Route path="/" exact component={LoginPage} />
<Route path="/dashboard" component={ExpenseDashboardPage} />
<Route path="/create" component={AddExpensePage} />
<Route path="/edit/:id" component={EditExpensePage} />
<Route path="/help" component={HelpPage} />
<Route component={LoginPage} />
</Switch>
</div>
</Router>
);
This is my app.js
import React, { Children } from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import "normalize.css/normalize.css"; //Normalize.css makes browsers render all elements more consistently and in line with modern standards.
import "./styles/styles.scss";
import AppRouter, { customHistory } from "./routers/AppRouter";
import configureStore from "./redux/store/configStore";
import { startSetExpenses } from "./redux/actions/expenses";
import { login, logout } from "./redux/actions/auth";
import "react-dates/lib/css/_datepicker.css";
import { firebase } from "./firebase/firebase";
//for testing: npm test -- --watch
const store = configureStore();
const jsx = (
<Provider store={store}>
<AppRouter />
</Provider>
);
ReactDOM.render(<p>Loading...</p>, document.getElementById("app"));
let hasRendered = false;
const renderApp = () => {
if (!hasRendered) {
ReactDOM.render(jsx, document.getElementById("app"));
hasRendered = true;
}
};
firebase.auth().onAuthStateChanged((user) => {
if (user) {
console.log("log in");
store.dispatch(login(user.uid));
store.dispatch(startSetExpenses()).then(() => {
renderApp();
if (customHistory.location.pathname === "/") {
customHistory.push("/dashboard");
}
});
} else {
console.log("log out");
store.dispatch(logout());
renderApp();
customHistory.push("/");
}
});
This is the header.js
import React from "react";
import { BrowserRouter, Route, Switch, Link, NavLink } from "react-router-dom";
import { connect } from "react-redux";
import { startLogout } from "../redux/actions/auth";
export const Header = ({ startLogout }) => (
<header>
<h1>Expensify</h1>
<NavLink to="/" activeClassName="is-active">
Dashboard
</NavLink>
<NavLink to="/create" activeClassName="is-active">
CreateExpense
</NavLink>
<button onClick={startLogout}>Logout</button>
</header>
);
const mapDispatchToProps = (dispatch) => ({
startLogout: () => dispatch(startLogout()),
});
export default connect(undefined, mapDispatchToProps)(Header);
This is a useEffect gotcha and I run into it at least once a month. :(
Anyway,
I have a component that is rendering one of two components based on a state condition.
This works fine except for one problem. I get the infamous "flicker" render of the previous component. I am trying to mask this with a third component - dumb loader spinner. This is where the problem occurs. I can't get the dumb thing to work.
My working code is the following. The only relevant parts are those with comments.
Scroll further down for my non-working pseudo code solution.
Thank you.
import React, {useState} from 'react';
import { BrowserRouter, Route, Redirect } from "react-router-dom";
import { withRouter } from "react-router";
import {Switch} from 'react-router';
import LandingWithoutClients from './PageComponents/Landing';
import LandingWithClients from './PageComponents/Landing/LandingWithClients';
import Workflows from "./PageComponents/Workflows";
import SaveAndLoad from "./PageComponents/SaveAndLoad";
import Calendar from "./PageComponents/Calendar";
import Navbar from "./PageComponents/Navigation/Navbar";
import Authentication from './PageComponents/Authentication'
import Navigation from "./PageComponents/Navigation";
import { MuiPickersUtilsProvider } from 'material-ui-pickers';
import MomentUtils from '#date-io/moment';
import db from "./services/indexDB";
import SaveIcon from "#material-ui/icons/Save";
function App(props){
const [clientExistsState, updateClientsExistsState] = useState(false);
db.clients.toArray(function(data){
if(data[0]){
updateClientsExistsState(true)
}else{
updateClientsExistsState(false)
}
})
let Nav = clientExistsState ? Navbar : Navigation
//_____________________________________________________If clientsExists assign Landing with LandingWithClients otherwise assign Landing with LandingWithoutClients
let Landing = clientExistsState ? LandingWithClients : LandingWithoutClients
//___________________________________________________________________________________
function redirectToClientsList(){
window.location.href = "/";
}
function redirectToCalendar(){
window.location.href = "/calendar";
}
function redirectToAuthentication(){
window.location.href = "/authentication";
}
function redirectToSaveAndLoad(){
window.location.href = "/save-and-load";
}
return (
<div className="App">
<Provider>
<MuiPickersUtilsProvider utils={MomentUtils}>
<BrowserRouter>
<div>
<Nav
endpointProps = {props}
redirectToClientsList = {redirectToClientsList}
redirectToCalendar={redirectToCalendar}
redirectToAuthentication={redirectToAuthentication}
redirectToSaveAndLoad={redirectToSaveAndLoad}
/>
<Switch>
<Route exact path="/" component={Landing} /> {/* Assign Landing Component*/}
<Route exact path="/client/:id/client-name/:client/workflows" component={Workflows} />
<Route exact path="/calendar" component={Calendar} />
<Route exact path="/authentication" component={Authentication} />
<Route exact path="/save-and-load" component={SaveAndLoad} />
<Redirect from="/*" to="/" />
</Switch>
</div>
</BrowserRouter>
</MuiPickersUtilsProvider>
</Provider>
</div>
);
}
export default withRouter(App);
here is a pseudo code fix with two useEffect instances
function App(props){
// code ...
cons [ loaderBool, setLoaderBool] = useState(true);
let Landing = Loader;
useEffect(() => {
if (loaderBool) {
setTimeout(() => {
setLoaderBool(false)
},500)
}
}, [])
useEffect(() => {
if (loaderBool) {
Landing = Loader
} else {
Landing = clientExistsState ? LandingWithClients : LandingWithoutClients
}
}, [loaderBool])
return(
<div>
<Route exact path="/" component={Landing} />
</div>
)
}
I fixed it like this:
import React, {useState, useEffect} from 'react';
import { BrowserRouter, Route, Redirect } from "react-router-dom";
import { withRouter } from "react-router";
import {Switch} from 'react-router';
import LandingWithoutClients from './PageComponents/Landing';
import LandingWithClients from './PageComponents/Landing/LandingWithClients';
import Workflows from "./PageComponents/Workflows";
import SaveAndLoad from "./PageComponents/SaveAndLoad";
import Calendar from "./PageComponents/Calendar";
import Navbar from "./PageComponents/Navigation/Navbar";
import Loader from './PageComponents/Loader';
import Authentication from './PageComponents/Authentication'
import Navigation from "./PageComponents/Navigation";
import { MuiPickersUtilsProvider } from 'material-ui-pickers';
import MomentUtils from '#date-io/moment';
import db from "./services/indexDB";
import SaveIcon from "#material-ui/icons/Save";
import Context,{Provider} from "./services/context";
// if client is active display Navigation.
// if client is not active then display NavigationWitSlide
// create new landing page
function App(props){
const [loaderBool, setLoaderBool] = useState(true)
const [clientExistsState, updateClientsExistsState] = useState(false);
db.clients.toArray(function(data){
if(data[0]){
updateClientsExistsState(true)
}else{
updateClientsExistsState(false)
}
})
let Nav = clientExistsState ? Navbar : Navigation
let Landing = clientExistsState ? LandingWithClients : LandingWithoutClients
function redirectToClientsList(){
window.location.href = "/";
}
function redirectToCalendar(){
window.location.href = "/calendar";
}
function redirectToAuthentication(){
window.location.href = "/authentication";
}
function redirectToSaveAndLoad(){
window.location.href = "/save-and-load";
}
// check if clients exists
useEffect(()=>{
setTimeout(()=>{
setLoaderBool(false)
},500)
},[])
return (
<div className="App">
<Provider>
<MuiPickersUtilsProvider utils={MomentUtils}>
<BrowserRouter>
<div>
<Nav
endpointProps = {props}
redirectToClientsList = {redirectToClientsList}
redirectToCalendar={redirectToCalendar}
redirectToAuthentication={redirectToAuthentication}
redirectToSaveAndLoad={redirectToSaveAndLoad}
/>
<Switch>
<Route exact path="/" component={(function(){
if(loaderBool){
return Loader
}else{
return Landing
}
}())} />
<Route exact path="/client/:id/client-name/:client/workflows" component={Workflows} />
<Route exact path="/calendar" component={Calendar} />
<Route exact path="/authentication" component={Authentication} />
<Route exact path="/save-and-load" component={SaveAndLoad} />
<Redirect from="/*" to="/" />
</Switch>
</div>
</BrowserRouter>
</MuiPickersUtilsProvider>
</Provider>
</div>
);
}
export default withRouter(App);
Try useMemo.
const Landing = useMemo(() => {
if (!loaderBool) {
if (clientExistsState) {
return LandingWithClients;
}
return LandingWithoutClients;
}
return Loader;
}, [clientExistsState, loaderBool]);
EDIT : I found the solution, I forget switch component in my routes.js
I have created a route like that : /post/:id
my routes.js look like :
import React, { Component } from "react";
import { BrowserRouter, Route } from "react-router-dom";
import Posts from "./components/Posts";
import Post from "./components/Post";
class Routes extends Component {
render() {
return (
<div>
<BrowserRouter>
<div>
<Route exact path="/" component={Posts} />
<Route path="/post/:id" component={Post} />
</div>
</BrowserRouter>
</div>
);
}
}
export default Routes;
At my /route I listed all my posts, and when I click on one of them I update the url http://localhost:3000/post/1 for example.
PS : I tried to use withRouter function as it says in the official document of react-router, but this case not work in my case.
The file in question is post.js it look like :
import React, { Component } from "react";
import { active_post } from "../actions/index";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
class Post extends Component {
componentWillMount() {
this.props.active_post(this.props.match.params.id);
}
render() {
console.log(this.props.post);
return (
<div>
<h1>Detail d'un post</h1>
<p>{this.props.match.params.id}</p>
</div>
);
}
}
function mapStateToProps(state) {
return {
post: state.activePost
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ active_post }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(Post);
So for example, if I enter manually in the URL bar http://localhost:3000/post/3 for example I have the component and the console.log() with the post when I need.
So please if you have a solution for this case I take this.
Thanks you :)
You should try adding a Switch from react-router-dom around your routes.
<BrowserRouter>
<div>
<Switch>
<Route exact path="/" component={Posts} />
<Route path="/post/:id" component={Post} />
</Switch>
</div>
</BrowserRouter>
Don't forget to include the Switch module from react-router-dom in your component.
import { Route, Switch } from 'react-router-dom';
I am developing an application in which I check if the user is not loggedIn. I have to display the login form, else dispatch an action that would change the route and load other component. Here is my code:
render() {
if (isLoggedIn) {
// dispatch an action to change the route
}
// return login component
<Login />
}
How can I achieve this as I cannot change states inside the render function.
Considering you are using react-router v4
Use your component with withRouter and use history.push from props to change the route. You need to make use of withRouter only when your component is not receiving the Router props, this may happen in cases when your component is a nested child of a component rendered by the Router and you haven't passed the Router props to it or when the component is not linked to the Router at all and is rendered as a separate component from the Routes.
import {withRouter} from 'react-router';
class App extends React.Component {
...
componenDidMount() {
// get isLoggedIn from localStorage or API call
if (isLoggedIn) {
// dispatch an action to change the route
this.props.history.push('/home');
}
}
render() {
// return login component
return <Login />
}
}
export default withRouter(App);
Important Note
If you are using withRouter to prevent updates from being blocked by
shouldComponentUpdate, it is important that withRouter wraps the
component that implements shouldComponentUpdate. For example, when
using Redux:
// This gets around shouldComponentUpdate
withRouter(connect(...)(MyComponent))
// This does not
connect(...)(withRouter(MyComponent))
or you could use Redirect
import {withRouter} from 'react-router';
class App extends React.Component {
...
render() {
if(isLoggedIn) {
return <Redirect to="/home"/>
}
// return login component
return <Login />
}
}
With react-router v2 or react-router v3, you can make use of context to dynamically change the route like
class App extends React.Component {
...
render() {
if (isLoggedIn) {
// dispatch an action to change the route
this.context.router.push('/home');
}
// return login component
return <Login />
}
}
App.contextTypes = {
router: React.PropTypes.object.isRequired
}
export default App;
or use
import { browserHistory } from 'react-router';
browserHistory.push('/some/path');
In react-router version 4:
import React from 'react'
import { BrowserRouter as Router, Route, Redirect} from 'react-router-dom'
const Example = () => (
if (isLoggedIn) {
<OtherComponent />
} else {
<Router>
<Redirect push to="/login" />
<Route path="/login" component={Login}/>
</Router>
}
)
const Login = () => (
<h1>Form Components</h1>
...
)
export default Example;
Another alternative is to handle this using Thunk-style asynchronous actions (which are safe/allowed to have side-effects).
If you use Thunk, you can inject the same history object into both your <Router> component and Thunk actions using thunk.withExtraArgument, like this:
import React from 'react'
import { BrowserRouter as Router, Route, Redirect} from 'react-router-dom'
import { createBrowserHistory } from "history"
import { applyMiddleware, createStore } from "redux"
import thunk from "redux-thunk"
const history = createBrowserHistory()
const middlewares = applyMiddleware(thunk.withExtraArgument({history}))
const store = createStore(appReducer, middlewares)
render(
<Provider store={store}
<Router history={history}>
<Route path="*" component={CatchAll} />
</Router
</Provider>,
appDiv)
Then in your action-creators, you will have a history instance that is safe to use with ReactRouter, so you can just trigger a regular Redux event if you're not logged in:
// meanwhile... in action-creators.js
export const notLoggedIn = () => {
return (dispatch, getState, {history}) => {
history.push(`/login`)
}
}
Another advantage of this is that the url is easier to handle, now, so we can put redirect info on the query string, etc.
You can try still doing this check in your Render methods, but if it causes problems, you might consider doing it in componentDidMount, or elsewhere in the lifecycle (although also I understand the desire to stick with Stateless Functional Compeonents!)
You can still use Redux and mapDispatchToProps to inject the action creator into your comptonent, so your component is still only loosely connected to Redux.
This is my handle loggedIn. react-router v4
PrivateRoute is allow enter path if user is loggedIn and save the token to localStorge
function PrivateRoute({ component: Component, ...rest }) {
return (
<Route
{...rest}
render={props => (localStorage.token) ? <Component {...props} /> : (
<Redirect
to={{
pathname: '/signin',
state: { from: props.location },
}}
/>
)
}
/>
);
}
Define all paths in your app in here
export default (
<main>
<Switch>
<Route exact path="/signin" component={SignIn} />
<Route exact path="/signup" component={SignUp} />
<PrivateRoute path="/" component={Home} />
</Switch>
</main>
);
Those who are facing issues in implementing this on react-router v4. Here is a working solution for navigating through the react app programmatically.
history.js
import createHistory from 'history/createBrowserHistory'
export default createHistory()
App.js OR Route.jsx. Pass history as a prop to your Router.
import { Router, Route } from 'react-router-dom'
import history from './history'
...
<Router history={history}>
<Route path="/test" component={Test}/>
</Router>
You can use push() to navigate.
import history from './history'
...
render() {
if (isLoggedIn) {
history.push('/test') // this should change the url and re-render Test component
}
// return login component
<Login />
}
All thanks to this comment: https://github.com/ReactTraining/react-router/issues/3498#issuecomment-301057248
render(){
return (
<div>
{ this.props.redirect ? <Redirect to="/" /> :'' }
<div>
add here component codes
</div>
</div>
);
}
I would suggest you to use connected-react-router https://github.com/supasate/connected-react-router
which helps to perform navigation even from reducers/actions if you want.
it is well documented and easy to configure
I was able to use history within stateless functional component, using withRouter following way (needed to ignore typescript warning):
import { withRouter } from 'react-router-dom';
...
type Props = { myProp: boolean };
// #ts-ignore
export const MyComponent: FC<Props> = withRouter(({ myProp, history }) => {
...
})
import { useNavigate } from "react-router-dom"; //with v6
export default function Component() {
const navigate = useNavigate();
navigate.push('/path');
}
I had this issue and just solved it with the new useNavigate hook in version 6 of react-router-dom