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

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.

Related

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

Render a function inside a component in react js

My target is to create a breadcrumb component in react js.
I used Ant Design in my App, but I have some issues with a parth of it.
In the documentation of Ant Design i found a solution for creating dynamic breadcrumbs, but can't find out how to apply the code.
Using, Ant design i built the next app:
import React from "react";
import {Link, BrowserRouter, Route} from "react-router-dom";
import {Breadcrumb} from 'antd';
//from here starts the code from Ant Design Documentation
const routes = [
{
path: 'index',
breadcrumbName: 'home',
},
{
path: 'first',
breadcrumbName: 'first',
children: [
{
path: '/general',
breadcrumbName: 'General',
},
{
path: '/layout',
breadcrumbName: 'Layout',
},
{
path: '/navigation',
breadcrumbName: 'Navigation',
},
],
},
{
path: 'second',
breadcrumbName: 'second',
},
];
function itemRender(route, params, routes, paths) {
const last = routes.indexOf(route) === routes.length - 1;
return last ? (
<span>{route.breadcrumbName}</span>
) : (
<Link to={paths.join('/')}>{route.breadcrumbName}</Link>
);
}
return <Breadcrumb itemRender={itemRender} routes={routes} />;
//here is the end of the code from Ant Design
function App() {
return (
<div>
<p>here i want to render my breadcrumb</p>
</div>
);
}
export default App;
Also, the return statement is located outside of the function and i get error due of this.
How to create, using this implementation, a breadcrumb, and how to render itemRender function inside my component, and from where should i get these params itemRender(route, params, routes, paths)?
You need to use Breadcrum component in App body
function App() {
return (
<div>
<Breadcrumb itemRender={itemRender} routes={routes} />
</div>
);
}

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

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
}
// ...
]
},
]
})

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.

React Router - new component not being loaded

So it's my first time setting something like this up and I'm struggling a little bit.
I used https://github.com/reactjs/react-router/tree/master/examples/huge-apps as my source of learning and am trying to set up a very basic layout for a react application.
What seems to be happening is that react isn't re-rendering after a path change therefore nothing ever gets added to the dom
When I click on the go to home component Link the URL bar changes but no DOM changes occur...
Here is my code [i'm leaving out my directory structure since i don't think it's important for the problem]
index.jsx: Load up the react app and get all routes
import 'babel-polyfill';
import React from 'react';
import { render } from 'react-dom';
import { Router, browserHistory } from 'react-router';
import Routes from './app/Routes.js';
render(
<Router
history={browserHistory}
routes={Routes}
/>,
document.querySelector('.js-mount-point')
);
Routes.js: Constant to keep all of my routes so that I don't have to manually specify them in index.js
import App from './App.jsx';
import Home from '../routes/Home/Home.js';
const Routes = {
path: '/',
component: App,
childRoutes: [
Home,
],
};
export default Routes;
App.jsx: Parent most component for my app
import React from 'react';
import { Link } from 'react-router';
const App = props => {
console.log(props);
return (
<div>
<h1>Hello World!</h1>
<p><Link to="#/home">Go to Home Component</Link></p>
{props.children}
</div>
);
};
export default App;
Home.js Grab all my route information and getComponent lives here
const HomeRoute = {
path: 'home',
title: 'Home',
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./HomeComponent.jsx').default);
});
},
};
export default HomeRoute;
HomeComponent.jsx my very basic home component
import React from 'react';
const HomeComponent = () => (
<div>
<h2>Welcome Home</h2>
</div>
);
export default HomeComponent;
Edit1: Made App.jsx pure function
Edit2: Fixed Routes.js
Edit3: Fixed Home.js
Edit4: Final fix,
const HomeComponent =
changed to
const HomeComponent = () => (
You App should be a component not a function returning an object. Right now you are mixing two approaches.
Either a function (stateless component)
import React from 'react';
import { Link } from 'react-router';
const App = props => {
console.log(props.children);
return (
<div>
<h1>Hello World!</h1>
<p><Link to="/home">Go to Home Component</Link></p>
{props.children}
</div>
);
};
export default App;
Or a statefull component that has render method
import React, { Component } from 'react';
import { Link } from 'react-router';
class App extends Component {
render() {
console.log(this.props.children);
return (
<div>
<h1>Hello World!</h1>
<p><Link to="/home">Go to Home Component</Link></p>
{this.props.children}
</div>
);
}
};
export default App;
Same with HomeComponent
const HomeComponent = () => (
<div>
<h2>Welcome Home</h2>
</div>
);
And you need to fix route config as well
const HomeRoute = {
path: 'home', //no # needed
title: 'Home',
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./HomeComponent.jsx').default);
});
},
};
Ohh, and I think you need.
const Routes = {
path: '/',
component: App,
childRoutes: [
Home
],
};

Categories