I came back to react world after a few years. And things certainly have changed for good. I'm using MemoryRouter for my app. And I can navigate fine by using Link. But useNaviate hook is not working as expected. It does nothing on the page. Could you please help me here? Here is my code:
Router:
<MemoryRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</MemoryRouter>
Here is how I'm trying the navigation:
function Home() {
// demo purpose
const navigate = useNavigate()
navigate('/dashboard')
}
I'm not sure if I'm using it right, or if I need to do something else here.
The code is calling navigate as an unintentional side-effect directly in the function component body.
Either call navigate from a component lifecycle or callback to issue an imperative navigation action:
function Home() {
const navigate = useNavigate()
useEffect(() => {
if (/* some condition */) {
navigate('/dashboard');
}
}, [/* dependencies? /*]);
...
}
Or conditionally render the Navigate component to a declarative navigation action:
function Home() {
...
if (/* some condition */) {
return <Navigate to="/dashboard" />;
};
...
}
The problem was that I was calling navigate directly when the component was rendering. It should either be called in an event, or it should be called in useEffect hook.
Make your navigate in function call or in useEffect like this:
function Home() {
// demo purpose
const navigate = useNavigate()
useEffect(() => {
navigate('/dashboard')
}, []);
}
Related
I use the useEffect hook to dispatch the getQuestions function in order to get the data from the server
function App () {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getQuestions());
}, [dispatch]);
return (
<Routes>
<Route exact path="/" element={<Layout/>}>
<Route path="repetition" element={<Repetition/>}/>
<Route path="family" element={<Family/>}/>
</Route>
</Routes>
);
}
The problem is that when I, for example, open the family link (which I declared in the App function), initially I get the data, but when I refresh the page, the data disappears.
I certainly understand that when the page is refreshed the parent App component is not rendered from this and I get an error, similar issues I have looked at in the forums where it was suggested to use withRouter which updates the parent component, but my version of react-router-dom does not supports withRouter, except that I don't want to downgrade my version of react-router-dom to use withRouter.
I would like to know if there is any way to fix this problem.
I tried the option that #Fallen suggested, i.e. I applied the useEffect hook in each child element and analyzed this approach in GoogleLighthouse, and I'm happy with the results.
Here is my final code in child component
function Family () {
const dispatch = useDispatch();
const questions = useSelector(state => state.QuestionsSlices.familyQuestions);
useEffect(() => {
dispatch(getFamilyQuestions());
}, [dispatch]);
return (
<>
{questions.data.map((item, idx) => (
<div key={idx}>
{ idx + 1 === questions.score && CheckQuestionsType(item, questions) }
</div>
))}
</>
);
}
I have the following line of code:
<Route path="/users/:id" exact ><User url={structure.users} /></Route>
In User component I do the following:
function User(props, {match}) {
useEffect(() => {
console.log(match);
console.log(props);
}, [props]);
// ...
}
I removed a bit of the code because I only want to show what is necessary but here I see that match is undefined but it should have match.params.id from the link. How can I achieve this so i can make the full url with the url with the attached id?
React components receive only a single props object argument.
Use the Route's render prop so you can pass the route props on to the component.
<Route
path="/users/:id"
exact
render={routeProps => <User url={structure.users} {...routeProps} />}
/>
Alternatively you can use the useParams React hook to access match.params of the current Route.
import { ...., useParams, .... } from 'react-router-dom';
function User(props) {
const { id } = useParams();
useEffect(() => {
console.log(id);
console.log(props);
}, [props]);
// ...
}
I have following App component:
<Route render={( { location } ) => (
<TransitionGroup component="div" className="content">
<CSSTransition key={location.key} classNames="slide" timeout={{
enter: 1000,
exit: 300
}} appear>
<Switch location={location}>
<Route exact path='/' component={Intro}/>
<Route path="/history" component={History}/>
<Route path="/rules" component={Rules}/>
<Route path="/faq" component={Faq}/>
<Route path="/feedback" component={Feedback}/>
<Route path="/partners" component={Partners}/>
</Switch>
</CSSTransition>
</TransitionGroup>
)}/>
And it works fine, but every animation executes immediately. For example, if I go from /rules to /history, I got full animation on both components, but history component require data from the server, so animation applied on empty container.
How could I pause animation in react-transition-group components? I have Redux, so I could change loading variable anywhere in my app. Also, I don't want to preload all data in the store on app start.
I would make your component return null when it's loading and make the loading state determine the CSSTransition key like <CSSTransition key={location.key+loading?'-loading':''}
see example here: https://stackblitz.com/edit/react-anim-route-once
note that to make this work without duplication I had to make the component copy the loading prop and persist it in state, so that one of the copies of the component never displays (which would create a duplication of the component as seen here: https://stackblitz.com/edit/react-anim-route-twice)
<Route render={({ location }) => (
<TransitionGroup component="div" className="content">
<CSSTransition key={location.key+(this.state.loading?'-loading':'-loaded')} classNames="crossFade" timeout={{
enter: 3000,
exit: 3000
}} appear>
<Switch location={location} >
<Route exact path='/' component={Hello} />
<Route exact path='/history' render={() =>
<Delayed setLoading={this.handleSetLoading} loading={this.state.loading} />} />
</Switch>
</CSSTransition>
</TransitionGroup>
)} />
and in the component something like this:
export default class History extends React.Component {
state={loading:this.props.loading}
componentDidMount() {
setTimeout(() => {
this.props.setLoading(false);
}, 2000);
}
render() {
return !this.state.loading ? <div><h1>History! <Link to="/">Home</Link></h1></div> : null;
}
}
So my cases have been a bit different but they might help you think of a solution.
You can delay the initial display easily by adding an if (this.state.isloaded == true) block around your whole router. Start loading when your component mounts, and when the async call completes, setState({isloaded: true}).
You can make your own <Link> component, which launches a request, and only once it’s complete changes the page location. You can do whatever special loading spinners you like in the meantime.
Basically, keep the routing and transition components to one side. I find them to be brittle and painful with cases like this. Let me know if you want any more details or snippets.
I've done peloading through redux and redux-saga. Maybe it's one and only way to achieve following with react-router and react-transition-group, because transition toggle animation anytime when render method is run, even if it return null.
I've implemented following actions:
const LoadingActions = {
START_LOADING: 'START_LOADING',
STOP_LOADING: 'STOP_LOADING',
REDIRECT: 'REDIRECT',
startLoading: () => ({
type: LoadingActions.START_LOADING
}),
stopLoading: () => ({
type: LoadingActions.STOP_LOADING
}),
redirect: ( url, token ) => ({
type: LoadingActions.REDIRECT,
payload: {
url,
token
}
})
};
export default LoadingActions;
In the reducers I've implemented simple loader reducer, that will toggle on and off loading variable:
import { LoadingActions } from "../actions";
const INITIAL_STATE = {
loading: false
};
export default function ( state = INITIAL_STATE, { type } ) {
switch ( type ) {
case LoadingActions.START_LOADING:
return { loading: true };
case LoadingActions.STOP_LOADING:
return { loading: false };
default:
return state;
}
}
The most irritating thing is reducer chain - this.props.loader.loading. Too complex for such simple thing.
import { combineReducers } from "redux";
...
import LoadingReducer from './LoadingReducer';
export default combineReducers( {
...
loader: LoadingReducer
} );
This most work goes in saga:
function* redirect ( action ) {
yield put( LoadingActions.startLoading() );
const { url } = action.payload;
switch ( url ) {
case MENU_URL.EXCHANGE:
yield call( getExchangeData, action );
break;
... other urls...
}
yield put( LoadingActions.stopLoading() );
BrowserHistory.push( url );
}
... loaders ...
function* watchRedirect () {
yield takeLatest( LoadingActions.REDIRECT, redirect );
}
const sagas = all( [
...
fork( watchRedirect )
] );
export default sagas;
I put listener on redirect action, so it will call redirect generator. It will start loading and call data preloading yield call will await for preload to finish and after it will stop loading and redirect. Though it won't wait for positive result, so preloaders should handle errors themselves.
I hoped that I could avoid redux complexity with built-in feature of router or transition library, but it has no such tools to stop transition. So it is one of the best way to achieve transition with preloded data.
I have an issue with setState().
Here is my routes.tsx file
export const Routes = (props:any) => (
<Router {...props}>
<Route path="/" component={Miramir}>
<Route path="/profile">
<IndexRoute component={Profile} />
<Route path="/profile/update" component={ProfileUpdate} />
</Route>
</Route>
So, when i trying to use /profile/update route
I have an warning and see that component which only for /profile route exists on /profile/update
This is an error
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the CountDown component.
Component CountDown
componentDidMount = () => {
let timeLeft = this.secondsToTime(this.state.date);
this.setState({time: timeLeft});
}
_countDown = () => {
this.setState({
time: this.secondsToTime(this.state.date)
})
}
Calling _countDown every sec
Hope your help!
Thanks
You probably call _countDown() via setInterval() I'd imagine. Did you clear that Interval in componentWillUnmount()?
you can not call the _countDown before the component mount (it means the component render method is already called and then you calling _countDown function).
calling _countDown inside setInterval may solve, but may fail again if you render method take more time than time provided in setInterval.
_countDown = () => {
this.setState({
time: this.secondsToTime(this.state.date)
})
}
I am using react-router 2. My routes are defined as
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="/about" component={About}/>
<Route path="/login" component={Login} onEnter={redirectToDashboard}/>
<Route path="/logout" component={Logout} onEnter={logoutSession}/>
<Route path="/dashboard" component={Dashboard} onEnter={redirectToLogin}/>
</Route>
Everything working fine but I am having problem disabling back button from my dashboard page.
After successful login I am redirecting user to dashboard page but when user clicks back button it goes to login page again. I want to disable back button of browser when user is on dashboard page.
Your best bet, is when the user is login he/ she is redirected to dashbaord. if for some reason the user click on back button you should:
if the user is logged in
stay on the page dashboard
if(logged) {
history.pushState(null, null, location.href);
window.onpopstate = function(event) {
history.go(1);
};
}
it will be not possible to go back.
Applying all these hacks the URL changes to login for a moment and then to wherever-we-push.
Instead, what we can do is: In login, where api endpoint returns success, do:
history.replace('/Whatever_screen')
This will remove login screen from window.history stack, and the screen will not flicker.
On your page which you want to disable back (example, on LoginApp ) add this block, to disable web history back
componentDidMount() {
window.history.pushState(null, document.title, window.location.href);
window.addEventListener('popstate', function (event){
window.history.pushState(null, document.title, window.location.href);
});
}
it's not possible to disable browser buttons.
my advice is to redirect user back to dashboard page if he/she is logged
Actually you can't disable back button. You can use a hack by preventing browser's "back" action. Just add to your Dashboard component compnentWillMount() lifecycle method some code that will trigger browser's "forward" action:
componentWillMount() {
setTimeout(() => {
window.history.forward()
}, 0)
window.onunload=function(){null};
}
But most probably a better solution would be some redirection based on users logged state.
in your login screen add replace to /dashboard
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom'
import createBrowserHistory from 'history/createBrowserHistory'
const history = createBrowserHistory()
class LoginPage extends Component {
componentDidMount(){
history.replace({ pathname: '/dashboard' })
}
render() {
const { history } = this.props
return (
<div>
<h1>Login Page</h1>
<button onClick={() => {
login().then(() => {
history.push('/dashboard')
})
}}>Login</button>
</div>
);
}
}
export default withRouter(LoginPage);
The reason is replace your current path (/login) to /dashboard. Before adding this, please make sure you setup your authentication correctly.
In order to improve the code reusability, we can add an event listener in our index.html and dispatch the browser back disable event from all of our componentDidMount() methods.
In the index.html,
window.addEventListener('navigationhandler', function (e) {
window.history.pushState(null, document.title, window.location.href);
window.addEventListener('popstate', function (event) {
window.history.pushState(null, document.title, window.location.href);
});
});
In React componentDidMount() method,
componentDidMount() {
window.dispatchEvent(new CustomEvent("navigationhandler"));
}
It's not possible to disable browser buttons. But we can use history methods like listen(), go() and push() to override the default behaviour of back button in react.js. Also try to use withRouter().
The following is the sample code for doing this. Please look into componentDidMount() and componenetDidUnmount() methods.
import React from "react";
import { Redirect, Switch, Route, withRouter } from "react-router";
import Page1 from "./Page1";
import Page2 from "./Page2";
import Page3 from "./Page3";
class App extends React.Component {
constructor(props) {
super(props);
// Store the previous pathname and search strings
this.currentPathname = null;
this.currentSearch = null;
}
componentDidMount() {
const { history } = this.props;
history.listen((newLocation, action) => {
if (action === "PUSH") {
if (
newLocation.pathname !== this.currentPathname ||
newLocation.search !== this.currentSearch
) {
this.currentPathname = newLocation.pathname;
this.currentSearch = newLocation.search;
history.push({
pathname: newLocation.pathname,
search: newLocation.search
});
}
} else {
history.go(1);
}
});
}
componentWillUnmount() {
window.onpopstate = null;
}
render() {
return (
<Switch>
<Route exact path="/" render={() => <Redirect to="/page1" />} />
<Route path="/page1" component={Page1} />
<Route path="/page2" component={Page2} />
<Route path="/page3" component={Page3} />
</Switch>
);
}
}
export default withRouter(App);
For more: Disable react back button
Simple and Sweet.
No need to mess with history stack.
solution even does not depend on react router.
It will even prevent the current component from unmounting when back button is clicked, hence it also preserves state of the app as well.
// prevent back
useEffect(() => {
window.addEventListener('popstate', (e) => {
window.history.go(1);
});
}, []);