Recommended approach for route-based tests within routes of react-router - reactjs

I'm using react-testing-library within a project of mine and am trying to write tests that validate in-app routing.
e.g. testing that a button on the AccessDenied page brings you back to the Home page.
I've been able to write these sorts of tests successfully for my App component because it defines all of the app routes. But if AccessDenied is one of those routes, how do I need to set up my tests to validate a button clicked there will route my back to Home?
Here is a contrived example:
App.tsx
<>
<Router>
<Route exact path="/" component={Home} />
<Route exact path="/access-denied" component={AccessDenied} />
</Router>
<Footer />
</>
AccessDenied.tsx
<div>
<div>Access Denied</div>
<p>You don't have permission to view the requested page</p>
<Link to="/">
<button>Go Home</button> <--- this is what i want tested
</Link>
</div>
As I said earlier the reason my tests work inside App.test.tsx is because my App component defines the routes inside itself, whereas my AccessDenied is just one of those routes. However, is it possible to leverage the router defined in my App.tsx in my AccessDenied.test.tsx tests? Perhaps I'm approaching this problem incorrectly? That's where I'm struggling. For reference, here is my working App.test.tsx tests.
App.test.tsx
describe('App', () => {
it('should allow you to navigate to login', async () => {
const history = createMemoryHistory()
const { findByTestId, getByTestId } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<AuthContext.Provider
value={{
authState: AUTH_STATES.UNAUTHENTICATED,
}}
>
<Router history={history}>
<App />
</Router>
</AuthContext.Provider>
</MockedProvider>,
)
fireEvent.click(getByTestId('sidebar-login-button'))
expect(await findByTestId('login-page-login-button')).toBeInTheDocument()
fireEvent.click(getByTestId('login-page-register-button'))
expect(await findByTestId('register-page-register-button')).toBeInTheDocument()
})
})
Any thoughts or suggestions are appreciated!

If you think about the responsibility of the AccessDenied component, it isn't really to send the user home. That's the overall behaviour you want, but the component's role in that is simply to send the user to "/". At the component unit level, therefore, the test could look something like this:
import React, { FC } from "react";
import { Link, Router } from "react-router-dom";
import { fireEvent, render, screen } from "#testing-library/react";
import { createMemoryHistory } from "history";
const AccessDenied: FC = () => (
<div>
<div>Access Denied</div>
<p>You don't have permission to view the requested page</p>
<Link to="/">
<button>Go Home</button>
</Link>
</div>
);
describe("AccessDenied", () => {
it("sends the user back home", () => {
const history = createMemoryHistory({ initialEntries: ["/access-denied"] });
render(
<Router history={history}>
<AccessDenied />
</Router>
);
fireEvent.click(screen.getByText("Go Home"));
expect(history.location.pathname).toBe("/");
});
});
Note that "/" is the default path, so if you don't provide initialEntries the test passes even if the click doesn't do anything...
At that point you might be thinking "but what if the home route changes?" If you moved the home page to "/home", for example, this test would continue to pass but the application would no longer actually work. This is a common problem with relying too much on very low-level tests and is where higher-level tests come into play, including:
Integration: render the whole App and use fireEvent to simulate navigation. This is challenging in your current setup, because the Router is at the App level; I'd move the Router to index.tsx and have a Switch in App.tsx instead, so you can render App within a MemoryRouter or use the createMemoryHistory method I show above (I've done this in this starter kit for example).
End-to-end: use a browser driver (e.g. Cypress or the various Selenium-based options) to automate actual user interactions with the app.
I haven't got as far as showing tests for routing, but do cover these different levels of test for a simple React app on my blog.
In React Router v6, you need to update the Router usage slightly (see "Cannot read properties of undefined (reading 'pathname')" when testing pages in the v6 React Router for details):
render(
<Router location={history.location} navigator={history}>
<AccessDenied />
</Router>
);

Related

useNavigate() issue in micro frontend app

I'm designing a MFE app called header. Implementation is something like below,
header.js
const headerApp = () => {
const navigate = useNavigate();
const logoClickHandler = () => {
navigate('/some-route'); // router v6
}
return(
...
<Logo onClick={logoClickHandler} />
)
}
App.js
I want to keep/use it like below
const App = () = {
return(
<div>
<HeaderApp /> // This component just uses useNavigation or <NavLink to='/some-route' />
</div
)
}
Problem is Header app doesn't have its own routing mechanism in it. It is just a separate app and to be more specific standalone component and just provides navigations among different MFE apps using useNavigate() OR <NavLink /> router feature.
Since, I'm using useNaviage() OR <NavLink />, react is asking me to wrap the component inside <Routes> (as shown below) which is unnecessary for my header app.
React Error
useNavigate() may be used only in the context of a <Router> component.
don't want to end up like below,
const App = () = {
return(
<div>
<Routes>
<Route path='/' element={ <HeaderApp /> } />
</Routes>
</div
)
}
NOTE : Routing is handled in separate app called container. Header only provides links for navigations.
React Router uses React Context, which is a way of passing information down through the React tree. Because of this, you only need to make sure you have at least one <Router> as a parent of whatever component is rendering <headerApp /> for this to work.
If this is not acceptable to you - you want your application to be used in non-React router contexts, for example - you may want to refactor your header application such that it either provides its own React Router instance or accepts the required methods and attributes through props.
It is not possible to use <NavLink /> or useNavigate() without one of the parents of <headerApp /> using Router />.

React Functional Component Render twice after navigate to that Component, But when trying to refresh, Rendered only once

When I go to one functional component using react-router, it renders twice.
However, when I refresh the page of that component, it only renders once.
For the test, created empty functional component like that:
import React from 'react'
const TestFunctional: React.FC<any> = () => {
console.log('Test===>>>') // console log twice when navigate to this component
return <></>
}
export default TestFunctional
Here is Router in App.tsx
import React from 'react'
import { Route, Switch, useLocation, withRouter } from 'react-router-dom'
import TestFunctional from 'views/Test'
const AnimatedSwitch = withRouter(({ location }) => (
<Switch>
<Route exact path="/" component={StartPage} />
<Route exact path="/test" component={TestFunctional} />
</Switch>
))
const App = () => {
return (
<div className="app">
<Web3ReactManager>
<AnimatedSwitch />
</Web3ReactManager>
</div>
)
}
export default App
I did not use React.StrictMode in index.tsx.
ReactDOM.render(
<ApolloProvider client={client}>
<Provider store={store}>
<ConnectedRouter history={history}>
<Web3ReactProvider getLibrary={getLibrary}>
<Web3ProviderNetwork getLibrary={getLibrary}>
<App />
</Web3ProviderNetwork>
</Web3ReactProvider>
</ConnectedRouter>
</Provider>
</ApolloProvider>,
document.getElementById('root')
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers
serviceWorker.unregister()
So it is some weird.
When I refresh this page, console.log('Test===>>>') show only once.
What is a mistake and how to fix the double render problem?
Why is that a problem? You should design/write your components assuming that it could re-render at anytime. React is even working on a new rendering mode where your component might be rendered multiple times before it actually gets "rendered in DOM".
As for why it actually renders twice? Not sure, might just be a quick of ReactDOM. As a side note, the documentation for component does have this warning for Route though:
When you use component (instead of render or children, below) the
router uses React.createElement to create a new React element from the
given component. That means if you provide an inline function to the
component prop, you would create a new component every render. This
results in the existing component unmounting and the new component
mounting instead of just updating the existing component. When using
an inline function for inline rendering, use the render or the
children prop (below).
While that shouldn't apply in this case, still handy to know.

Protecting routes in React app with React Router

I've created a simple React app with Redux, React Router and Auth0 which handles user authentications.
I'm trying to create this basic behavior to control access:
All unauthenticated users will automatically be sent to /public
Authenticated users can access all the other parts of the app
Once a user is authenticated by Auth0, I want to process the access_token and send user to / which is the Home component
Everything is "almost" working the way it should. The problem I'm having is that render() function in App.jsx is executing BEFORE the lock.on('authenticated') listener even has a chance to process the tokens returned by Auth0. As a result, the tokens are never stored and the user always seems to be unauthenticated. If I send user to /login, everything works fine because I'm not checking to see if the user is authenticated before rendering the Login component.
I think the way I'm handling protected routes needs to change. Any suggestions as to how to handle protected routes?
I'm providing the code that you need here. If you want to see the whole app, go to https://github.com/imsam67/react-redux-react-router-auth0-lock
The following is the App.jsx:
class App extends Component {
render() {
const isAuthed = isAuthenticated();
return (
<div>
<Switch>
<Route exact path="/" render={ props => isAuthed ? <Home {...props} /> : <Redirect to="/public" /> } />
<Route exact path="/login">
<Login />
</Route>
<Route path="/public">
<Public />
</Route>
</Switch>
</div>
);
}
}
This is the AuthWrapper component where I handle Auth0:
class AuthWrapper extends Component {
constructor(props) {
super(props);
this.onAuthenticated = this.onAuthenticated.bind(this);
this.lock = new Auth0Lock('my_auth0_client_id', 'my_domain.auth0.com', {
auth: {
audience: 'https://my_backend_api_url',
redirectUrl: 'http://localhost:3000/',
responseType: 'token id_token',
sso: false
}
});
this.onAuthenticated();
}
onAuthenticated() {
debugger; // After successful login, I hit this debugger
this.lock.on('authenticated', (authResult) => {
debugger; // But I never hit this debugger
let expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
sessionStorage.setItem('access_token', authResult.accessToken);
sessionStorage.setItem('id_token', authResult.idToken);
sessionStorage.setItem('expires_at', expiresAt);
});
}
render() {
return(
<AuthContext.Provider value={{ lock: this.lock }}>
{this.props.children}
</AuthContext.Provider>
);
}
}
And here's index.js in case you need to see it:
import App from './components/App';
import AuthWrapper from './components/auth/AuthWrapper';
// Store
import appStore from './store/app-store';
const store = appStore();
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<AuthWrapper>
<App />
</AuthWrapper>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
Here are the changes I made to make this work.
I now initialize Auth0 Lock in index.js to make it global
I moved the onAuthenticated() listener to LoginCallback.jsx component which is where the user is sent after a successful login. I believe moving the onAuthenticated() from App.jsx to LoginCallback.jsx made the biggest impact because the listener in LoginCallback.jsx executes before App.jsx.
On successful authentication, I also use this.props.history.push('/'); to send user to Home component
For full code, please see the repo at https://github.com/imsam67/react-redux-react-router-auth0-lock
Remember nothing is protected in the client side at all. If your concerned of routing to a component without auth just make sure no data is exposed(assuming they can't get any data without a token) and redirect if they landed there even after your router checks for auth or shows an error.
Remember nothing is protected in the client side at all. If your concerned of routing to a component without auth just make sure no data is exposed and redirect if they landed there even after your router checks for auth. I think #Sam has it right. The routes may not respond as expected to an asynchronous call changing it or may have odd behavior. I've never attempted a dynamic route this way but always had conditional renders of content components. A better approach may be to send the call and in the then block redirect to a url which the router knows to handle. Just not sure the router handles this very well. Catch the component checking for auth on load and redirect back to log on if not authorized. Sorry I'm not helping much here but conditional routes almost seem like an anti pattern but I guess it could work if we knew how the router renders its data after changes or if it actually does at all(the routes them selves.) So if they were to bookmark the url and try to return back that would be a 404 right? Maybe like a 401 unauthorized showing and redirect or link to log in might be better?
Dynamic routing need to be defined outside of the <Switch> scope. Here is an exemple assuming your function isAuthenticated() is a state (Redux or wathever)
import React from "react";
import ReactDOM from "react-dom";
import { createBrowserHistory } from "history";
import { Router, Route, Switch, Redirect } from "react-router-dom";
// core components
import Admin from "layouts/Admin.js";
import SignIn from "layouts/SignIn";
const hist = createBrowserHistory();
const loggedRoutes = () => (
<Switch>
<Route path="/" component={SignIn} />
<Route path="/admin" component={Admin} />
<Redirect from="/admin" to="/admin/aboutUs/whatWeDo" />
</Switch>
);
const routes = () => (
<Switch>
<Route path="/" component={SignIn} />
<Route path="/login" component={Admin} />
<Redirect from="/" to="/login" />
</Switch>
);
ReactDOM.render(
<Router history={hist}>
{checkIfAuth? loggedRoutes():routes()}
</Router>,
document.getElementById("root")
);
In this exemple, If you are not login you are redirect to /login.

React Router or Link Not Rendered

I am using react-router-dom in a redux app.
This is my initial setup in index.js:
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
, document.getElementById('root'));
Then in my App.js I have:
render() {
return (
<div className="App">
<Route exact path="/" render={ () => {
return (
<div>
{
this.props.categories.map((category)=>{
console.log('category', category)
return (
<Link key={category.name} to="/category" >{category.name}</Link>
)
})
}
</div>
)
}}
/>
<Route path="/category" render={ () => {
console.log('category path this.props', this.props)
return (<p>Category is whatever</p>)
}}
/>
</div>
);
}
I would think that whenever I click any of the Links displayed the browser would automatically know how to render the new Route path /category but for some reason it does not.
What am I doing wrong?
The above post by Dane has the solution.
But in the spirit of presenting the solution with more clarity, I will copy and paste the relevant codes that made react router work well with redux and other middleware.
import { withRouter } from 'react-router-dom'
export default withRouter(connect(
mapStateToProps,
)(App))
From React Router docs,
Generally, React Router and Redux work just fine together.
Occasionally though, an app can have a component that doesn’t update
when the location changes (child routes or active nav links don’t
update). This happens if:
The component is connected to redux via
connect()(Comp).
The component is not a “route component”, meaning it
is not rendered like so: <Route component={SomeConnectedThing}/>
The
problem is that Redux implements shouldComponentUpdate and there’s no
indication that anything has changed if it isn’t receiving props from
the router. This is straightforward to fix. Find where you connect
your component and wrap it in withRouter.
So maybe it's a problem with using render props. So:
either replace render with component, or
try their solution, with withRouter ( even there you have to make them into components )
https://reacttraining.com/react-router/core/guides/redux-integration/blocked-updates
Both Link and Router is compulsory.
Not Work!
import { BrowserRouter as Link } from "react-router-dom";
Work in my case.
import { BrowserRouter as Router, Link } from "react-router-dom";
In my case, this is working properly. If you will import router and link both together.
import { BrowserRouter as Router, Link } from "react-router-dom";

a better pattern for authenticated React components and routes

I'm working on adding Auth0 authentication to my React app, and even though I have it working, I feel like there's a better way to approach this. I'm struggling to figure out a better pattern for the authentication logic.
The app setup is react + redux + react-router-redux + redux-saga + immutable + auth0-lock.
Beginning at the top, the App component defines the basic page layout, both Builder and Editor components require the user to be logged in, and authenticated() wraps each in a Higher Order Component responsible for handling authentication.
// index.js
import App from './containers/App';
import Builder from './containers/Builder';
import Editor from './containers/Editor';
import Home from './containers/Home';
import Login from './containers/Login';
import AuthContainer from './containers/Auth0/AuthContainer';
...
ReactDOM.render(
<Provider store={reduxStore}>
<Router history={syncedHistory}>
<Route path={'/'} component={App}>
<IndexRoute component={Home} />
<Route path={'login'} component={Login} />
<Route component={AuthContainer}>
<Route path={'builder'} component={Builder} />
<Route path={'editor'} component={Editor} />
</Route>
</Route>
<Redirect from={'*'} to={'/'} />
</Router>
</Provider>,
document.getElementById('app')
);
At the moment, AuthContainer doesn't do much except check the redux store for isLoggedIn. If isLoggedIn is false, the user is not allowed to view the component, and is redirected to /login.
// containers/Auth0/AuthContainer.js
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { redirectToLogin } from './Auth0Actions';
class AuthContainer extends React.Component {
componentWillMount() {
if (!this.props.isLoggedIn) {
this.props.actions.redirectToLogin();
}
}
render() {
if (!this.props.isLoggedIn) {
return null;
}
return this.props.children;
}
}
// mapStateToProps(), mapDispatchToProps()
export default connect(mapStateToProps, mapDispatchToProps)(AuthContainer);
The next piece is Auth0. The Auth0 Lock works in "redirect" mode, which means the user will leave the app to log in, and then be redirected back to the app at /login. As part of the redirect, Auth0 attaches a token as part of the URL, which needs to be parsed when the app loads.
const lock = new Auth0Lock(__AUTH0_CLIENT_ID__, __AUTH0_DOMAIN__, {
auth: {
redirect: true,
redirectUrl: `${window.location.origin}/login`,
responseType: 'token'
}
});
Since Auth0 will redirect to /login, the Login component also needs authentication logic. Similar to AuthContainer, it checks the redux store for isLoggedIn. If isLoggedIn is true, it redirects to the root /. If isLoggedIn is false, it'll attempt to authenticate.
// containers/Login/index.js
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { authenticate, redirectToRoot } from '../Auth0/Auth0Actions';
class Login extends React.Component {
componentDidMount() {
if (!this.props.isLoggedIn) {
this.props.actions.authenticate();
}
else {
this.props.actions.redirectToRoot();
}
}
render() {
return (
<div>Login Page</div>
);
}
}
// mapStateToProps(), mapDispatchToProps()
export default connect(mapStateToProps, mapDispatchToProps)(Login);
With these pieces in place, my integration with Auth0 seems to be working. However, I now have AuthContainer and Login component, and they are very similar. I can't place the Login component as a child to AuthContainer since the login page does not actually require the user to be logged in.
Ideally, all authentication logic lives in one place, but I'm struggling to figure out another way to get it working, especially with the special case of the Auth0 redirect. I can't help but think that there must be a different approach, a better pattern for authentication flow in a react + redux app.
One thing that would be helpful is to better understand how to dispatch an async action on page load, before the app starts initializing. Since Auth0 works with callbacks, I'm forced to delay setting the redux initial state until after Auth0 invokes the registered callback. What is the recommended way to handle async actions on page load?
I've left out some pieces for brevity, like the actions and sagas, but I'll be more than happy to provide those if it'll be helpful.
May not be a complete answer, so sorry for that. Few things to address here:
Ideally, all authentication logic lives in one place
I'm not so sure this is ideal, depending on what you mean by "one place". There's noting wrong with having two functions that are similar but are different enough in some aspect that warrants a little repetition. From what I can see your code the logic is indeed slightly different so two components seems perfectly fine.
Instead of componentDidMount, use Route's onEnter prop
Putting your auth logic after component mounting will likely cause a flicker of your authenticated html showing before the auth logic can run. Conceptually, you would like to prevent rendering this component at all until the auth logic has run. Route's onEnter is perfect for this. https://github.com/ReactTraining/react-router/blob/master/docs/API.md#onenternextstate-replace-callback
let authenticate = (nextState, replace) => {
// check store details here, if not logged in, redirect
}
<Route path={'builder'} onEnter={authenticate} component={Builder} />
how to dispatch an async action on page load, before the app starts initializing
This is quite a common question for React Apps / SPAs. I think the best possible user experience is to display something right away, perhaps a loading spinner or something that says "Fetching user details" or whatnot. You can do this in your top level App container or even before your first call to ReactDOM.render
ReactDOM.render(<SplashLoader />, element)
authCall().then(data =>
ReactDOM.render(<App data={data} />, element)
).catch(err =>
ReactDOM.render(<Login />, element)
}
I'm doing the same thing in my project and working fine with redux, react-router, just have a look at my code below:
routes:
export default (
<div>
<Route path="/" component={AuthenticatedComponent}>
<Route path="user" component={User} />
<Route path="user/:id" component={UserDetail} />
</Route>
<Route path="/" component={notAuthenticatedComponent}>
<Route path="register" component={RegisterView} />
<Route path="login" component={LoginView} />
</Route>
</div>
);
AuthenticatedComponent:
export class AuthenticatedComponent extends React.Component {
constructor( props ) {
super( props );
}
componentWillMount() {
this.props.checkAuth().then( data => {
if ( data ) {
this.props.loginUserSuccess( data );
} else {
browserHistory.push( '/login' );
}
} );
}
render() {
return (
<div>
{ this.props.isAuthenticated && <div> { this.props.children } </div> }
</div>
);
}
}
notAuthenticatedComponent:
export class notAuthenticatedComponent extends React.Component {
constructor(props){
super(props);
}
componentWillMount(){
this.props.checkAuth().then((data) => {
if(data && (this.props.location.pathname == 'login')){
browserHistory.push('/home');
}
});
}
render(){
return (
<div>
{ this.props.children }
</div>
);
}
}
If you are following the Thanh Nguyen's answer use React's "Constructor" instead of "componentWillMount". As its the recommended way according to the docs.

Resources