Problem: My App component is responsible to render the other components based on the path. So inorder to test it, I need to be able to "set" the path. e.g I'd like to see the SignUp component is rendered when the path is /sign-up.
What I've done: I thought I could use initialEntries to give the MemoryRouter an initial path (unsuccessful) then I thought I might be able to use Route/Redirect directly in my test inside MemoryRouter to set the path, but that did not work either.
Specification:
React 15
React Router 4 (react-router-dom)
Redux 3.6
Jest (the one comes with create-react-app)
Yo can find the code on the branch with the issue
I spent a while trying to figure this out myself.
Simply put you cannot nest a <Router> within the <MemoryRouter> as they are both Routers.
Solution Example:
Routes.js - Remove <BrowserRouter> from this so you can test your code.
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import SignUp from '../SignUp/SignUp'; //your signup component
import Home from '../Home/Home'; //some other component for an example
const MyRoutes = () => (
<Switch>
<Route path="/" component={Home} />
<Route path="/signup" component={SignUp} />
</Switch>
);
export default MyRoutes;
index.js - Add <BrowserRouter> here as you will need to wrap a <Switch> in a <Router>.
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import Routes from './Components/Routes/Routes';
ReactDOM.render(
(
<BrowserRouter>
<Routes />
</BrowserRouter>
), document.getElementById('root')
);
tests/Routes.test.js - Now add the <MemoryRouter> in here for testing.
import React from 'react';
import ReactDOM from 'react-dom';
import { MemoryRouter } from 'react-router-dom';
import Routes from '../Components/Routes/Routes';
const TestRoute = props => (
<MemoryRouter initialEntries={props.initialEntries}>
<Routes {...props} />
</MemoryRouter>
);
it('at /signup it displays the signup page', () => {
const div = document.createElement('div');
ReactDOM.render(<TestRoute initialEntries={['/signup']} />, div); // render on the route "/signup"
const elem = div.getElementsByClassName('signup')[0];
expect(elem).not.toBe(undefined); // check an element exists with that class
if(elem !== undefined) {
expect(elem.innerHTML).toEqual('signup'); // check it has worked
}
});
In other words you are separating the Routes/Switch from the Router to enable you to be able to test it, as you cannot nest two Routers.
I'm having a similar issue, I have found that the following works for testing components whose behaviour changes based upon the route:
<MemoryRouter initialEntries={['/about']}>
<Navigation {...props} />
</MemoryRouter>
In this instance, the snapshot shows that an .active className is added to the 'about' link in Navigation.
What does not seem to work is adding memory router around the component/container holding the initial routes. To keep things simple I have reduced my basic routing page to this:
<Router>
<div className="App">
<Navigation app={this.props.app} />
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/about" component={About} />
<Route exact path="/login" render={() => <Login app={this.props.app} />} />
<Route exact path="/signup" render={() => <Signup app={this.props.app} />} />
<Route exact path="/account" render={() => <Account app={this.props.app} />} />
<Route component={ErrorPage} />
</Switch>
</div>
</Router>
The react-router team states in current testing docs:
We have a lot of tests that the routes work when the location changes,
so you probably don’t need to test this stuff.
Related
I want to create a mobx store that is only loaded by admin users. It will only be injected into anything along the /admin route. I've looked into the use of Nested Providers but without much luck.
Ideally I'd like it to be linked to the route, so if a user goes to domain.com/admin then it is loaded.
I could write botchy code that simply checks if the user is admin, but I'm hoping theres a more elegant pattern I could follow.
EDIT
heres my temporary solution, let me know if theres a better way
AdminRoutes
export const AdminRoutes = () => {
const adminStores = createAdminStores()
return (
<Switch>
<Provider {...adminStores}>
<AuthedRoute path="/admin" exact component={Admin} />
</Provider>
</Switch>
)
};
App.ts
export const App = hot(({ history }: any) => (
<Provider {...rootStores}/>
<Router history={history}>
<Switch>
<UnauthedRoute path="/login" exact component={Login}></UnauthedRoute>
<Route path="/admin" exact component={AdminRoutes} />
<Route path="/error" component={ErrorComponent} />
<Route path="/abc" component={Test} />
<Route path="/:path" component={Issue} />
<Route path="/" component={Home} />
</Switch>
</Router>
</Provider>
));
I create the admin stores only when the user runs the AdminRoutes route. The only issue with this is that the store reloads when the user changes the URL however that happens with all my other stores.
One thing you could improve is to use React.lazy with your whole Admin component.
// import it like that
const AdminRoutes = React.lazy(() => import('./path-to-admin-routes'))
...
// and use like that
<Route
path="/admin"
render={() => (
<Suspense fallback={'Loading'}>
<AdminRoutes />
</Suspense>
)}
/>
That way regular non-admin users won't even load admin stuff and your final bundle will be smaller.
More info: https://reactjs.org/docs/code-splitting.html#reactlazy
Or you could also inject your AdminStore inside RootStore dynamically. I made example for it, hope it makes sense https://codesandbox.io/s/httpsstackoverflowcomquestions62759240-ew8hf?file=/src/AdminComponent.js
Basically you create your AdminStore first time admin component is loaded and then use newly created instance by saving it inside RootStore
react-admin loads the <Dashboard> at the base "/". And for that reason, the custom page never opens by default because the <Admin> always routes to the dashboard.
I've implemented <PrivateRoutes> to handle authentication and it's a success.The <Login> page is loaded by default, and upon authentication, it routes to dashboard.
Challenge: This process breaks the functionality of Sidebar <links>, like "Users".
// CustomRoutes.js
export default [
<Route exact path="/login" component={LoginPage} noLayout />,
<Route exact path="/forgot-password" component={ForgotPassword} noLayout />,
<Route exact path="/confirm-password" component={ConfirmForgotPassword} noLayout />,
<PrivateRoute path="/" loggedIn={localStorage.getItem("access_token")} component={dashboard} />
];
And then...
// PrivateRoute.js
import React from "react";
import { Redirect, Route } from "react-router-dom";
const PrivateRoute = ({ path, component: Component, ...rest }) => {
const isLoggedIn = localStorage.getItem("access_token");
return (
<Route
path={path}
{...rest}
render={props => isLoggedIn
? <Component {...props} />
: <Redirect to={{ pathname: "/login", state: { from: props.location } }} />
}
/>
);
};
export default PrivateRoute;
Here's how it looks within the main <App>
// App.js
const App = () => (
<Admin
theme={myTheme}
dashboard={dashboard}
authProvider={authProvider}
dataProvider={dataProvider}
customRoutes={customRoutes}
loginPage={LoginPage}
logoutButton={LogoutButton}
forgotPassword={ForgotPassword}
>
<Resource name="users" list={UserList} create={UserCreate} show={UserShow} icon={UserIcon} />
</Admin>
);
export default App;
Please note that the <resource> "users" is not working.
Do I need to add custom route for that too?
Alright, let's debug this step-by-step:
We can take advantage of react-admin directly to handle the authentication via the authProvider.
Once you include the authProvider, react-adminenables a new page on the /login
route, which displays a <Login> form asking for username and password.
You can definitely customize that login page (as I assume you did),
and I advise that you avoid using a <PrivateRoute> since the authProvider redirects all users who aren't yet authenticated to /login.
Kindly note that <Admin> creates an application with its own state, routing, and controller logic.In your code, you added forgotPassword which is not a prop accepted by <Admin.
<admin
// remove this...
- forgotPassword={ForgotPassword}
>
I wonder exactly what error you get in this case, but this is the main bug.
I'm building a react-native application that displays Service overlay (like those facebook messenger bubble heads), implement in Android only, and on the overlay click it should go back to the app in a specific screen.
I'm using react-router-native and I have my routes structured like this:
App.js
<NativeRouter>
<ApolloProvider client={client}>
<Main>
<Switch>
<Route exact path="/home" component={Home} />
<Route exact path="/progress" component={RouteProgress} />
<Route exact path="/search" component={Search} />
</Switch>
</Main>
</ApolloProvider>
</NativeRouter>
The Main component has these:
Main.js
componentDidMount() {
console.log(this.props.location.pathname);
if(this.props.location.pathname === '/') {
this.props.history.push("/home");
}
}
The callback from my Native module is being called like this:
FloatingView.java
case MotionEvent.ACTION_UP:
if (lastAction == MotionEvent.ACTION_DOWN || delta < 3) {
Intent intent = new Intent(FloatingWindow.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
FloatingViewPackage.callback.invoke();
stopSelf();
}
The callback is defined in the component Search, which also executes the native module:
Search.js
<Button onPress={() => FloatingView.init(() => {
console.log('go to progress 1');
this.props.history.push("/progress");
setTimeout(() => {
console.log('go to progress 2');
this.props.history.push("/progress");
}, 1000);
})}
The problem is that this.props.history.push("/progress"); doesn't work neither outside the timeout nor inside.
Outside the timeout, the function is called before Main componentDidMount but location.pathname is not updated. Inside it the function is called after, but it doesn't navigate to the right screen. It always fall into /home.
I thought this my be a life cycle issue, since the Search component is not mounted. I've been trying to figure a way out to make this work. I tried using the Redirect component:
<Button onPress={() => FloatingView.init(() => <Redirect to="/progress" />)}
Does anyway can think of a way around this? Thanks
Found a solution, don't know if the best one, but it works.
Created a singleton navigation.js
navigation.js
export default {
entryPoint: '/home'
};
And changed the following files:
Search.js
<Button onPress={() => FloatingView.init(() => {
navigation.entryPoint = "/progress";
})}
Main.js
componentDidMount() {
if(this.props.location.pathname === '/') {
this.props.history.push(navigation.entryPoint);
}
}
I have a component available at different urls. This is a simplified version of my app.js
<Router>
<Switch>
<Route path="/login"
render={() => {
return <SignUpAndRegister status="login" />;
}}
</Route>
<Route path="/register"
render={() => {
return <SignUpAndRegister status="register" />;
}}
</Route>
</Switch>
<Router>
I pass the status prop and then in the component setState based on the prop value. This works so far but I also need Links within the component that link to other other state. When your on the register page/state I have a link to login. When you're on the login page/state I have a link to register.
This means I have to have a link to change the url and also call a function to setState:
<Link
to="/login"
onClick={() => this.registerOrLogin('login')}
>
Log in
</Link>
How can I have the components state change when a url change is detected? It seems like browserHistory did this prior to v4 but I cant find an up up date solution.
I can set the state within the SignUpAndRegister component when it first mounts and then every time new props are received eg when the url changes.
componentDidMount() {
this.setState({ status: this.props.status });
}
componentWillReceiveProps(props) {
this.setState({ status: props.status });
}
I am now upgrading my project from react-router 3.0.0 to 4.1.2 and now meet the nested routes problem.
I want to get a page with a sidebar with menus on the left, a header on the top and on the right bottom is the main content part.
Studied from here: Nested Routes in v4, my code is just like this:
<Router>
<AppLayout>
<Route path='/abc' component={ABC} />
<Route path='/xyz' component={XYZ} />
</AppLayout>
</Router>
where component AppLayout is a common layout contains
<Layout>
<AppHeader/>
<Layout>
<Sider></Sider>
<Layout>
<Content>
<div>{this.props.children}</div>
</Content>
</Layout>
</Layout>
<Footer></Footer>
</Layout>
The content of AppLayout can be changed, the menus on the sidebar need to be closely related to the the route url. So my problem is, how to get the route paths from Router and set those values into AppLayout, so that menu items can be selected along with the route changes.
You can use context for that.
In your appLayout Component:
class AppLayout extends Component {
static contextTypes = {
router: PropTypes.object
}
render (){
// your code here.
}
You can get the URL from const { url } = this.context.router.route.match.