Await for react-router history.goBack to complete - reactjs

I'm in a route that indexes an array in state. When clicking a button, I want to delete that item out of state. Before doing this, I want to go back to another route that doesn't use the item. I do this to avoid a TypeError when indexing the item that no longer exists.
Is it possible to wait for the route change to complete before updating state? It seems like there is no promise capability with useHistory.
Minimal example:
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter, Switch, Route, useHistory } from 'react-router-dom'
class App extends Component {
constructor (props) {
super(props)
this.state = {
stuff: [{
message: 'hello'
}]
}
}
componentDidMount () {
console.log('mounted')
}
render () {
return (
<Switch>
<Route path='/1'>
<Child
stuff={this.state.stuff} callback={() => {
this.props.history.goBack()
this.setState({
stuff: []
})
}}
/>
</Route>
<Route path='/'>
<button onClick={() => this.props.history.push('/1')}>
Go to friend
</button>
home
</Route>
</Switch>
)
}
}
const Child = ({ callback, stuff }) =>
<>
<button onClick={callback}>
Go back
</button>
{stuff[0].message} friend
</>
const withUseHistory = (Component) => {
return function WrappedComponent (props) {
const history = useHistory()
return <Component {...props} history={history} />
}
}
const AppWithHistory = withUseHistory(App)
const wrapper = document.getElementById('root')
ReactDOM.render(
<BrowserRouter>
<AppWithHistory />
</BrowserRouter>,
wrapper)

I've found a solution through using history.push instead of history.goBack:
this.props.history.push('/')
this.setState({
stuff: []
})
It seems history.push is synchronous while goBack is async.

history.goBack();
// Register a popstate listener that executes only once during the next history entry change
window.addEventListener('popstate', onPopStateCallback, {once: true});
We can register a callback for the next history entry(pop state) update

Related

Router Router Switch causes unmount on updating route search/query strings

In my App.js I have a number of components wrapped in a Switch component from react-router-dom
App.js
import React from "react";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
const Test = Loadable({
loader: () => import("./Test"),
loading: () => <h1>LOADING....</h1>
});
const Test1 = Loadable({
loader: () => import("./Test1"),
loading: () => <h1>LOADING....</h1>
});
const Test2 = Loadable({
loader: () => import("./Test2"),
loading: () => <h1>LOADING....</h1>
});
const App = () => {
return (
<Switch>
<ProtectedRoute bgColour="blue" exact path="/" component={Test} />
<ProtectedRoute bgColour="red" exact path="/1" component={Test1} />
<ProtectedRoute bgColour="green" exact path="/2" component={Test2} />
</Switch>
);
};
export default App;
The ProtectedRoute component renders a Route component from react-router-dom passing in the specified component. It also has a HOC, which in my actual application checks the user is authenticated
ProtectedRoute.js
import React from "react";
import { Route } from "react-router-dom";
const withAuth = (Component) => {
return (props) => {
return <Component {...props} />;
};
};
const ProtectedRoute = ({ component, bgColour, ...args }) => {
return (
<div style={{ backgroundColor: bgColour || "transparent" }}>
<Route component={withAuth(component)} {...args} />
</div>
);
};
export default ProtectedRoute;
For each component, I have alerts setup to trigger on mount and unmount of the component. On a click on an element it updates the query string to a random number via history.push, however, this currently triggers an unmount, due to the Switch added in App.js, without the Switch there is no unmount. This is causing an issue in my application as an unmount is not desired behaviour and is causing issues with loading the correct data.
Test.js
import React, { useEffect } from "react";
import { useHistory } from "react-router-dom";
export default function Test() {
const history = useHistory();
useEffect(() => {
alert("MOUNTED BASE");
return () => {
alert("UNMOUNTED BASE");
};
}, []);
return (
<div>
<h1>TEST COMPONENT BASE - BLUE</h1>
<div
onClick={() =>
history.push({
pathname: history.location.pathname,
search: `?query=${Math.random().toFixed(2)}`
})
}
>
UPDATE QUERY STRING
</div>
<div onClick={() => history.push("/1")}>GO TO Next ROUTE</div>
</div>
);
}
I still want the functionality of the Switch but prevent the unmount on history.push, is this possible?
I have a CodeSandbox below to recreate this issue
Issue
Based on only the code you've provided, an issue I see is how every component the ProtectedRoute renders is decorated with the withAuth Higher Order Component. This results in a new component being created every time ProtectedRoute renders.
Solution
You want to only decorate the routed components with HOCs once prior to where they are used.
Example:
const ProtectedRoute = ({ bgColour, ...props }) => {
return (
<div style={{ backgroundColor: bgColour || "transparent" }}>
<Route {...props} />
</div>
);
};
...
import React from "react";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
import withAuth from "..path/to/withAuth";
// Decorate components with HOCs once out here
const Test = withAuth(Loadable({
loader: () => import("./Test"),
loading: () => <h1>LOADING....</h1>
}));
const Test1 = withAuth(Loadable({
loader: () => import("./Test1"),
loading: () => <h1>LOADING....</h1>
}));
const Test2 = withAuth(Loadable({
loader: () => import("./Test2"),
loading: () => <h1>LOADING....</h1>
}));
// Render decorated components in App
const App = () => {
return (
<Switch>
<ProtectedRoute bgColour="red" path="/1" component={Test1} />
<ProtectedRoute bgColour="green" path="/2" component={Test2} />
<ProtectedRoute bgColour="blue" path="/" component={Test} />
</Switch>
);
};

TypeError: Cannot read property 'params' of undefined in React

I am new to react and I try to pass Id from a component to another component. To do that I used
<a className="btn btn-view" href={`/buyer/viewpostdetails/${posts._id}`}>View Post <i className="fas fa-angle-double-right"></i></a>
this code. It works correctly and shows the URL correctly with the ID.
Then I tried to get that Id
componentDidMount(){
const id = this.props.match.params.id;
axios.get(`/post/${id}`).then((res) => {
if (res.data.success) {
this.setState({
post:res.data.post
});
console.log(this.state.post);
}
});
}
I used the above code to do that but I got an error
TypeError: Cannot read property 'params' of undefined
How do I solve this issue?
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import { useEffect, useState } from "react";
// import axios from "axios";
export default function App() {
return (
<div className="App">
<Router>
<Switch>
<Route path="/" component={FirstPage} exact />
<Route path="/:id" component={SecondPage} />
</Switch>
</Router>
</div>
);
}
function FirstPage() {
const [post, setPost] = useState({ _id: 5 });
return (
<div>
<div>First Page</div>
<Link to={`/${post._id}`}>Link</Link>
</div>
);
}
function SecondPage(props) {
const [state, setState] = useState();
useEffect(() => {
const id = props.match.params.id;
setState(id);
// axios.get(`/post/${id}`).then((res) => {
// if (res.data.success) {
// setState(res.data.post);
// }
// });
}, []);
return <div>SecondPage id: {state}</div>;
}
You must do something like that.
First you define a router.
Then you can access the id from the relevant page or component.
And I suggest you to define components as functions and use useEffect and useState hooks.

How to link to a component using persistant header that's outside of BrowserRouter?

I'm attempting to link to somewhere within my application using react-router-dom within an appBar/header that is persistent throughout the app. I keep getting "TypeError: history is undefined" when I attempt to use RRD within the header component.
I've been playing around with this for a good few hours now and I'm not getting any where with it. I can't think straight thanks to the heat, and I'm clearly searching for the wrong answers in my searches. The best solution I have come-up with thus-far is having each component contain the header component at the top but this is obv not ideal. I know I must be missing something simple as this can't be an uncommon pattern.
Demo Code
Node Stuff
npx create-react-app rdr-header --template typescript
npm install react-router-dom
App.tsx
import React from "react";
import "./App.css";
import {
BrowserRouter as Router,
Switch,
Route,
useHistory,
} from "react-router-dom";
function App() {
let history = useHistory();
const handleClick = (to: string) => {
history.push(to);
};
return (
<div className='App'>
<header className='App-header'>
<button onClick={() => handleClick("/ger")}>German</button>
<button onClick={() => handleClick("/")}>English</button>
</header>
<Router>
<Switch>
<Route exact path='/' component={English} />
<Route path='/ger' component={German} />
</Switch>
</Router>
</div>
);
}
const English = () => {
let history = useHistory();
const handleClick = () => {
history.push("/ger");
};
return (
<>
<h1>English</h1>
<button onClick={handleClick}>Go to German</button>
</>
);
};
const German = () => {
let history = useHistory();
const handleClick = () => {
history.push("/");
};
return (
<>
<h1>German</h1>
<button onClick={handleClick}>Go to English</button>
</>
);
};
export default App;
You should create separate component for header
header.js
import React from 'react';
import './style.css';
import { useHistory } from 'react-router-dom';
function Header() {
let history = useHistory();
const handleClick = to => {
history.push(to);
};
return (
<header className="App-header">
<button onClick={() => handleClick('/ger')}>German</button>
<button onClick={() => handleClick('/')}>English</button>
</header>
);
}
export default Header;
Use Header component inside Router like below:-
import React from 'react';
import './style.css';
import {
BrowserRouter as Router,
Switch,
Route,
useHistory
} from 'react-router-dom';
import Header from './header.js'; // import header component
function App() {
return (
<div className="App">
<Router>
<Header /> // use Header component inside Router
<Switch>
<Route exact path="/" component={English} />
<Route path="/ger" component={German} />
</Switch>
</Router>
</div>
);
}
const English = () => {
let history = useHistory();
const handleClick = () => {
history.push('/ger');
};
return (
<>
<h1>English</h1>
<button onClick={handleClick}>Go to German</button>
</>
);
};
const German = () => {
let history = useHistory();
const handleClick = () => {
history.push('/');
};
return (
<>
<h1>German</h1>
<button onClick={handleClick}>Go to English</button>
</>
);
};
export default App;
Instead of changing the history object using history.push(), you can use the <Link> or <NavLink> components from react-router.
React Router - Link component
Make sure to place the header component inside the Router component.

How to pass state in history.push in React-Router

I am not able to send the parameter through state using useHistory history.push method from react-router dom.
Now suppose I want to pass more than a string to the Paging component i.e. some props too.
My Paging Component which throws error for state value state is not defined
const PAGING = ({ location }) => {
console.log(location);
console.log(location.state);
console.log(location.state.id);
return <div>Hello <div>}
History.push method in another component
const handleDetails = (id,name) => {
console.log(name)
if (id) {
return history.push({
pathname: `/detailing/${name}`,
state: { id }
});
} else {
return history.push("/");
}
};
const Switch = () => {
const { state: authState } = useContext(AuthContext)
return (
<div>
<Router>
<Switch>
<ProtectedSystem
path= "/detailing/:name"
exact
auth={authState.isAuthenticated}
component={PAGING}
/>
</Switch>
</Router>
</div>
);
const ProtectedSystem = ({auth , component: Component, ...rest}) =>{
return(
<Route
{...rest}
render={() => auth ? (<Component/>) : (<Redirect to = '/' /> )}
/>
)
}
If I use simple route without condition based its working fine
<Route path= "/detailing/:name" exact component={PAGING} />
You need to pass on the Route params to the rendered component so that it can use them
const ProtectedSystem = ({auth , component: Component, ...rest}) =>{
return(
<Route
{...rest}
render={(routeParams) => auth ? (<Component {...routeParams}/>) : (<Redirect to = '/' /> )}
/>
)
}
You can do this entirely with React hooks and pure functions, eg.
import React from 'react';
import { useHistory } from 'react-router-dom';
const ProtectedSystem = ({ auth }) => {
const history = useHistory();
if (!authUser) {
history.push("/signin");
}
return (
<div><h1>Authorized user</h1></div>
)
}
export default ProtectedSystem

I can't implement Redirect in React

I want to redirect to the home page when some condition returns null or false but the action of Redirect is not triggered.
import { Link, Redirect } from "react-router-dom";
if(localStorage.getItem("example") === null || localStorage.getItem("example") === false){
return <Redirect to="/" />
}
I put this code inside in a simple function triggered in one OnClick and componentDidMount(), but it's not working.
You could use Redirect to home page, based on redirect flag that could be changed by using setState in onClickHandler or handleSubmit.
import { Redirect } from "react-router-dom";
class MyComponent extends React.Component {
state = {
redirect: false
}
handleSubmit () {
if(localStorage.getItem("example") === null || localStorage.getItem("example") === false){
return this.setState({ redirect: true });
}
}
render () {
const { redirect } = this.state;
if (redirect) {
return <Redirect to='/'/>;
}
return <YourForm/>;
}
You need to use the Redirect inside render. It is a React Component which renders and then sends the user to the desired path:
import React, { Component } from "react";
import { Route, Switch, Redirect } from "react-router-dom";
class RootPage extends React.Component {
state = {
isLoggedOut: false
};
onClick = () => {
this.setState({
isLoggedOut: true
});
};
render() {
return (
<div>
{this.state.isLoggedOut && <Redirect to="/logout" />}
<button onClick={this.onClick}>Logout</button>
</div>
);
}
}
const Log = () => <h1>Logout</h1>;
class App extends Component {
render() {
return (
<div>
<nav className="navbar navbar" />
<Switch>
<Route exact path="/" component={RootPage} />
<Route exact path="/logout" component={Log} />
</Switch>
</div>
);
}
}
export default App;
When you click on the logout button it will redirect you to the rootPath.
Here is the Demo: https://codesandbox.io/s/q9v2nrjnx4
Have a look at this example in the official docs.
<Redirect /> should be inside your render method if you use a class component. or if you use a function component it should be in what's returned by it.
Example bellow:
import { Component } from 'react';
const PrivateComponent = (props) => {
return(
localStorage.getItem("example")
? <RandomComponent />
: <Redirect to="/signin" />
)
}

Resources