React Router - Cannot read property 'history' of undefined - javascript

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);
});

Related

Nested routing using protected routes is not working properly

earlier I posted a similar question: Method for checking admin is redirecting me to main page when trying to login
I tried to implement the protected route, inside a protectRoute.tsx:
import { Navigate, Outlet } from "react-router";
import { RootStateOrAny, useSelector } from "react-redux";
interface ProtectRouteProps {}
export default function ProtectRoute(props: ProtectRouteProps) {
const userSignin = useSelector((state: RootStateOrAny) => state.userSignin);
const { userInfo } = userSignin;
return userInfo?.user?.isAdmin ? <Outlet /> : <Navigate to='/' />;
}
I don't really know what ProtectRouteProps is and what I should put in it. Also in the routing part I did like he told me:
<Route path='/createItem' element={<ProtectRoute />} />
<Route path='/createItem' element={<CreateItem />} />
The problem now is that can't access CreateItem because is going on the ProtectRoute page that is an empty one. What should i do?
I don't really know what ProtectRouteProps is and what I should put in it.
There are no props. This is clear by the usage:
<Route path='/createItem' element={<ProtectRoute />} />
No props are passed to ProtectRoute. You can drop the props object:
import { Navigate, Outlet } from "react-router";
import { RootStateOrAny, useSelector } from "react-redux";
export default function ProtectRoute() {
const userSignin = useSelector((state: RootStateOrAny) => state.userSignin);
const { userInfo } = userSignin;
return userInfo?.user?.isAdmin ? <Outlet /> : <Navigate to='/' replace />;
}
The problem now is that can't access CreateItem because is going on
the ProtectRoute page that is an empty one. What should i do?
"Auth" routes are what are called layout routes. They apply some logic, perhaps some styled layout CSS, and render an Outlet for nested Route components to be rendered into. The nested Route components use the path prop for route matching.
Example:
<Route element={<ProtectRoute />}>
<Route path='/createItem' element={<CreateItem />} />
... other protected routes ...
</Route>
<Route exact path='/Login' element={<Login name="Login Page"></Login>}></Route>
<Route element={<Protected/>}>
<Route exact path='/' element={<Home/> }></Route>
<Route exact path='/Content' element={<Content />}></Route>
</Route>
<Route path='*' element={<Login/>} ></Route>
</Routes>
Create Protected.js
import { Navigate, Outlet } from 'react-router-dom';
const useAuth = ()=>{
if(localStorage.getItem("isLogged")){
const user = {loggedIN :true};
return user && user.loggedIN
}else{
const user = {loggedIN :false};
return user && user.loggedIN
}
}
const Protected = () => {
const isAuth = useAuth();
return isAuth ? <Outlet/>:<Navigate to={"/login"}/>
}
export default Protected

Timed out in waitForElementToBeRemoved error in RTL when using routing in useEffect react hook

I have tried different solutions to resolve it but none is working. I am using React testing library to test the Login component. I have different routes stacked like this:
<BrowserRouter>
<div>
<Home />
<Switch>
<Route path="/" exact component={Home} />
<Route path="/user" exact component={User} />
<Route path="/login" component={Login} />
<Route component={NotFound} />
</Switch>
</div>
</BrowserRouter>
This is my Login Component.
export const Login = () => {
const history = useHistory();
useEffect(() => {
console.log("In useEffect");
history.push("/user");
}, []);
return <div data-testid="user-login">In progress</div>;
};
I have written test case like this:
it("redirects to /user", async () => {
testRender(<Login />);
await waitForElementToBeRemoved(() =>
screen.findByTestId("user-login")
);
screen.debug();
});
Here is my testRender code:
import React from "react";
import { render } from "#testing-library/react";
import { MemoryRouter } from "react-router-dom";
export function testRender(tsx) {
window.history.pushState({}, "Test page", "/");
return render(tsx, { wrapper: MemoryRouter });
}
When I run the test case for:
expect(getByTestId("user-login").textContent).toBe(
"In progress"
);
It is passing but 'waitForElementToBeRemoved' case is not working (error- 'Timed out in waitForElementToBeRemoved'). The component does executes the useEffect but history.push("/user"); is not working it seems. Please let me know if there a better way to test this kind of scenario.

React: "history" prop is not available in a private route

I'm using React 16, React-router-dom 4 and Mobx in my app.
I have this code for a private route:
export default (props) => {
console.log('props from private',props)//Here i can see that the component doesn't contain the "history" prop.
const Component = props.component;
const match = props.computedMatch
if (isValidated()) {
return (
<div>
<div><Component {...props} match={match} /></div>
</div>
)
} else {
return <Redirect to="/login" />
}
};
This is the routing setup:
export const history = createHistory();
const AppRouter = () => (
<Router history={history}>
<Switch>
<PrivateRoute path="/" component={Chat} exact={true} />
<Route path="/login" component={Login} />
</Switch>
</Router>
);
For some reason, the history prop just doesn't exist in the private route, therefore i'm unable to use the this.props.history.push function, to redirect programatically. The prop does get passed to a "normal" route though.
What is wrong with my code?
Use below:
import {withRouter} from 'react-router-dom';
wrap component with withRouter.
withRouter(component_name)

React router, redux, electron: router isn't rendering

I'm trying to get react-router to work in an electron app. I'm using a boilerplate and stripped away some of the stuff I don't want but that should provide a pretty solid summary of the technologies being used.
I cannot for the life of my get the router to work as expected. To me this means that when I use history.push('/someroute/') or <Link to="someroute"..., the corresponding component to that router should be rendered and the previous one gone.
Here is my routes file:
export default () => (
<App>
<Switch>
<Route path="/" component={MyDevkitsPage} />
<Route path="/browser/" component={DevkitBrowserPage} />
<Route path="/Console/" component={ConsolePage} />
<Route path="/Controller/:devkitIp" component={ControllerPage} />
<Route path="/Setting/:devkitIp" component={SettingPage} />
</Switch>
</App>
);
I would expect that if <Link to="/browser/"> aaaa </Link> is clicked, it would go to the /browser/ component. Instead, I get an error:
Warning: Hash history cannot PUSH the same path; a new entry will not be added to the history stack
Likewise, if I move the path="/browser/" line above the path="/" line in routes, it will always go to /browser/.
edit: RootContainer
export default function Root({ store, history }: RootType) {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<Routes />
</ConnectedRouter>
</Provider>
);
}
Edit 2: Configure store
const configureStore = (initialState?: counterStateType) => {
// Redux Configuration
const middleware = [];
const enhancers = [];
// Thunk Middleware
middleware.push(thunk);
// Logging Middleware
const logger = createLogger({
level: 'info',
collapsed: true
});
middleware.push(logger);
// Router Middleware
const router = routerMiddleware(history);
middleware.push(router);
// Redux DevTools Configuration
const actionCreators = {
...counterActions,
...routerActions,
};
// If Redux DevTools Extension is installed use it, otherwise use Redux compose
/* eslint-disable no-underscore-dangle */
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Options: http://zalmoxisus.github.io/redux-devtools-extension/API/Arguments.html
actionCreators,
})
: compose;
/* eslint-enable no-underscore-dangle */
// Apply Middleware & Compose Enhancers
enhancers.push(applyMiddleware(...middleware));
const enhancer = composeEnhancers(...enhancers);
// Create Store
const store = createStore(rootReducer, initialState, enhancer);
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers')) // eslint-disable-line global-require
);
}
return store;
};
You will need to change <Route path="/" component={MyDevkitsPage} /> to <Route exact path="/" component={MyDevkitsPage} />.
By adding exact to that <Route /> you will prevent react-router from trying to load both <MyDevkitsPage /> and <DevkitBrowserPage /> at the same time.
I had the same issue. One thing that I notice is that if I place the route path after all the others, it will not throw the exception.
My Code:
export default () => (
<App>
<Switch>
<Route path="/counter" component={CounterPage} />
<Route path="/calculator" component={Calculator} />
<Route path="/" component={HomePage} />
</Switch>
</App>
);
This code worked for me I've changed BrowseRouter to MemoryRouter
import React from 'react'
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
import './App.css';
import routes from './Route';
function App() {
return (
<>
<Router>
<Routes>
{routes.map((route) => {
return <Route path={route.path} element={route.element} />
})}
</Routes>
</Router>
</>
);
}
export default App;

Accessing Redux Store from routes set up via React Router

I would like to make use of react-router's onEnter handler in order to prompt users to authenticate when entering a restricted route.
So far my routes.js file looks something like this:
import React from 'react';
import { Route, IndexRoute } from 'react-router';
export default (
<Route path="/" component={App}>
<IndexRoute component={Landing} />
<Route path="learn" component={Learn} />
<Route path="about" component={About} />
<Route path="downloads" component={Downloads} onEnter={requireAuth} />
</Route>
)
Ideally, I'd like my requireAuth function to be a redux action that has access to the store and current state, that works like this: store.dispatch(requireAuth()).
Unfortunately I don't have access to the store in this file. I don't think I can use really use connect in this case to access the relevant actions that I want. I also can't just import store from the file where the store is created, as this is undefined when the app first loads.
The easiest way to accomplish this is to pass your store to a function that returns your routes (rather than return your routes directly). This way you can access the store in onEnter and other react router methods.
So for your routes:
import React from 'react';
import { Route, IndexRoute } from 'react-router';
export const getRoutes = (store) => (
const authRequired = (nextState, replaceState) => {
// Now you can access the store object here.
const state = store.getState();
if (!state.user.isAuthenticated) {
// Not authenticated, redirect to login.
replaceState({ nextPathname: nextState.location.pathname }, '/login');
}
};
return (
<Route path="/" component={App}>
<IndexRoute component={Landing} />
<Route path="learn" component={Learn} />
<Route path="about" component={About} />
<Route path="downloads" component={Downloads} onEnter={authRequired} />
</Route>
);
)
Then update your main component to call the getRoutes function, passing in the store:
<Provider store={ store }>
<Router history={ history }>
{ getRoutes(store) }
</Router>
</Provider>
As for dispatching an action from requireAuth, you could write your function like this:
const authRequired = (nextState, replaceState, callback) => {
store.dispatch(requireAuth()) // Assume this action returns a promise
.then(() => {
const state = store.getState();
if (!state.user.isAuthenticated) {
// Not authenticated, redirect to login.
replaceState({ nextPathname: nextState.location.pathname }, '/login');
}
// All ok
callback();
});
};
Hope this helps.
If you want that you could write route.js like this:
var requireAuth = (store, nextState, replace) => {
console.log("store: ", store);
//now you have access to the store in the onEnter hook!
}
export default (store) => {
return (
<Route path="/" component={App}>
<IndexRoute component={Landing} />
<Route path="learn" component={Learn} />
<Route path="about" component={About} />
<Route path="downloads" component={Downloads} onEnter={requireAuth.bind(this, store)} />
</Route>
);
);
I've setup an example which you could play with in this codepen.
Not sure if triggering an action in order to handle the auth is a good idea. Personally I prefer handling auth in a different way:
Instead of using an onEnter hook, I use a wrapping function. I want the admin section of my blog protected, therefore I wrapped the AdminContainer component in the routes with a function, requireAuthentication, see below.
export default (store, history) => {
return (
<Router history={history}>
<Route path="/" component={App}>
{ /* Home (main) route */ }
<IndexRoute component={HomeContainer}/>
<Route path="post/:slug" component={PostPage}/>
{ /* <Route path="*" component={NotFound} status={404} /> */ }
</Route>
<Route path="/admin" component={requireAuthentication(AdminContainer)}>
<IndexRoute component={PostList}/>
<Route path=":slug/edit" component={PostEditor}/>
<Route path="add" component={PostEditor}/>
</Route>
<Route path="/login" component={Login}/>
</Router>
);
};
requireAuthentication is a function that
if the user is authenticated, renders the wrapped component,
otherwise redirects to Login
You can see it below:
export default function requireAuthentication(Component) {
class AuthenticatedComponent extends React.Component {
componentWillMount () {
this.checkAuth();
}
componentWillReceiveProps (nextProps) {
this.checkAuth();
}
checkAuth () {
if (!this.props.isAuthenticated) {
let redirectAfterLogin = this.props.location.pathname;
this.context.router.replace({pathname: '/login', state: {redirectAfterLogin: redirectAfterLogin}});
}
}
render () {
return (
<div>
{this.props.isAuthenticated === true
? <Component {...this.props}/>
: null
}
</div>
)
}
}
const mapStateToProps = (state) => ({
isAuthenticated: state.blog.get('isAuthenticated')
});
AuthenticatedComponent.contextTypes = {
router: React.PropTypes.object.isRequired
};
return connect(mapStateToProps)(AuthenticatedComponent);
}
Also, requireAuthentication will protect all routes under /admin. And you can reuse it wherever you like.
Lots have changed over the time. onEnter no longer exists on react-router-4
The following is from my real project for your reference
export const getRoutes = (store) => {
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
checkIfAuthed(store) ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/login'
}}/>
)
)}/>
)
return (
<Router>
<div>
<PrivateRoute exact path="/" component={Home}/>
<Route path="/login" component={Login} />
</div>
</Router>
)
}
After trying out a few of the above suggestions, I found the best way to track the state of your store with updates is to use React-Redux's useSelector function which basically connects a functional component to the store.
import * as React from "react";
import {Redirect, Route, Switch} from "react-router";
import {Provider, useSelector} from "react-redux";
import { createBrowserHistory } from "history";
// Your imports
import {IApplicationState,} from "./store/store";
import {Login} from "./routes/login/login.component";
import {getToken} from "./store/helpers/httpHelpers";
function handleRedirect() {
if(!getToken()) {
return <Redirect to="/login"/>;
}
}
const restricted = (Component: _ComponentType, isLoggedIn: boolean) => {
// Don't redirect here if there is a token in localStorage.
// This is happening when we are on a restricted route and the user
// refreshes & the isLoggedIn state hasn't been updated yet.
return !isLoggedIn ? (
() => handleRedirect()
) : () => <Route component={Component}/>
};
const AuthenticateRoutes = () => {
const isLoggedIn = useSelector((state: IApplicationState) => state.auth.isLoggedIn);
return (
<Switch>
<Route path="/login" component={Login} />
<Route path="/downloads" render={restricted(Download, isLoggedIn)} />
</Switch>
);
};
export function App() {
return (
<Provider store={store}>
<>
<Router history={createBrowserHistory()}>
<AuthenticateRoutes />
</Router>
</>
</Provider>
);
}

Categories

Resources