React router v4 onUpdate - javascript

I have recently updated to react router version 4. In the previous version I was using the onUpdate call back to trigger a function to make the page scroll to the top when the route has been changed.
It appears that onUpdate has been deprecated and I cannot find anywhere in the docs what it has been replaced with.
Has anyone else come across this issue?
const handlePageChange = () => {
window.scrollTo(0, 0);
};
ReactDOM.render(
<Provider store={store}>
<Router onUpdate={handlePageChange} history={browserHistory}>
<Redirect from="/" to="/music" />
{routes}
</Router>
</Provider>,
document.getElementById('root')
);

"onUpdate" is depreciated. You can use "onEnter" property in "Routes".
<Router history={browserHistory}>
<Route path="/" component={App} >
<IndexRoute component={Home} />
<Route path="/contact-us" component={ContactUs} onEnter={handlePageChange}/>
</Route>
</Router>
Also need to modify your "handlePageChange" function as below:
const handlePageChange = () => {
window.scrollTo(0, 0);
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
}

#Alireza's answer was in the right direction, but it's not quite complete.
To be able to access the router in using React's Context API, the component both has to be a child of the Router, and it should define the contextTypes property:
static contextTypes = {
router: PropTypes.object
};
That will make sure that the Router is attached to that component.
Furthermore, you can not (or no longer?) subscribe to the router. However, you can attach a listener to the History:
this.context.router.history.listen(handlePageChange)
You'll probably want to do that in the component's componentDidMount lifecycle method.

Another option is to scroll the page when your page component mounts:
class NotFoundView extends React.Component {
componentDidMount() {
window.scrollTo(0, 0);
}
render() {
var props = this.props;
return (
<div className="NotFound">
<HeaderContainer />
<h1>Coming Soon!</h1>
</div>
)
}
}
export default NotFoundView;

There is something called context.router.subscribe as replacement...
you can use something like this:
import React from 'react';
class App extends React.Component {
//something like this in your code
componentDidMount() {
this.context.router.subscribe(() => {
console.log("change router");
})
}
render() {
return <button>hi</button>;
}
}
export default App;

Related

Can't divide Logged In screens and Logged out ones in React Router + FIrebase

I'm working on the application with React + Firebase. The app requires authentication that users accesses to their own timeline. So I want the app separated between LoggedIn screens and LoggedOut screens.
In App.js, I set up <Auth /> to the screens require authentication. Like this:
function App() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Switch>
<Switch>
<Auth>
<Route path="/timeline" component={Timeline} />
</Auth>
</Switch>
<Route path="/" component={Home} />
</Switch>
</div>
</BrowserRouter>
</Provider>
);
}
export default App;
And added Auth.
import React from "react";
import { Redirect } from "react-router-dom";
import firebase from "../config/firebase";
class Auth extends React.Component {
constructor(props) {
super(props);
this.state = {
user: false
};
}
componentDidMount() {
firebase.auth().onAuthStateChanged(user => {
if (user) {
this.setState({ user });
}
});
}
render() {
if (!this.state.user) {
return (
<div>{this.state.user ? this.props.children : <Redirect to="/" />}</div>
);
}
}
}
export default Auth;
But it is not working. It has no error message but I can't access any components...
Router Issue
As mentioned in this thread, nested Switch components is not the intended way that react-router was designed and should probably be avoided.
I think you have overcomplicated the App component here. I would recommend something more like the below example because your routing logic doesn't need to care about authentication. You can wrap the Timeline component with the Auth component inside the implementation of Timeline, and then the router just does the routing, nothing else.
function App() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Switch>
<Route path="/timeline" component={Timeline} />
<Route path="/" component={Home} />
</Switch>
</div>
</BrowserRouter>
</Provider>
);
}
export default App;
An alternative, as mentioned in the linked thread above, is to write your own Switch logic. This might allow you to keep all of the authenticated routes all visible upfront, but you may not want to get into messing around with that.
You need an 'authenticating' state
As touched on in Soroush's answer, you also have an issue with the state management in the Auth component. At the moment, it goes like this:
Component renders with user = false
Redirect component is rendered and page redirects to '/'
Do you see how there is never an opportunity to render the child of auth properly for an authenticated user because the initial render will always trigger the redirect? Firebase never has a chance to log the user in before they are redirected.
To fix this, you Auth to load the page into some sort of 'authenticating' state and only redirect the user after you know whether they are logged in or not. Remember, onAuthStateChanged will only ever be triggered after the initial render.
Good luck!
In this case and your authentication structure I recommended to you using programmatically navigate.
import { withRouter } from "react-router";
class Auth extends React.Component {
constructor(props) {
super(props);
this.state = {
user: false,
authenticating: true,
};
}
componentDidMount() {
firebase.auth().onAuthStateChanged(user => {
if (!user) {
this.props.history.push('/login');
} else {
this.setState({ authenticating: false });
}
});
}
render() {
return (
<div>{authenticating ? <p>Authenticating...</p> : this.props.children}</div>
);
}
}
export default withRouter(Auth);

React Router - Cannot read property 'history' of undefined

I am building a styleguide app. I have two dropdown components where a user can choose both a brand and a component - the app will then display the chosen component branded according to the selected brand. I want both of these options to be included in the URL.
The two dropdown's that are programatically changing the route. I am getting the error TypeError: Cannot read property 'history' of undefined whenever the user interacts with either dropdown.
I have a component being rendered by a route :
<Route path="/:brand/:component"
render={props => <DisplayComponent {...props} />} />
That component has two event handlers for two dropdown component that let the user select the root:
handleComponentPick(event: any) {
const component = event.target.value;
this.props.history.push(`/${this.props.match.params.brand}/${component}`);
}
handleBrandChange = (event: any) => {
if (event.target instanceof HTMLElement) {
const brand = event.target.value;
this.props.history.push(`/${brand}/${this.props.match.params.component}`);
}
};
render = () => {
return (
<div className={"constrain-width-wide center "}>
<ThemePicker
component={this.props.match.params.component}
brand={this.props.match.params.brand}
handleBrandChange={this.handleBrandChange}
handleComponentPick={this.handleComponentPick}
/>
<div className="currently-selected-component" />
<Route path="/:brand/button" component={Button} />
<Route path="/:brand/card" component={Card} />
</div>
);
};
}
I am wrapping the whole app in the Router.
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById("root")
);```
If you are getting this error inside a test using jest, you need to wrap your componen within a router. I am using react-testing-library, so my logic looks as follows:
import { render, cleanup } from '#testing-library/react'
import { BrowserRouter as Router } from 'react-router-dom'
import YourComponent from '../path/to/YourComponent'
// ...
describe('YourComponent component', () => {
afterEach(cleanup)
it('matches snapshot', () => {
const { asFragment } = render(
// The following will solve this issue
<Router>
<YourComponent />
</Router>
)
expect(asFragment()).toMatchSnapshot()
})
})
Can you try these following changes
handleComponentPick(event: any) { to handleComponentPick = (event: any) => {
then
render = () => { to render() {
Hope this works.
you have to pass the history like
<Router history={browserHistory} routes={routes} />,
that way, you can use history with props to navigate.
font: https://github.com/ReactTraining/react-router/blob/v3/docs/guides/Histories.md
try to use browserHistory on you app.js, like
render(
<Router history={browserHistory}>
<Route path='/' component={App}>
<IndexRoute component={Home} />
<Route path='about' component={About} />
<Route path='features' component={Features} />
</Route>
</Router>,
document.getElementById('app')
)
that way, you are passing history for all of your another router.
We need to pass history as a prop to Router. I am expecting that you are using react router v4 aka react-router-dom.
import { createBrowserHistory } from "history";
import { Router } from "react-router-dom";
const history = createBrowserHistory();
...
<Router history={history}>
<Routes />
</Router>
Demo : https://codesandbox.io/s/yv5y905ojv
Spied on the useHistory() hook and provided the mock route data.
import routeData from 'react-router';
describe('<Component /> container tests', () => {
beforeEach(() => {
const mockHistory = {
pathname: '/dashboard'
};
jest.spyOn(routeData, 'useHistory').mockReturnValue(mockHistory);
});

How to reload a URL without refreshing the page in ReactJs?

I am trying to reload onto the same route without having to refresh the page. For this specific case, using history.pushState(), but I'm getting an error:
TypeError: history.pushState is not a function.
Here is my code:
import React from 'react';
import PropTypes from 'prop-types';
import { Container } from 'kawax-js';
import { Switch, Route } from 'react-router-dom';
import File from './FileContainer';
import Folder from './FolderContainer';
import HomeContainer from './HomeContainer';
class RootContainer extends React.Component {
static stateToProps = ({ ownProps, select }) => {
const files = select('files');
const lastFile = _.last(files);
return ({
lastFile: lastFile || {}
})
};
static propTypes = {
history: PropTypes.object.isRequired
};
static defaultProps = {
lastFile: {}
};
render() {
const { lastFile, history } = this.props;
if( lastFile === {} || !lastFile.isUploaded
|| lastFile.isUploaded === null) {
return (
<Switch>
<Route exact path="/" component={HomeContainer} />
<Route exact path="/file/:itemPath/:refHash" component={File} />
<Route exact path="/:folderName" component ={Folder}/>
</Switch>
);
}
return history.pushState(null, "/:folderName")
}
}
export default Container(RootContainer);
Is there a better way of doing this or am I missing something here?
You may get the desired result by forcing the component to rerender, take a look at the documentation here. I see you are extending React.Component so you should be able to do the following:
...
constructor() {
this.reload = this.reload.bind(this);
}
...
reload() {
this.forceUpdate();
}
...
I know it does not use history but there will be no other code required as it is included with the Component class.
please use this code
Router.browserHistory.push('/');
instaed of history.pushState(null, "/:folderName")
You have few possibilities to do that, currently my favorite way to do that is using anonymous function in component prop:
<Switch>
<Route exact path="/" component={()=><HomeContainer/>} />
<Route exact path="/file/:itemPath/:refHash" component={()=><File/>} />
<Route exact path="/:folderName" component ={()=><Folder/>}/>
</Switch>
Or if you want to refresh with current url params, you'll need extra route (reload), and play a little with router stack:
reload = ()=>{
const current = props.location.pathname;
this.props.history.replace(`/reload`);
setTimeout(() => {
this.props.history.replace(current);
});
}
<Switch>
<Route path="/reload" component={null} key="reload" />
<Route exact path="/" component={HomeContainer} />
<Route exact path="/file/:itemPath/:refHash" component={File} />
<Route exact path="/:folderName" component ={Folder}/>
</Switch>
<div onCLick={this.reload}>Reload</div>

Meteor createContainer and Redux

I created a Meteor createContainer to wrap the entire App to just look for Meteor.users():
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import App from './../App.js';
export default AccountContainer = createContainer(() => {
const _id = Meteor.userId();
const accountHandle = Meteor.subscribe('userData', _id);
const loading = !accountHandle.ready();
const user = Meteor.users.findOne(_id);
const userExist = !loading && !!user;
return {
loading,
userExist,
user
};
}, App);
I need to use this data all around the App to show / hide elements and to take control of User private pages like a control panel.
I'm using Redux to store the data coming from createContainer and actually I'm doing it in the App.js file:
// a few imports here...
class App extends Component {
render() {
const { loading, userExist, user } = this.props;
this.props.updateUser({ loading, userExist, user });
return (
<Router history={history}>
<div className="app">
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
<Route exact path="/account" component={Account} />
<Route exact path="/account/pictures" component={Pictures} />
</Switch>
<Newsletter />
<Footer />
</div>
</Router>
);
}
};
export default connect(null, { updateUser })(App);
As you can see here I call this.props.updateUser({ loading, userExist, user }); and it update my redux store.
The issue is that I do receive the following error:
Warning: setState(...): Cannot update during an existing state
transition (such as within render or another component's
constructor). Render methods should be a pure function of props and
state; constructor side-effects are an anti-pattern, but can be moved
to componentWillMount.
This because I'm updating Redux state within the render() App method.
I tried wrapping it between a setTimeout:
setTimeout(() => {
this.props.updateUser({ loading, userExist, user });
});
This solve my issue but I think it is not the best solution to fix it.
I'm confused about how and when to update Redux state using Meteor createContainer.
What I wish to achieve is to be able share the { loading, userExist, user } data all around the App taking that information using for ie in Account.js:
const mapStateToProps = (state) => {
return state.user;
}
export default connect(mapStateToProps)(Account);
Thanks.
EDIT:
Just moved the Redux update frm render() to componentDidMount() method.
It doesn't show the error anymore, it update well in App.js but in Account.js I don't get it updated, it looks like it miss it:

React router v4 use declarative Redirect without rendering the current component

I am using a similar code like this to redirect in my app after users logged in. The code looks like the following:
import React, { Component } from 'react'
import { Redirect } from 'react-router'
export default class LoginForm extends Component {
constructor () {
super();
this.state = {
fireRedirect: false
}
}
submitForm = (e) => {
e.preventDefault()
//if login success
this.setState({ fireRedirect: true })
}
render () {
const { from } = this.props.location.state || '/'
const { fireRedirect } = this.state
return (
<div>
<form onSubmit={this.submitForm}>
<button type="submit">Submit</button>
</form>
{fireRedirect && (
<Redirect to={from || '/home'}/>
)}
</div>
)
}
}
Works fine when a successful login has been triggered. But there is the case, that logged in users enter the login page and should be automatically redirected to the "home" page (or whatever other page).
How can I use the Redirect component without rendering the current component and without (as far as I understand discouraged) imperative pushing to the history (e.g. in componentWillMount)?
Solution 1
You could use withRouter HOC to access history via props.
Import withRouter.
import {
withRouter
} from 'react-router-dom';
Then wrap with HOC.
// Example code
export default withRouter(connect(...))(Component)
Now you can access this.props.history. For example use it with componentDidMount().
componentDidMount() {
const { history } = this.props;
if (this.props.authenticated) {
history.push('/private-route');
}
}
Solution 2 Much better
Here is example on reacttraining.
Which would perfectly work for you.
But you just need to create LoginRoute to handle problem you described.
const LoginRoute = ({ component: Component, ...rest }) => (
<Route
{...rest} render={props => (
fakeAuth.isAuthenticated ? (
<Redirect to={{
pathname: '/private-route',
state: { from: props.location }
}} />
) : (
<Component {...props} />
)
)} />
);
and inside <Router /> just replace
<Route path="/login" component={Login}/>
with
<LoginRoute path="/login" component={Login}/>
Now everytime somebody will try to access /login route as authenticated user, he will be redirected to /private-route. It's even better solution because it doesn't mount your LoginComponent if condition isn't met.
Here is another solution which doesn't touch React stuff at all. E.g. if you need to navigate inside redux-saga.
Have file history.js:
import {createBrowserHistory} from 'history';
export default createBrowserHistory();
Somewhere where you define routes, don't use browser router but just general <Router/>:
import history from 'utils/history';
...
<Router history={history}>
<Route path="/" component={App}/>
</Router>
That's it. Now you can use same history import and push new route.
In any part of your app:
import history from 'utils/history';
history.push('/foo');
In saga:
import {call} from 'redux-saga/effects';
import history from 'utils/history';
...
history.push('/foo');
yield call(history.push, '/foo');

Categories

Resources