Can't navigate to URL param using nested routes - reactjs

I built a small example of nested routes using the useRoutes hook. I don't understand what I am doing different than the examples.
Here's a codesandbox.
I am unable to navigate to the :customerId URL param while in the 'customers' route.
import React from "react";
import { BrowserRouter, Link, RouteObject, useRoutes } from "react-router-dom";
const contentRoutes: RouteObject[] = [
{
element: <div>Home Page</div>,
index: true
},
{
element: (
<div>
Customers <Link to="microsoft">Microsoft</Link> (this is the link that doesn't work)
</div>
),
path: "customers",
children: [
{
path: ":customerId",
element: <div>Customer Microsoft</div>
}
]
}
];
const Content: React.FC = () => {
const content = useRoutes(contentRoutes);
return <div style={{ display: "flex" }}>{content}</div>;
};
export default function App() {
return (
<BrowserRouter>
<div className="App">
<div style={{ display: "flex", gap: "10px", marginBottom: '20px' }}>
<Link to="/">Home</Link>
<Link to="customers">Customers</Link>
</div>
<Content />
</div>
</BrowserRouter>
);
}

Looks like I haven't completely understood the concept of Outlet.
In the examples showing the Routes and Route React components, they use the Outlet component to display children routes.
In my example, I should just change to:
<div>
Customers <Link to="microsoft">Microsoft</Link>
<Outlet /> // HERE, use Outlet to paint children routes.
</div>

Related

React Router 6.4 nested deferred showing only parent loading indicator

I'm trying to understand how suspense / await work with nested routes.
The scenario I have:
I have a collection page that shows a list of items.
I have a detail page that shows the details of a single item (this page is a nested route)
What I want to achieve:
When the collection page is loaded, the page shows a waiting indicator while the data is being fetched
(I have added a random delay to simulate this).
Once the data is loaded, if I click in one of the items it will navigate to the nested detail page
(this page is also wrapped in a suspense / await), so we get on the left side the list and the right side
the detail page for the selected character, ... here I'm showing a loading indicator.
The expected behavior:
Collection loading indicator is shown when the collection page is loaded.
Detail loading indicator is shown when the detail page is loaded.
The actual behavior:
Collection loading indicator is shown when the collection page is loaded.
Detail loading indicator is shown when the detail page is loaded, plus list page is reloaded.
How I have defined the routes:
routes.tsx
import React from "react";
import { defer, createBrowserRouter } from "react-router-dom";
import { ListPage, DetailPage } from "./pages";
import { getCharacter, getCharacterCollection } from "./api";
export const applicationRoutes = createBrowserRouter([
{
path: "/",
loader: () => defer({ characterCollection: getCharacterCollection("") }),
element: <ListPage />,
children: [
{
path: ":id",
loader: ({ params }) =>
defer({ character: getCharacter(params.id as string) }),
element: <DetailPage />
}
]
}
]);
How I have implemented each page:
list.tsx
import React from "react";
import { Await, Link, Outlet, useLoaderData } from "react-router-dom";
import { Character } from "../api/model";
export const ListPage = () => {
const characters = useLoaderData() as { characterCollection: Character[] };
return (
<div>
<h2>Character Collection</h2>
<React.Suspense
fallback={<h4 style={{ color: "green" }}>👬 Loading characters...</h4>}
>
<Await resolve={characters.characterCollection}>
{(characters) => (
<div style={{ display: "flex", flexDirection: "row" }}>
<div>
<ul>
{characters.map((character) => (
<li key={character.id}>
<Link to={`./${character.id}`}>{character.name}</Link>
</li>
))}
</ul>
</div>
<Outlet />
</div>
)}
</Await>
</React.Suspense>
</div>
);
};
detail.tsx
import React from "react";
import { Await, useLoaderData } from "react-router-dom";
import { Character } from "../api/model";
export const DetailPage: React.FunctionComponent = () => {
const data = useLoaderData() as { character: Promise<Character> };
return (
<div>
<h1>Detail</h1>
<React.Suspense
fallback={<h4 style={{ color: "blue" }}> 👩🏻 Loading character...</h4>}
></React.Suspense>
<Await resolve={data.character}>
{(character) => (
<div>
<img src={character.image} alt={character.name} />
<h2>{character.name}</h2>
<p>{character.status}</p>
</div>
)}
</Await>
</div>
);
};
Codesandbox example: https://codesandbox.io/s/vigilant-agnesi-z7tvk1
It seems like the way that I'm using to navigate just forces a full reload:
<Link to={`./${character.id}`}>{character.name}</Link>
Instead of updating only loading the nested page, is this behavior by default? or am I doing something wrong?

Component does not display when clicking the navBar button

I created a NavBar component where I have some links.
I then call this component on my App.js, so I can display the NavBar.
When I click on a link I navigate to the route but the component is not displayed.
Here is my code:
MainNavigation.js
import {
AppBar,
Toolbar,
Typography,
Button,
} from "#material-ui/core";
import React from "react";
import { Link as RouterLink } from "react-router-dom";
import { useStyles } from "./styles";
function MainNavigation() {
const classes = useStyles();
const MyApp = (
<Typography variant="h6" component="h1" button='/'>
<RouterLink to='/' className={classes.logo}>MyApp</RouterLink>
</Typography>
);
const headersData = [
{
label: "First",
href: "/first",
},
{
label: "Second",
href: "/second"
}
];
const getMenuButtons = () => {
return headersData.map(({ label, href }) => {
return (
<Button
{...{
key: label,
color: "inherit",
to: href,
component: RouterLink,
className: classes.menuButton,
}}
>
{label}
</Button>
);
});
};
const displayDesktop = () => {
return (
<Toolbar className={classes.toolbar}>
{MyApp}
<div>{getMenuButtons()}</div>
</Toolbar>
);
};
return (
<header>
<AppBar className={classes.header}>{displayDesktop()}</AppBar>
</header>
);
}
export default MainNavigation;
App.js
import './App.css';
import { Route, Routes } from 'react-router-dom';
import Second from './pages/Second';
import First from './pages/First';
import Home from './pages/Home';
import MainNavigation from './components/layout/MainNavigation';
function App() {
return (
<div>
<MainNavigation/>
<Routes>
<Route path="/" exact element={<Home />} />
<Route path="/first" element={<First />} />
<Route path="/second" element={<Second />} />
</Routes>
</div>
);
}
export default App;
Second.js
function Second() {
return <div>Second</div>;
}
export default Second
The Component is called when I click on a link because I console logged just to check.
I don't know what is happening, since I don't get any errors, any clue?
I don't see any overt issues with your routing/navigation implementation, it's working just fine...
BUT
You should be aware that the AppBar component uses fixed positioning by default, so it actually overlays your content.
AppBar props
You can either use some other position value and make the app bar render with the content (probably not what you want in an app header), or you can app margin-top to your contents to push them down below the app bar header.
Example:
h1.header {
margin-top: 5rem;
height: 200vh;
}

How to dynamically create a React component based on url param?

I'd like my React component to generate based on the url param, in this case, the :id param. I'm struggling rendering the component. I have the different Components defined in separate files (ex. Loader.js, Radio button.js, Accordion menu.js).
Here's my (reduced for clarity) code that is continuously failing :)
import React from 'react';
import { Switch, Link, Route } from 'react-router-dom';
import Grid from '../Components/Grid'
function Overview () {
const components = [
{id: 'accordion-menu',
name: 'Accordion menu'},
{id: 'radio-button',
name: 'Radio button'},
{id: 'loader',
name: 'Loader'},
]
const componentPage = ({match}) => {
const findId = components.find((el) => {
match.params.id = el.id;
return findId.name;
}
)}
return (
<Router>
<div className="components">
<h3>Components</h3>
<p>This header and the menu will always appear on this page!</p>
<menu>
{components.map(({id, name}) => (
<li>
<Link to={`/components/${id}`}>{name}</Link>
</li>
))}
</menu>
<Switch>
<Route exact path={'/components/'} component={Grid}/>
<Route path={'/components/:id'} component={componentPage}/>
</Switch>
</div>
</Router>
)
}
export default Overview;
const componentPage = ({match}) => {
const findId = components.find((el) => {
match.params.id = el.id;
return findId.name;
}
)}
I think what you mean here is to return el.name not findId.name.
the other thing is you are trying to display the function as a component which won't work.

Set Antd's Menu defaultSelectedKeys value using React and Redux

I'm using antd for building a react application. I'm using antd's Layout to design the layout. Also, I'm using react-router for routing and redux for state management.
In antd's Menu I've set the defaultSelectedKeys to a blank array so that when page loads I don't want to show the active menu item. Instead, When the user accesses a certain page{Component) using application URL in the browser, then that page's menu item should be active.
For example, by default defaultSelectedKeys array will be blank. So no menu item will be active initially. When the application loads and react-router routes to the home component then the home menu item should be active.
For this, I'm using useEffect hook to dispatch an action with the menu key when a particular component is mounted.
Example Home Component:
import React, { Fragment, useEffect } from "react";
import { useDispatch } from "react-redux";
import { CURRENT_COMPONENT } from "./../reducers/types";
export default function HomeComponent() {
const dispatch = useDispatch();
useEffect(() => {
dispatch({
type: CURRENT_COMPONENT,
payload: { component: "Landing", sideBarMenuKey: "1" }
});
}, [dispatch]);
return (
<Fragment>
<h1>This is a home componet </h1>
<br />
...
<br />
<br />
...
<br />
<br />
...
<br />
</Fragment>
);
}
Up here in the useEffect I'm dispatching an action to the reducer with sideBarMenuKey: "1" and it is dispatching perfectly when this component mounts and able to receive the state change in Layout component as well using the react-redux useSelector hook.
Example Layout Component:
import React, { Fragment } from "react";
import { useSelector } from "react-redux";
import { Layout, Menu, Breadcrumb } from "antd";
import { Switch, Route } from "react-router-dom";
import HomeComponent from "./Home";
import AboutComponent from "./About";
const { Header, Content, Footer } = Layout;
export default function LayoutComponent() {
const sideBarMenuKey = useSelector(
state => state.currentComponetReducer.sideBarMenuItemKey
);
console.log(sideBarMenuKey);
return (
<Fragment>
<Layout className="layout">
<Header>
<div className="logo" />
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={[sideBarMenuKey]}
style={{ lineHeight: "64px" }}
>
<Menu.Item key="1">Home</Menu.Item>
<Menu.Item key="2">About</Menu.Item>
</Menu>
</Header>
<Content style={{ padding: "0 50px" }}>
<Breadcrumb style={{ margin: "16px 0" }}>
<Breadcrumb.Item>Home</Breadcrumb.Item>
<Breadcrumb.Item>List</Breadcrumb.Item>
<Breadcrumb.Item>App</Breadcrumb.Item>
</Breadcrumb>
<div style={{ background: "#fff", padding: 24, minHeight: 280 }}>
<Switch>
<Route exact path="/" component={HomeComponent} />
<Route exact path="/about" component={AboutComponent} />
</Switch>
</div>
</Content>
<Footer style={{ textAlign: "center" }}>
Ant Design ©2018 Created by Ant UED
</Footer>
</Layout>
</Fragment>
);
}
Even though I'm receiving the state change in the Layout Component and when I use sideBarMenuKey in antd's Menu prop as defaultSelectedKeys={[sideBarMenuKey]} the menu item doesn't get active state.
Sample Example:
I've created a sample codesandbox example. The link is below
https://codesandbox.io/s/styled-antd-react-starter-ytxko
I've created a Layout, Home and About components. Initially, when you log in it will show Home component. If you want to render the About component, then change the browser URL in the codesandbox to https://url/about. Both in Home and About component I'm dispatching an action with sideBarMenuKey and the state is also getting updated. But the menu item is not getting active.
Changing defaultSelectedKeys to selectedKeys should make it work:
<Menu
theme="dark"
mode="horizontal"
selectedKeys={[sideBarMenuKey]}
style={{ lineHeight: "64px" }}
>
<Menu.Item key="1">Home</Menu.Item>
<Menu.Item key="2">About</Menu.Item>
</Menu>
Putting NavLink inside the Menu.Item will allow you to switch from a nav tab to another.
<Menu
theme="dark"
mode="horizontal"
selectedKeys={[sideBarMenuKey]}
style={{ lineHeight: "64px" }}
>
<Menu.Item key="1">
<NavLink to="/">nav 1</NavLink>
</Menu.Item>
<Menu.Item key="2">
<NavLink to="/about">nav 1</NavLink>
</Menu.Item>
<Menu.Item key="3">nav 3</Menu.Item>
</Menu>
You can see it in action here
const [sideBarMenuKey, setSideBarMenuKey] = useState(1);
<Menu theme="dark" mode="inline" defaultSelectedKeys={[sideBarMenuKey]}>
<Menu.Item key="1" icon={<BirthIcon style={{ paddingRight: '10px' }} />}
onClick={ () => {
setSideBarMenuKey(1)
setShowBirth(true);
}
} >
Birth
</Menu.Item>
<Menu.Item key="2"
icon={<DeathIcon style={{ paddingRight: '10px' }} />}
onClick={ () => {
setSideBarMenuKey(2)
setShowBirth(false)
} }>
Death
</Menu.Item>
</Menu>
Using hooks look on to this

react router - pass props via url arguments

I am developing a multilingual and multi-currency app that uses react-router, react-router-redux & react-router-bootstrap for routing.
Currently users arrive at the app with a default language and currency however I'd like to be able to provide some arguments in the url to set either the language or currency - something along the lines of http://myapp.com/page1?lang='fr'
I'd also like to have the ability to use these arguments on any page, so that:
http://myapp.com/page1?lang='fr' and http://myapp.com/page2/example1/test?lang='ru' are both handled the same way. Basically I want the router to check for the presence of these arguments in any url that is passed to it, and if they are present execute a function.
I can't find any clear information about this in the documentation for react-router but I'm sure it's possible. Can anyone point me in the right direction?
You can access the query params via
this.props.location.query.langs from inside the component also mentioned by #Lucas.
I have provided a working example for better understanding.
Apart from that In your scenario I will suggest you to look into onEnter hooks available in react router.
let Router = ReactRouter.Router;
let RouterContext = Router.RouterContext;
let Route = ReactRouter.Route;
let Link = ReactRouter.Link;
function redirectBasedOnLang(nextState, replace) {
// check nextState.location.query
// and perform redirection or any thing else via
console.log("called");
console.log(nextState.location.query);
}
const Dashboard = (props) => {
return (
<b className="tag">{"Dashboard"}</b>
);
}
FrDashboard
const FrDashboard = (props) => {
return (
<div>
<h3> Dashboard </h3>
<hr/>
Primary language: {props.location.query.lang}
</div>
);
}
class App extends React.Component {
render() {
return (
<div>
<h2>App</h2>
{/* add some links */}
<ul>
<li><Link to={{ pathname: '/dashboard', query: { lang: 'fr' } }} activeClassName="active">Dashboard</Link></li>
</ul>
<div>
{this.props.children}
</div>
</div>
)
}
};
App.contextTypes = {
router: React.PropTypes.func.isRequired,
};
ReactDOM.render(<Router history={ReactRouter.hashHistory}>
<Route path="/" component={App}>
<Route path="dashboard" component={Dashboard} onEnter={redirectBasedOnLang} />
<Route path="dashboardfr" component={FrDashboard} />
</Route>
</Router>, document.getElementById('test'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-router/3.0.0/ReactRouter.min.js"></script>
<div id="test"></div>
let Router = ReactRouter.Router;
let RouterContext = Router.RouterContext;
let Route = ReactRouter.Route;
const Link = (props) => {
return (
<b className="lang" onClick={() => props.onClick(props.lang)}>{props.lang}</b>
);
}
class App extends React.Component {
constructor(props) {
super(props);
this.handleLang = this.handleLang.bind(this);
this.state = { langs: [] };
}
handleLang(lang) {
let langs = [].concat(this.state.langs);
if(langs.indexOf(lang) != -1) {
langs = langs.filter(item => item != lang);
}
else langs.push(lang);
this.setState({langs: langs});
this.context.router.push({
pathname: '/',
query: { langs: langs }
});
}
render() {
return (
<div>
<Link onClick={this.handleLang} lang={"fr "} />
<Link onClick={this.handleLang} lang={"en "} />
<Link onClick={this.handleLang} lang={"hi "} />
<div>
<hr/>
<br/>
active languague :
<div>{this.props.location.query.langs ? [].concat(this.props.location.query.langs).join(',') : ''}</div>
</div>
</div>
)
}
};
App.contextTypes = {
router: React.PropTypes.func.isRequired,
};
ReactDOM.render(<Router history={ReactRouter.hashHistory}>
<Route path="/" component={App} />
</Router>, document.getElementById('test'));
.lang {
display: inline-block;
background: #d3d3d3;
padding: 10px;
margin: 0 3px;
cursor: pointer;
width : 50px;
border : 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-router/3.0.0/ReactRouter.min.js"></script>
<div id="test"></div>
You can access the location prop passed by react-router to the component. You can see that it has a query property. In that way you can simply access this.props.location.query.lang and you'll get the value that's in your query string.
You can check this example in react-router project.

Resources