Converting multiple configuration routes in react-router's plain objects - javascript

I'm trying to achieve something like the following router structure in plain route objects.
const Demo = () => (
<Router history={hashHistory}>
<Route path="/" component={App}>
<Route path="fade" component={FadeDemo}>
<IndexRoute component={Lorem} />
<Route path="demo-1" component={Lorem} />
<Route path="demo-2" component={Lorem} />
<Route path="demo-3" component={Lorem} />
</Route>
My app router looks like this:
export const createRoutes = (store) => ({
path: '/',
component: CoreLayout,
indexRoute: Home,
childRoutes: [
CounterRoute(store)
]
})
So I want to add the FadeDemo transition container from the former JSX as a route without a path on my latter example. Is that possible?
EDIT:
That's my updated route index file, now I get can't match the '/counter' location:
import CoreLayout from '../layouts/CoreLayout/CoreLayout'
import Home from './Home'
import CounterRoute from './Counter'
import TransitionWrapper from './TransitionWrapper'
export const createRoutes = (store) => ({
path: '/',
component: CoreLayout,
indexRoute: Home,
childRoutes: [{
//path: 'fade',
component: TransitionWrapper,
childRoutes: [
CounterRoute(store)
]
}]
})
counter app index:
import { injectReducer } from '../../store/reducers'
export default (store) => ({
path: 'counter',
/* Async getComponent is only invoked when route matches */
getComponent (nextState, cb) {
/* Webpack - use 'require.ensure' to create a split point
and embed an async module loader (jsonp) when bundling */
require.ensure([], (require) => {
/* Webpack - use require callback to define
dependencies for bundling */
const Counter = require('./containers/CounterContainer').default
const reducer = require('./modules/counter').default
/* Add the reducer to the store on key 'counter' */
injectReducer(store, { key: 'counter', reducer })
/* Return getComponent */
cb(null, Counter)
/* Webpack named bundle */
}, 'counter')
}
})
TransitionWrapper
import React from 'react'
import { Link } from 'react-router'
import { RouteTransition } from 'react-router-transition'
const TransitionWrapper = (props) => (
<div>
<RouteTransition
component={"div"}
className="transition-wrapper"
pathname={this.props.location.pathname}
{...props.preset}
>
{this.props.children}
</RouteTransition>
</div>
)
export default TransitionWrapper

Here is described how you can achieve it.
export const createRoutes = (store) => ({
path: '/',
component: CoreLayout,
indexRoute: Home,
childRoutes: [
{
component: FadeDemo,
childRoutes: [
{
path: 'demo-1',
component: Lorem
},
{
path: 'demo-2',
component: Lorem
}
// ...
]
},
]
})

Related

How to use an array element as a route element?

I am writing routes with react router:
<Route path="/" element={<Homepage />} />
I have an array with the element names:
const page = ["Hompage", "About"];
How can I use the array element as a route element?
I tried to add strings of angle brackets and use the array element but it didn't work.
const edit = () => {
for (let i=0; i<10; i++) {
page[i]="<"+page[i]+" />"
}
Thanks
You will need to import the actual components you want to render at some point, and then map the array to JSX.
Example:
import HomePage from '../path/to/HomePage';
import About from '../path/to/About';
const pageMap = {
HomePage,
About
};
...
const pages = ["Hompage", "About"];
...
const edit = () => {
return pages.map(Page => <Page key={Page} />);
};
...
If you are wanting to map the pages to Route components, then it would be a similar process.
const pageMap = {
HomePage: { path: "/", element: <HomePage /> },
About: { path: "/about", element: <About /> },
};
const pages = ["Hompage", "About"];
...
pages.map(({ path, element }) => (
<Route key={path} path={path} element={element} />
))
At this point though, you may as well use the useRoutes hook and pass your routes config to it.
Example:
import { useRoutes } from 'react-router-dom';
import HomePage from '../path/to/HomePage';
import About from '../path/to/About';
...
const routesConfig = [
{ path: "/", element: <HomePage /> },
{ path: "/about", element: <About /> }
];
...
const routes = useRoutes(routesConfig);
...
return routes;
You should be able to do something like this:
import Home from '../Home'
import About from '../About'
const Routes = () => {
const pages = [{ route: '/', page: Home }, { route: '/about', page: About }]
return (
<Switch>
{pages.map(({route, page: Page }) => (
<Route path={route} element={<Page />} />
)}
</Switch>
)
}
The key here is to use the imported component as the value for your page key and then you can use normal JSX component syntax as you are iterating with map

You cannot render a <Router> inside another <Router> -- using React Router 6 with Storybook

I am understanding the error message, but not sure how I should handle it in this case. I believe that using the decorator below is causing the issue, but the decorator is needed to use the component with Storybook.
Here is the error message:
You cannot render a <Router> inside another <Router>. You should never have more than one in your app.
Believe this is due to the decorator and I can only assume the BrowserRouter found way upstream in my app, but from what I understand, Storybook isn't loading my index file. So I'm unsure how to proceed.
Here is the component, simplified:
export const Component = () => {
...
return (
<Routes>
<Route path="/screening" element={<Screening {...propBag} />} />
</Routes>
);
};
Then, the Story:
import { Story, Meta } from '#storybook/react';
import { MemoryRouter } from 'react-router-dom';
import { Component } from '..';
export default {
title: 'Province',
component: Component,
decorators: [
(Story) => (
<MemoryRouter>
<Story />
</MemoryRouter>
)
],
} as Meta;
const Template: Story = (args) => <IntakeQuestionnaire {...args} />;
export const Province = Template.bind({});
Province.parameters = {};
Province.args = {};
Finally, the preview.js file:
import 'tailwindcss/tailwind.css';
import { MockedProvider } from '#apollo/client/testing';
import { i18n } from './i18next';
export const parameters = {
i18n,
locale: 'en',
locales: {
en: 'English',
fr: 'Français',
},
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
apolloClient: { MockedProvider },
};
export const decorators = [
(Story) => (
<MemoryRouter>
<Story />
</MemoryRouter>
)
];
Not sure why, but removing the decorators array from preview.js file and putting it only in the component Story file fixed this issue. Less than ideal but at least I am unblocked now
export default {
title: 'Province',
component: Component,
decorators: [
(Story) => (
<MemoryRouter>
<Story />
</MemoryRouter>
)
],
} as Meta;
EDIT: see below comment - i was being silly with decorators

Easier way to dynamically render components with sidebar navigation using react router

Currently going through this tutorial on creating a sidebar navigation system with react router https://reacttraining.com/react-router/web/example/sidebar
I am planning to have multiple routes, so that means I'll have to keep importing the routes and add them to the routes array. Is there a smart/right way to load them dynamically?
All my components will be in my /Views folder.
App.js
import React, { Component } from 'react';
import SideBar from './components/SideBar/SideBar';
import MainContent from './components/MainContent/MainContent';
import { BrowserRouter as Router,
} from 'react-router-dom';
// Import all components here
import Button from './components/Views/Button/Button';
import Color from './components/Views/Color/Color';
import Card from './components/Views/Card/Card';
import Filter from './components/Views/Filter/Filter';
const routes = [
{
path: "/",
name: 'home',
exact: true,
main: () => <h2>Home</h2>
},
{
path: "/button",
name: 'Button',
main: () => <Button />
},
{
path: "/color",
name: 'Color',
main: () => <Color />
},
{
path: "/card",
name: 'Card',
main: () => <Card />
},
{
path: "/filter",
name: 'Filter',
main: () => <Filter />
},
];
class App extends Component {
render() {
return (
<Router>
<div className="ds-container">
<SideBar routes={routes} />
<MainContent routes={routes} />
</div>
</Router>
);
}
}
export default App;
Since, you're using create-react-app that uses webpack internally, you could look into require-context. This will help you dynamically import all files in a folder that match a certain regex. (ex: ending with .jsx/.js)
However, I'd advice you against it as:
At no point will you know what routes you're currently catering to.
It may decrease your code readability.
You may have to also export the mapping(path in the Route) of the component along with the component itself.
To avoid all of this, You could simply create a index.js file in your Views component that would require any new route component that you create and return the final array that you have formed in the App.js file.
So essentially, /Views/index.js :
// Import all components here
import Button from './components/Views/Button/Button';
import Color from './components/Views/Color/Color';
import Card from './components/Views/Card/Card';
import Filter from './components/Views/Filter/Filter';
const routes = [
{
path: "/",
name: 'home',
exact: true,
main: () => <h2>Home</h2>
},
{
path: "/button",
name: 'Button',
main: () => <Button />
},
{
path: "/color",
name: 'Color',
main: () => <Color />
},
{
path: "/card",
name: 'Card',
main: () => <Card />
},
{
path: "/filter",
name: 'Filter',
main: () => <Filter />
},
// add new routes here
];
export default routes;
In SideBar.js:
import routes from 'path/to/views/Views';
//rest of your code to render the routes.
This way, you would clean up the code in your App.js and would also be able to effectively separate the concerns of the individual components.
I hope this makes sense :)
There are several ways to choose a main component depending on current location. But all of them would require listing all the possible routes and importing respective components. You can use dynamic imports and React.lazy for easier code splitting.
But you can avoid extra configuration for your sidebar. The sidebar can get configuration from global state and your main components can update that state on mount (componentDidMount or useEffect(() => {...}, [])):
const SideBarContext = React.createContext(() => {});
function useSidebar(name) {
const setSidebarName = useContext(SideBarContext);
useEffect(() => {
setSidebarName(name);
return () => setSidebarName(null);
}, []);
}
function Home() {
useSidebar('home');
return <h2>Home</h2>;
}
function Button() {
useSidebar('Button');
return <button>press me</button>;
}
function App() {
const [name, setName] = useState(null);
return (
<Router>
<div className="ds-container">
<SideBar name={name} />
<SideBarContext.Provider value={setName}>
<Route path="/" exact component={Home}/>
<Route path="/button" component={Button}/>
</SideBarContext.Provider>
</div>
</Router>
);
}
So each component will take care about options for sidebar and you only need to import them and add Routes.

Testing NotFoundPage with jest & Enzyme

I just joined a front team and I am asked to do some tests of the application,
I need to check that my Not Found page appears only if the user does not enter a correct url.
I know there is MemoryRouter if our application routing use react router but my team does not use it:
<MemoryRouter initialEntries={["/random"]}>
<Routes />
</MemoryRouter>
this is the component that manages the routing:
const routes = {
path: "",
children: [
{
path: "/",
load: () => import(/* webpackChunkName: 'home' */ "./home")
},
{
path: "/login",
load: () => import(/* webpackChunkName: 'login' */ "./login")
},
{
path: "/delivery",
load: () => import(/* webpackChunkName: 'delivery' */ "./delivery")
},
{
path: "/offers",
load: () => import(/* webpackChunkName: 'delivery' */ "./offers")
},
// Wildcard routes, e.g. { path: '(.*)', ... } (must go last)
{
path: "(.*)",
load: () => import(/* webpackChunkName: 'not-found' */ "./not-found")
}
],
async action({ next }) {
const route = await next();
return route;
}
};
**export default routes:**
So I still tried with MemoryRouter but it does not work:
import React from "react";
import { mount } from "enzyme";
import { MemoryRouter } from "react-router";
import { Route } from "react-router-dom";
import NotFoundPage from "../routes/not-found/NotFound";
import Routes from "../routes/index";
describe("testing NotFoundPage", () => {
it("should go to 404 page", () => {
const component = mount(
<MemoryRouter initialEntries={["/random"]}>
<Routes />
</MemoryRouter>
);
expect(component.find(NotFoundPage)).toHaveLength(1);
});
});

Nesting child routes within another child route

EDIT
My Initial question included routes with split points, but I've reduced it to the most simple use case of just nesting child routes under each other.
For reference I'm using the popular react-redux-starter-kit and I'm trying to add a simple wrapper component to my routes like so:
export const createRoutes = (store) => ({
path: '/',
component: CoreLayout,
indexRoute: Home,
childRoutes: [{
component: TransitionWrapper,
childRoutes: [
CounterRoute(store)
]
}]
})
but I get the following error and my child routes aren't being rendered:
Warning: Failed prop type: Required prop `children` was not specified in `CoreLayout`.
in CoreLayout (created by RouterContext)
in RouterContext (created by Router)
in Router (created by AppContainer)
in div (created by AppContainer)
in Provider (created by AppContainer)
in AppContainer
So basically if I nest child routes in a child route I get a complaint about missing children.
Here is the full setup:
main.js
const MOUNT_NODE = document.getElementById('root')
let render = () => {
const routes = require('./routes/index').default(store)
ReactDOM.render(
<AppContainer
store={store}
history={history}
routes={routes}
/>,
MOUNT_NODE
)
}
/routes/index.js
import CoreLayout from '../layouts/CoreLayout/CoreLayout'
import Home from './Home'
import NestedChild from './NestedChild'
import TransitionWrapper from './TransitionWrapper'
export const createRoutes = (store) => ({
path: '/',
component: CoreLayout,
indexRoute: Home,
childRoutes: [{
component: TransitionWrapper,
childRoutes: [
NestedChild
]
}]
})
AppContainer.js
class AppContainer extends Component {
static propTypes = {
history: PropTypes.object.isRequired,
routes: PropTypes.object.isRequired,
store: PropTypes.object.isRequired
}
render () {
const { history, routes, store } = this.props
return (
<Provider store={store}>
<div style={{ height: '100%' }}>
<Router history={history} children={routes} />
</div>
</Provider>
)
}
}
export default AppContainer
CoreLayout.js
import React from 'react'
import Header from '../../components/Header'
import classes from './CoreLayout.scss'
import '../../styles/core.scss'
export const CoreLayout = ({ children }) => (
<div className='container text-center'>
<Header />
<div className={classes.mainContainer}>
{children}
</div>
</div>
)
CoreLayout.propTypes = {
children: React.PropTypes.element.isRequired
}
export default CoreLayout
TransitionWrappper.js <--- IS NOT RENDERING
const TransitionWrapper = (props) => (
<div className="im-not-working">
{this.props.children}
</div>
)
export default TransitionWrapper
NestedChild.js <--- IS NOT RENDERING
Have you tried just removing isRequired from the children prop of CoreLayout?
If you are loading your child components dynamically, there will be a period of time where the CoreLayout renders before you've got child components to put in it.

Categories